美文网首页
muduo 网络库关于线程的阅读记录

muduo 网络库关于线程的阅读记录

作者: wayyyy | 来源:发表于2021-03-17 23:01 被阅读0次
进程与线程

线程特点是共享地址空间,从而可以高效地共享数据。


单线程服务程序的编程模型 和 多线程服务程序的编程模型
  • 单线程服务程序的编程模型

    while(!done)
    {
        int timeout_ms = max(1000, getNextTimedCallback());
        int retval = ::poll(fds, nfds, timeout_ms);
        if (retval < 0) {
            // 处理错误
        }
        else {
            // 处理到期的timers,回调用户的timer handler
            if (retval > 0) {
                // 处理IO事件,回调用户的IO event handler
            }
        }
    }
    
  • 多线程服务程序的编程模型

    • 每个请求创建一个线程
    • 使用非阻塞IO + IO 复用
      • one loop per thread
        在这种模式下,程序里的每个IO线程都有一个Event loop。Event loop 代表了线程的主循环,需要哪个线程干活,就把 timer 或 IO channel 注册到哪个线程里的loop即可。
      • 线程池
        对于还有计算任务的服务程序来说,最好是使用blockingqueue实现的任务队列。
  • 单线程服务程序的优缺点

    • 单线程程序的优点:开发简单
    • 单线程程序的缺点:容易造成优先级反转。
      假如事件a的优先级高于事件b,处理事件a需要1ms,处理事件b需要10ms,如果事件b稍早于事件a发生,那么事件a则需要等待事件10ms才能被处理。
  • 适合用多线程的特点有:

    • 有多个CPU可用,单核使用多线程没有意义
    • 线程间有共享数据
    • 提供非均质的服务
      事件的响应有优先级差异,可以使用专门的线程来处理优先级高的事件
    • 利用异步操作
      比如logging,无论是往磁盘写log file,还是往log server发送消息都不应该阻塞
    • 能扩容
      一个好的多线程程序应该能享受增加CPU数目带来的好处。
  • 一个多线程服务程序中的线程可以分为3类:

    • IO线程
    • 计算线程
    • 第三方库使用的线程,比如 log 和 database 连接线程

多线程能提高并发度吗?

假如单纯采用 每个连接一个线程的 处理模型,那么并发连接数是极其有限的,Linux能同时创建的线程数量有限制。
但如果采用 one loop per thread,那么能轻轻松松处理很多个并发长连接。


多线程能提高吞吐量吗?


多线程能降低响应时间吗?


多线程程序 如何让IO和计算相互重叠,降低latency

基本思路是,把IO操作通过blockingqueue交给别的线程去做。比如:logging


什么是线程池大小阻抗匹配原则

如果池中线程在执行任务时,密集计算所占的时间比重为P,而系统一共有C个CPU,为了让这个CPU跑满而又不过载,线程池大小的经验公式是:

T = \frac{C}{P}

假设 C = 8,P = 1.0,线程池的任务完全是密集计算,那么 T = 8,只要8个活动线程就能让8个CPU饱和,再多也没用,因为CPU资源已经耗光了。假设 C = 8,P = 0.5,线程池的任务有一半是计算,有一半在等待IO上,那么 T = 16,考虑OS调度的灵活性,16个50%繁忙的线程能让CPU跑满,启动更多的线程并不能提高吞吐量。


基本线程原语选择

posix threads 的函数有110个,真正常用的不过十几个。主要常用的有:

  • 2个:线程创建和等待结束
  • 4个:std::mutex的创建,销毁,加锁,解锁
  • 5个:条件变量的创建,销毁,等待,通知,广播

利用其上封装为count_down_latch,多多利用高级的并发构件。
避免使用信号量,它的功能和条件变量重合,且容易用错。
避免使用读写锁,实际上读写锁的性能并不见得有多高。


线程标识符

posix thread库提供了pthread_self函数用于返回当前线程标识符,其类型为pthread_t
pthread_t 不一定是一个数值类型,也有可能是一个结构体。

  • 无法打印输出pthread_t,没法在日志中使用
  • 无法比较pthread_t的大小或者计算其hash,因此没法用作关联容器的key
  • 无法定义一个pthread_t
  • pthread_t值只在进程内有意义,与操作系统的任务调度之间无法建立有效关系
  • 另外glibc的 pthread 实现实际上把 pthread_t 用作一个结构体指针,指向一块动态分配的内存
    int main()
    {
        pthread_t t1, t2; 
    
        pthread_create(&t1, NULL, threadFunc, NULL);
        printf("%lx\n", t1);
        pthread_join(t1, NULL);
    
        pthread_create(&t2, NULL, threadFunc, NULL);
        printf("%lx\n", t2);
        pthread_join(t2, NULL);
    }
    
    上面程序输出的是一样的值。

在Linux上,建议使用gettid()系统调用的返回值作为线程id,这么做的好处有:

  • 它的类型是pid_t,其值通常是一个整数,可以在日志中输出。
  • 在Linux中,它直接表示内核的任务调度ID。
  • 在其他系统工具中可以很容易定位到具体的某一个线程,例如在top中可以找出CPU使用率较高的线程ID。
  • 任何时刻都是全局唯一的。
  • 0是非法值,因为第一个进程init的pid 是1。

线程的创建与销毁守则
  • 在进入main函数之前不应该启动线程
    因为会影响到全局对象的安全构造,C++保证在进入main之前完成全局对象的构造,但是,各个编译单元之间的对象构造顺序是不确定的。

  • 程序中线程的创建最好能在初始化阶段全部完成


多线程与IO

在进行多线程网络编程的时候,一个自然的问题是:如何处理IO?能否多个线程同时读写同一个socket文件描述符?

首先,操作文件描述符的系统调用本身是线程安全的,但是多个线程同时操作同一个socket 文件描述符是很麻烦的,需要考虑:

  • 如果一个线程正在阻塞地read 某个socket,而另一个线程close了这个socket。
  • 如果一个线程正在阻塞地accept某个socket,而另一个线程close了这个socket。
  • 一个线程正准备read某个socket,而另一个线程close了这个socket,第三个线程又恰好open了另一个文件描述符,其fd号码正好与前面地socket相同,而这显然程序地逻辑都不对了。
  • 如果两个线程同时read同一个TCP socket,两个线程几乎同时各自收到一部分数据,如何把数据拼成完整地消息?如何知道哪部分数据先到达。

所以为了简单起见,多线程程应该遵循的原则是:每个文件描述符只由一个线程操作,从而轻松解决消息收发的顺序性问题,也避免关闭文件描述符的各种race condition


善用__thread 关键字

__thread 是gcc内置的线程局部存储设施,它的使用规则:

  • 只能用于修饰POD类型,不能修饰class类型,因为无法自动调用构造函数和析构函数
  • __thread 可以用于修饰全局变量,函数内的静态变量,但是不能用于修饰函数的局部变量或者class的普通成员变量
  • __thread 变量的初始化只能用于编译期常量

__thread 变量保证每个线程有一份独立的实体,各个线程的变量值互不干扰。

exit 在C++中不是线程安全的

相关文章

网友评论

      本文标题:muduo 网络库关于线程的阅读记录

      本文链接:https://www.haomeiwen.com/subject/whciqltx.html