mini-muduo版本号传送门

version 0.00 从epoll构建muduo-1 mini-muduo介绍

version 0.01 从epoll构建muduo-2 最简单的epoll

version 0.02 从epoll构建muduo-3 增加第一个类。顺便介绍reactor

version 0.03 从epoll构建muduo-4 增加Channel

version 0.04 从epoll构建muduo-5 增加Acceptor和TcpConnection

version 0.05 从epoll构建muduo-6 增加EventLoop和Epoll

version 0.06 从epoll构建muduo-7 增加IMuduoUser

version 0.07 从epoll构建muduo-8 增加发送缓冲区和接收缓冲区

version 0.08 从epoll构建muduo-9 增加onWriteComplate回调和Buffer

version 0.09 从epoll构建muduo-10 Timer定时器

version 0.11 从epoll构建muduo-11 单线程Reactor网络模型成型

version 0.12 从epoll构建muduo-12 多线程代码入场

version 0.13 从epoll构建muduo-13 Reactor + ThreadPool 成型

mini-muduo v0.12版本号。mini-muduo完整可执行的演示样例可从github下载,使用命令git checkout v0.12可切换到此版本号。在线浏览此版本号到这里

本版本号是个过度版本号,仅仅简单引入了线程相关的C++代码,并没有将多线程引入逻辑流程。写这篇Blog主要目的是温习一下Linux多线程编程。下一个版本号将彻底改动网络IO模块(EventLoop)。本Blog的大部分内容在陈硕的书中均有涉及。

主要介绍5个最重要的类

1 Mutex

以下几个关键点:

1 使用RAII手法封装mutex的创建,销毁,加锁解锁四个操作。RAII是C++程序猿的最佳伴侣。

2 推荐使用非递归的mutex,缺省即是非递归。对于非递归的mutex,假设书写思路不够清晰,会导致死锁。引起警惕。对于非递归mutex。则会概率出问题。反而更难定位。

书中比喻非常贴切:“反正是死,不如死的有意义一点,留个全尸让验尸更easy些”。

3 死锁的两种经典场景

1 仅仅有一个Mutex,自己锁自己。比方A::x()和A::y()方法都使用了同一个Mutex进行了加锁,单独调用A::x()和A::y()都不会出问题,可是假设A::x()的实现里不小心要调用到A::y()就会发生死锁。

2 有两个Mutex,互相锁。比方A,B两个线程里分别依照相反的顺序锁住了MutexA和MutexB两个相互排斥量。像以下这样,会造成典型的死锁。注意并不是相反顺序就一定造成死锁,比方运行序列2尽管是相反顺序上锁,可是不会造成死锁。

死锁:  时间递增  -------------->

A线程      lock(MutexA)                           lock(MutexB)

B线程                               lock(MutexB)                         lock(MutexA)

非死锁:时间递增  -------------->

A线程      lock(MutexA)    lock(MutexB)

B线程                                                      lock(MutexB)  lock(MutexA)

2 Condition

引用BlockingQueue的实现来说明Condition的使用,BlockingQueue是一个经典的生产者消费者数据结构。dequeue()方法由消费者调用,从集合里取出数据。

enqueue()由生产者调用,将数据放入集合。

Condition的使用有2个关键点:

1 必须与mutex一起使用

该布尔表达式的读写需受此mutex的保护,參考一下pthread_cond_wait()的实现能够更好的理解这一点,pthread_cond_wait()会把以下两个动作作为原子操作来运行 1解锁mutex 2 让线程进入不消耗CPU的睡眠。之后另外的线程调用pthread_cond_noitfy()。wait()等待线程被唤醒 ,此时pthread_cond_wait()还未返回,在wait()函数体里会运行mutex加锁操作,此后wait()返回。(个人判断。这里的唤醒和运行mutex加锁操作并不是原子操作。详见后面BlockingQueue节的分析)。

2 while測试

while測试是为了解决虚假唤醒(spurious wakeup)。

在多核处理器中,某些时候。wait()操作会在没有调用broadcast和signal的情况下被意外唤醒。

这样的解释听上去非常牵强也非常奇怪,怎么看都像Linux的一个Bug。这当然不是一个bug,依据wikipedia的解释,出现这样的问题的解决办法是:在某些多核处理器上,制作一个行为可预期的条件变量(condition variable)会明显拖慢全部的条件变量。限于眼下没有精力阅读内核源代码的情况下,能够这样理解spurious
wakeup:它是操作系统在性能和易用性之间妥协的结果,spurious wakeup问题类似于“c语言中数组没有过界检查”这种问题。现实情况非常可能是要牺牲相当的操作系统总体性能(如果10%)来换取一个完美的的条件变量实现。显然这么做是不划算的。wikipedia原话是“Spurious wakeups may sound strange,
but on some multiprocessor systems, making condition wakeup completely predictable might substantially slow all condition variable operations.”。

int dequeue()
{
MutexLockGuard lock(mutex); --------------------------
while(queue.empty()) / 以下是 pthread_cond_wait的实现。1,2是原子过程
{ | 1 pthread_cond_unlock(mutex)
cond.wait(); -------| 2 等待...
} | 3 notify()被调用,本线程被增加就绪队列(不一定立马获得CPU)
int top = queue.front(); \ 4 pthread_cond_lock(mutex)
queue.pop_front(); ---------------------------
return top;
}
void enqueue(int x)
{
MutexLockGuard lock(mutex)
queue.push_back(x);
cond.notify();
}

3 BlockingQueue

仅仅有一点要注意,那就是调用notify的时机,并非当queue的size()从0变到1的时候才调用,而是每次push_back都会调用。

依照陈硕的描写叙述。“push 第一个 task 的时候,会唤醒一个消费者。可是这个消费者线程不一定会立即运行。它会进入就绪状态,等待被调度。假设操作系统继续运行生产者线程,push 第二个 task,那么依照你的算法(也就是size()从0变为1时才调用notify())。不会有新的消费者被唤醒。这样一来。在 queue_ 再次变空之前。仅仅有一个消费者醒过来处理
tasks。其余 4 个都在睡大觉。Stevens 的书上仅仅有一个消费者,它的代码不适用于多消费者的情形“。

这里的意思就是说,pthread_cond_wait里的两个操作是不正确等的,(1解锁2挂起,是一对原子操作,3唤醒4加锁,不是原子操作)。所以当notify通知过来的时候,一个线程被唤醒,添加到可运行队列,可是可能没有被马上运行,因为3唤醒4加锁不是原子操作,假设在3和4之间发生了cpu调度,cpu被派去运行新的生产者线程。这个生产者通过调用enqueue()添加一个可用项,这时发现size() !=
1 于是就不通知了。这次通知事件就丢失了!

4 Thread

线程标识符用tid,而不要使用pthread_t,原因例如以下:

1 pthread_t 不一定是一个数值类型。也可能是一个结构体,所以无法打印输出pthread_t。由于不知道其确切类型,无法比較pthread_t的大小,因此无法用作关联容器的key。glibc的Pthread实现是把pthread_t用作一个结构体指针,指向一块动态分配的内存,并且这块内存是反复使用的,这造成pthread_t的值非常easy反复,像以下的代码。而作为对比的tid是一个小整数。便于输出。tid是递增的,仅仅有经过一段时间递增超过最大值(默认32768)才会反复。

2 pthread_t 值仅仅在进程内有意义。与操作系统的任务调度之间无法建立有效关系。在/proc文件系统中找不到pthread_t相应的task。而作为对比的tid,能够在/proc中找到相应项,也能够使用top命令直接查看。很便于程序调试和排错。

注意。因为gettid是系统调用。为了提高效率,使用__thread变量缓存gettid()。

(注,mini-muduo当前v0.12版本号尚未加入此缓存功能)

//反复phread_t演示样例
in main()
{
pthread_t t1 t2;
pthread_create(&t1, NULL, htreadFunc, NULL);
printf(%lx\n", t1);
pthread_jion(t1, NULL); pthread_create(&2, NULL, threadFunc, NULL);
printf("%lx\n", t2);
pthread_jion(t2, NULL);
}

5 ThreadPool

这是个简化的线程池里,直接使用了上面介绍的BlockingQueue作为生产者/消费者容器。这个线程池和线程函数一样,仅仅能启动不能关闭,为了使代码简化,我仅仅保留了最重要的部分。

从epoll构建muduo-12 多线程入场的更多相关文章

  1. 从epoll构建muduo-13 Reactor + ThreadPool 成型

    mini-muduo版本号传送门 version 0.00 从epoll构建muduo-1 mini-muduo介绍 version 0.01 从epoll构建muduo-2 最简单的epoll ve ...

  2. 从epoll构建muduo-11 单线程Reactor网络模型成型

    mini-muduo版本传送门 version 0.00 从epoll构建muduo-1 mini-muduo介绍 version 0.01 从epoll构建muduo-2 最简单的epoll ver ...

  3. core java 10~12(多线程 & I/O & Network网络编程)

    MODULE 10 Threads 多线程-------------------------------- 进程: 计算机在运行过程中的任务单元,CPU在一个时间点上只能执行一个进程,但在一个时间段上 ...

  4. python基础-12 多线程queue 线程交互event 线程锁 自定义线程池 进程 进程锁 进程池 进程交互数据资源共享

    Python中的进程与线程 学习知识,我们不但要知其然,还是知其所以然.你做到了你就比别人NB. 我们先了解一下什么是进程和线程. 进程与线程的历史 我们都知道计算机是由硬件和软件组成的.硬件中的CP ...

  5. java 22 - 12 多线程之解决线程安全问题的实现方式1

    从上一章知道了多线程存在着线程安全问题,那么,如何解决线程安全问题呢? 导致出现问题的原因: A:是否是多线程环境 B:是否有共享数据 C:是否有多条语句操作共享数据 上一章的程序,上面那3条都具备, ...

  6. 12 - 多线程、执行队列、GCD

    一.多线程 进程:一个应用程序配套一个进程,进程会加载应用程序的资源,进程是放代码的,一个进程默认是一个线程(主线程),可以有多个线程 线程:执行代码的是线程,一个线程同时只能读取一段代码 栈里的变量 ...

  7. Java简明教程 12.多线程(multithreading)

    单线程和多线程 关于它们的区别,zhihu上有一个回答,我认为十分不错,如下: . 单进程单线程:一个人在一个桌子上吃菜. . 单进程多线程:多个人在同一个桌子上一起吃菜. . 多进程单线程:多个人每 ...

  8. 我理解的epoll(三)多线程模式下的ET

    ET模式下,需要循环从缓存中读取,直到返回EAGAIN没有数据可读后,一个被通知的事件才算结束.如果还读取过程中,同一个连接又有新的事件到来,触发其他线程处理同一个socket,就乱了.EPOLL_O ...

  9. 【UE4 C++ 基础知识】<12> 多线程——FRunnable

    概述 UE4里,提供的多线程的方法: 继承 FRunnable 接口创建单个线程 创建 AsyncTask 调用线程池里面空闲的线程 通过 TaskGraph 系统来异步完成一些自定义任务 支持原生的 ...

随机推荐

  1. JS - caller,callee,call,apply [transfer] aA ==> apply uses an array [] as the second argument. call uses different argument.

    在提到上述的概念之前,首先想说说javascript中函数的隐含参数:arguments Arguments : 该对象代表正在执行的函数和调用它的函数的参数. [function.]argument ...

  2. Android驱动之 Linux Input子系统之TP——A/B(Slot)协议【转】

    转自:http://www.thinksaas.cn/topics/0/646/646797.html 将A/B协议这部分单独拿出来说一方面是因为这部分内容是比较容易忽视的,周围大多数用到input子 ...

  3. rpm安装与卸载命令

    linux删除目录(文件夹):rmdir 目录名(目录需非空):直接删除可用: rm -rf 目录名 ,不需考虑是否为空 SecureCRT上传文件:rz  ,下载文件:sz rpm 安装:rpm - ...

  4. (10)centos搭建web服务器 (Nginx+ django)

    安装 python3 sudo yum install python34 安装uWSGI pip install uwsgi 安装 Nginx http://nginx.org/en/download ...

  5. 从壹开始 [ Ids4实战 ] 之四 ║ 用户数据管理 & 前后端授权联调

    前言 哈喽~~~ 大家周一好!夏天到了,大家舒服了没有,熟话说,打败你的不是天真,是天真热!

  6. 转:IAdaptable & IAdapterFactory

    IAdaptable & IAdapterFactory在Eclipse中使用IAdaptable接口的方式有两种 在Eclipse中使用IAdaptable接口的方式有两种1:某个类希望提供 ...

  7. 串口调试利器--Minicom配置及使用详解

    因为现在电脑基本不配备串行接口,所以,usb转串口成为硬件调试时的必然选择.目前知道的,PL2303的驱动是有的,在dev下的名称是ttyUSB*. Minicom,是Linux下应用比较广泛的串口软 ...

  8. main函数参数

    方法1. C/C++语言中的main函数,经常带有参数argc,argv,如下: int main(int argc, char** argv) int main(int argc, char* ar ...

  9. 缠中说禅股票交易系统图解 z

    缠中说禅股票交易系统图解 2010-03-23 10:51 (王纯阳)缠论祖师的经典语录 1. 就在买点买,卖点卖:当然,买点并不一定是一个点,一个价位,级别越大的,可以容忍的区间越大. 2. 你要经 ...

  10. 2017.2.28 activiti实战--第六章--任务表单(一)动态表单

    学习资料:<Activiti实战> 第六章 任务表单(一)动态表单 内容概览:本章要完成一个OA(协同办公系统)的请假流程的设计,从实用的角度,讲解如何将activiti与业务紧密相连. ...