出处:http://blog.csdn.net/pizi0475/article/details/17627631
【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】
(基础篇)
多线程编程是现代软件技术中很重要的一个环节。要弄懂多线程,这就要牵涉到多进程?当然,要了解到多进程,就要涉及到操作系统。不过大家也不要紧张,听我慢慢道来。这其中的环节其实并不复杂。
(1)单CPU下的多线程
在没有出现多核CPU之前,我们的计算资源是唯一的。如果系统中有多个任务要处理的话,那么就需要按照某种规则依次调度这些任务进行处理。什么规则呢?可以是一些简单的调度方法,比如说
1)按照优先级调度
2)按照FIFO调度
3)按照时间片调度等等
当然,除了CPU资源之外,系统中还有一些其他的资源需要共享,比如说内存、文件、端口、socket等。既然前面说到系统中的资源是有限的,那么获取这些资源的最小单元体是什么呢,其实就是进程。
举个例子来说,在linux上面每一个享有资源的个体称为task_struct,实际上和我们说的进程是一样的。我们可以看看task_struct(linux 0.11代码)都包括哪些内容,
- struct task_struct {
-
- long state;
- long counter;
- long priority;
- long signal;
- struct sigaction sigaction[32];
- long blocked;
-
- int exit_code;
- unsigned long start_code,end_code,end_data,brk,start_stack;
- long pid,father,pgrp,session,leader;
- unsigned short uid,euid,suid;
- unsigned short gid,egid,sgid;
- long alarm;
- long utime,stime,cutime,cstime,start_time;
- unsigned short used_math;
-
- int tty;
- unsigned short umask;
- struct m_inode * pwd;
- struct m_inode * root;
- struct m_inode * executable;
- unsigned long close_on_exec;
- struct file * filp[NR_OPEN];
-
- struct desc_struct ldt[3];
-
- struct tss_struct tss;
- };
- struct task_struct {
-
- long state;
- long counter;
- long priority;
- long signal;
- struct sigaction sigaction[32];
- long blocked;
-
- int exit_code;
- unsigned long start_code,end_code,end_data,brk,start_stack;
- long pid,father,pgrp,session,leader;
- unsigned short uid,euid,suid;
- unsigned short gid,egid,sgid;
- long alarm;
- long utime,stime,cutime,cstime,start_time;
- unsigned short used_math;
-
- int tty;
- unsigned short umask;
- struct m_inode * pwd;
- struct m_inode * root;
- struct m_inode * executable;
- unsigned long close_on_exec;
- struct file * filp[NR_OPEN];
-
- struct desc_struct ldt[3];
-
- struct tss_struct tss;
- };
每一个task都有自己的pid,在系统中资源的分配都是按照pid进行处理的。这也就说明,进程确实是资源分配的主体。
这时候,可能有朋友会问了,既然task_struct是资源分配的主体,那为什么又出来thread?为什么系统调度的时候是按照thread调度,而不是按照进程调度呢?原因其实很简单,进程之间的数据沟通非常麻烦,因为我们之所以把这些进程分开,不正是希望它们之间不要相互影响嘛。
假设是两个进程之间数据传输,那么需要如果需要对共享数据进行访问需要哪些步骤呢,
1)创建共享内存
2)访问共享内存->系统调用->读取数据
3)写入共享内存->系统调用->写入数据
要是写个代码,大家可能就更明白了,
- #include <unistd.h>
- #include <stdio.h>
-
- int value = 10;
-
- int main(int argc, char* argv[])
- {
- int pid = fork();
- if(!pid){
- Value = 12;
- return 0;
- }
- printf("value = %d\n", value);
- return 1;
- }
- #include <unistd.h>
- #include <stdio.h>
-
- int value = 10;
-
- int main(int argc, char* argv[])
- {
- int pid = fork();
- if(!pid){
- Value = 12;
- return 0;
- }
- printf("value = %d\n", value);
- return 1;
- }
上面的代码是一个创建子进程的代码,我们发现打印的value数值还是10。尽管中间创建了子进程,修改了value的数值,但是我们发现打印下来的数值并没有发生改变,这就说明了不同的进程之间内存上是不共享的。
那么,如果修改成thread有什么好处呢?其实最大的好处就是每个thread除了享受单独cpu调度的机会,还能共享每个进程下的所有资源。要是调度的单位是进程,那么每个进程只能干一件事情,但是进程之间是需要相互交互数据的,而进程之间的数据都需要系统调用才能应用,这在无形之中就降低了数据的处理效率。
(2)多核CPU下的多线程
没有出现多核之前,我们的CPU实际上是按照某种规则对线程依次进行调度的。在某一个特定的时刻,CPU执行的还是某一个特定的线程。然而,现在有了多核CPU,一切变得不一样了,因为在某一时刻很有可能确实是n个任务在n个核上运行。我们可以编写一个简单的open mp测试一下,如果还是一个核,运行的时间就应该是一样的。
- #include <omp.h>
- #define MAX_VALUE 10000000
-
- double _test(int value)
- {
- int index;
- double result;
-
- result = 0.0;
- for(index = value + 1; index < MAX_VALUE; index +=2 )
- result += 1.0 / index;
-
- return result;
- }
-
- void test()
- {
- int index;
- int time1;
- int time2;
- double value1,value2;
- double result[2];
-
- time1 = 0;
- time2 = 0;
-
- value1 = 0.0;
- time1 = GetTickCount();
- for(index = 1; index < MAX_VALUE; index ++)
- value1 += 1.0 / index;
-
- time1 = GetTickCount() - time1;
-
- value2 = 0.0;
- memset(result , 0, sizeof(double) * 2);
- time2 = GetTickCount();
-
- #pragma omp parallel for
- for(index = 0; index < 2; index++)
- result[index] = _test(index);
-
- value2 = result[0] + result[1];
- time2 = GetTickCount() - time2;
-
- printf("time1 = %d,time2 = %d\n",time1,time2);
- return;
- }
- #include <omp.h>
- #define MAX_VALUE 10000000
-
- double _test(int value)
- {
- int index;
- double result;
-
- result = 0.0;
- for(index = value + 1; index < MAX_VALUE; index +=2 )
- result += 1.0 / index;
-
- return result;
- }
-
- void test()
- {
- int index;
- int time1;
- int time2;
- double value1,value2;
- double result[2];
-
- time1 = 0;
- time2 = 0;
-
- value1 = 0.0;
- time1 = GetTickCount();
- for(index = 1; index < MAX_VALUE; index ++)
- value1 += 1.0 / index;
-
- time1 = GetTickCount() - time1;
-
- value2 = 0.0;
- memset(result , 0, sizeof(double) * 2);
- time2 = GetTickCount();
-
- #pragma omp parallel for
- for(index = 0; index < 2; index++)
- result[index] = _test(index);
-
- value2 = result[0] + result[1];
- time2 = GetTickCount() - time2;
-
- printf("time1 = %d,time2 = %d\n",time1,time2);
- return;
- }
(3)多线程编程
为什么要多线程编程呢?这其中的原因很多,我们可以举例解决
1)有的是为了提高运行的速度,比如多核cpu下的多线程
2)有的是为了提高资源的利用率,比如在网络环境下下载资源时,时延常常很高,我们可以通过不同的thread从不同的地方获取资源,这样可以提高效率
3)有的为了提供更好的服务,比如说是服务器
4)其他需要多线程编程的地方等等
(之数据同步)
多线程创建其实十分简单,在windows系统下面有很多函数可以创建多线程,比如说_beginthread。我们就可以利用它为我们编写一段简单的多线程代码,
- #include <windows.h>
- #include <process.h>
- #include <stdio.h>
-
- unsigned int value = 0;
-
- void print(void* argv)
- {
- while(1){
- printf("&value = %x, value = %d\n", &value, value);
- value ++;
- Sleep(1000);
- }
- }
-
- int main()
- {
- _beginthread( print, 0, NULL );
- _beginthread( print, 0, NULL);
-
- while(1)
- Sleep(0);
-
- return 1;
- }
- #include <windows.h>
- #include <process.h>
- #include <stdio.h>
-
- unsigned int value = 0;
-
- void print(void* argv)
- {
- while(1){
- printf("&value = %x, value = %d\n", &value, value);
- value ++;
- Sleep(1000);
- }
- }
-
- int main()
- {
- _beginthread( print, 0, NULL );
- _beginthread( print, 0, NULL);
-
- while(1)
- Sleep(0);
-
- return 1;
- }
注意,在VC上面编译的时候,需要打开/MD开关。具体操作为,【project】->【setting】->【c/c++】->Category【Code Generation】->【Use run-time library】->【Debug Multithreaded】即可。
通过上面的示例,我们看到作为共享变量的value事实上是可以被所有的线程访问的。这就是线程数据同步的最大优势——方便,直接。因为线程之间除了堆栈空间不一样之外,代码段和数据段都是在一个空间里面的。所以,线程想访问公共数据,就可以访问公共数据,没有任何的限制。
当然,事物都有其两面性。这种对公共资源的访问模式也会导致一些问题。什么问题呢?我们看了就知道了。
现在假设有一个池塘,我们雇两个人来喂鱼。两个人不停地对池塘里面的鱼进行喂食。我们规定在一个人喂鱼的时候,另外一个人不需要再喂鱼,否则鱼一次喂两回就要撑死了。为此,我们安装了一个牌子作为警示。如果一个人在喂鱼,他会把牌子设置为FALSE,那么另外一个人看到这个牌子,就不会继续喂鱼了。等到这个人喂完后,他再把牌子继续设置为TRUE。
如果我们需要把这个故事写成代码,那么怎么写呢?朋友们试试看,
- while(1){
- if( flag == true){
- flag = false;
- do_give_fish_food();
- flag = true;
- }
-
- Sleep(0);
- }
- while(1){
- if( flag == true){
- flag = false;
- do_give_fish_food();
- flag = true;
- }
-
- Sleep(0);
- }
上面的代码看上去没有问题了,但是大家看看代码的汇编代码,看看是不是存在隐患。因为还会出现两个人同时喂食的情况,
- 23: while(1){
- 004010E8 mov eax,1
- 004010ED test eax,eax
- 004010EF je do_action+56h (00401126)
- 24: if( flag == true){
- 004010F1 cmp dword ptr [flag (00433e04)],1
- 004010F8 jne do_action+43h (00401113)
- 25: flag = false;
- 004010FA mov dword ptr [flag (00433e04)],0
- 26: do_give_fish_food();
- 00401104 call @ILT+15(do_give_fish_food) (00401014)
- 27: flag = true;
- 00401109 mov dword ptr [flag (00433e04)],1
- 28: }
- 29:
- 30: Sleep(0);
- 00401113 mov esi,esp
- 00401115 push 0
- 00401117 call dword ptr [__imp__Sleep@4 (004361c4)]
- 0040111D cmp esi,esp
- 0040111F call __chkesp (004011e0)
- 31: }
- 00401124 jmp do_action+18h (004010e8)
- 32: }
- 23: while(1){
- 004010E8 mov eax,1
- 004010ED test eax,eax
- 004010EF je do_action+56h (00401126)
- 24: if( flag == true){
- 004010F1 cmp dword ptr [flag (00433e04)],1
- 004010F8 jne do_action+43h (00401113)
- 25: flag = false;
- 004010FA mov dword ptr [flag (00433e04)],0
- 26: do_give_fish_food();
- 00401104 call @ILT+15(do_give_fish_food) (00401014)
- 27: flag = true;
- 00401109 mov dword ptr [flag (00433e04)],1
- 28: }
- 29:
- 30: Sleep(0);
- 00401113 mov esi,esp
- 00401115 push 0
- 00401117 call dword ptr [__imp__Sleep@4 (004361c4)]
- 0040111D cmp esi,esp
- 0040111F call __chkesp (004011e0)
- 31: }
- 00401124 jmp do_action+18h (004010e8)
- 32: }
我们此时假设有两个线程a和b在不停地进行判断和喂食操作。设置当前flag = true,此时线程a执行到004010F8处时,判断鱼还没有喂食,正准备执行指令004010F8,但是还没有来得及对falg进行设置,此时出现了线程调度。线程b运行到004010F8时,发现当前没有人喂食,所以执行喂食操作。等到b线程喂食结束,运行到00401113的时候,此时又出现了调度。线程a有继续运行,因为之前已经判断了当前还没有喂食,所以线程a继续进行了喂食了操作。所以,可怜的鱼,这一次就连续经历了两次喂食操作,估计有一部分鱼要撑死了。
当然鱼在这里之所以会出现撑死的情况,主要是因为line 24和line 25之间出现了系统调度。所以,我们在编写程序的时候必须有一个牢固的思想意识,如果缺少必须要的手段,程序可以任何时刻任何地点被调度,那此时公共数据的计算就会出现错误。
那么有没有方法避免这种情况的发生呢?当然有。朋友们可以继续关注下面的博客。
(之数据互斥)
在多线程存在的环境中,除了堆栈中的临时数据之外,所有的数据都是共享的。如果我们需要线程之间正确地运行,那么务必需要保证公共数据的执行和计算是正确的。简单一点说,就是保证数据在执行的时候必须是互斥的。否则,如果两个或者多个线程在同一时刻对数据进行了操作,那么后果是不可想象的。
也许有的朋友会说,不光数据需要保护,代码也需要保护。提出这个观点的朋友只看到了数据访问互斥的表象。在程序的运行空间里面,什么最重要的呢?代码吗?当然不是。代码只是为了数据的访问存在的。数据才是我们一切工作的出发点和落脚点。
那么,有什么办法可以保证在某一时刻只有一个线程对数据进行操作呢?四个基本方法:
(1)关中断
(2)数学互斥方法
(3)操作系统提供的互斥方法
(4)cpu原子操作
为了让大家可以对这四种方法有详细的认识,我们可以进行详细的介绍。
(1)关中断
要让数据在某一时刻只被一个线程访问,方法之一就是停止线程调度就可以了。那么怎样停止线程调度呢?那么关掉时钟中断就可以了啊。在X86里面的确存在这样的两个指令,
- #include <stdio.h>
-
- int main()
- {
- __asm{
- cli
- sti
- }
- return 1;
- }
- #include <stdio.h>
-
- int main()
- {
- __asm{
- cli
- sti
- }
- return 1;
- }
其中cli是关中断,sti是开中断。这段代码没有什么问题,可以编过,当然也可以生成执行文件。但是在执行的时候会出现一个异常告警:Unhandled exception in test.exe: 0xC0000096: Privileged Instruction。告警已经说的很清楚了,这是一个特权指令。只有系统或者内核本身才可以使用这个指令。
不过,大家也可以想象一下。因为平常我们编写的程序都是应用级别的程序,要是每个程序都是用这些代码,那不乱了套了。比如说,你不小心安装一个低质量的软件,说不定什么时候把你的中断关了,这样你的网络就断了,你的输入就没有回应了,你的音乐什么都没有了,这样的环境你受的了吗?应用层的软件是千差万别的,软件的水平也是参差不齐的,所以系统不可能相信任何一个私有软件,它相信的只是它自己。
(2)数学方法
假设有两个线程(a、b)正要对一个共享数据进行访问,那么怎么做到他们之间的互斥的呢?其实我们可以这么做,
- unsigned int flag[2] = {0};
- unsigned int turn = 0;
-
- void process(unsigned int index)
- {
- flag[index] = 1;
- turn = index;
-
- while(flag[1 - index] && (turn == index));
- do_something();
- flag[index] = 0;
- }
- unsigned int flag[2] = {0};
- unsigned int turn = 0;
-
- void process(unsigned int index)
- {
- flag[index] = 1;
- turn = index;
-
- while(flag[1 - index] && (turn == index));
- do_something();
- flag[index] = 0;
- }
其实,学过操作系统的朋友都知道,上面的算法其实就是Peterson算法,可惜它只能用于两个线程的数据互斥。当然,这个算法还可以推广到更多线程之间的互斥,那就是bakery算法。但是数学算法有两个缺点:
a)占有空间多,两个线程就要flag占两个单位空间,那么n个线程就要n个flag空间,
b)代码编写复杂,考虑的情况比较复杂
(3)系统提供的互斥算法
系统提供的互斥算法其实是我们平时开发中用的最多的互斥工具。就拿windows来说,关于互斥的工具就有临界区、互斥量、信号量等等。这类算法有一个特点,那就是都是依据系统提高的互斥资源,那么系统又是怎么完成这些功能的呢?其实也不难。
系统加锁过程,
- void Lock(HANDLE hLock)
- {
- __asm {cli};
-
- while(1){
- if(){
-
- __asm {sti};
- return;
- }
-
- __asm{sti};
- schedule();
- __asm{cli};
- }
- }
- void Lock(HANDLE hLock)
- {
- __asm {cli};
-
- while(1){
- if(){
-
- __asm {sti};
- return;
- }
-
- __asm{sti};
- schedule();
- __asm{cli};
- }
- }
系统解锁过程,
- void UnLock(HANDLE hLock)
- {
- __asm {cli};
-
- __asm{sti};
- }
- void UnLock(HANDLE hLock)
- {
- __asm {cli};
-
- __asm{sti};
- }
上面其实讨论的就是一种最简单的系统锁情况。中间没有涉及到就绪线程的压入和弹出过程,没有涉及到资源个数的问题,所以不是很复杂。朋友们仔细看看,应该都可以明白代码表达的是什么意思。
(4)CPU的原子操作
因为在多线程操作当中,有很大一部分是比较、自增、自减等简单操作。因为需要互斥的代码很少,所以使用互斥量、信号量并不合算。因此,CPU厂商为了开发的方便,把一些常用的指令设计成了原子指令,在windows上面也被称为原子锁,常用的原子操作函数有
- InterLockedAdd
-
- InterLockedExchange
-
- InterLockedCompareExchange
-
- InterLockedIncrement
-
- InterLockedDecrement
-
- InterLockedAnd
-
- InterLockedOr
- InterLockedAdd
-
- InterLockedExchange
-
- InterLockedCompareExchange
-
- InterLockedIncrement
-
- InterLockedDecrement
-
- InterLockedAnd
-
- InterLockedOr
(之读写锁)
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢?
有,那就是读写锁。
(1)首先,我们定义一下基本的数据结构。
- typedef struct _RWLock
- {
- int count;
- int state;
- HANDLE hRead;
- HANDLE hWrite;
- }RWLock;
- typedef struct _RWLock
- {
- int count;
- int state;
- HANDLE hRead;
- HANDLE hWrite;
- }RWLock;
同时,为了判断当前的锁是处于读状态,还是写状态,我们要定义一个枚举量,
- typedef enum
- {
- STATE_EMPTY = 0,
- STATE_READ,
- STATE_WRITE
- };
- typedef enum
- {
- STATE_EMPTY = 0,
- STATE_READ,
- STATE_WRITE
- };
(2)初始化数据结构
- RWLock* create_read_write_lock(HANDLE hRead, HANDLE hWrite)
- {
- RWLock* pRwLock = NULL;
-
- assert(NULL != hRead && NULL != hWrite);
- pRwLock = (RWLock*)malloc(sizeof(RWLock));
-
- pRwLock->hRead = hRead;
- pRwLock->hWrite = hWrite;
- pRwLock->count = 0;
- pRwLock->state = STATE_EMPTY;
- return pRwLock;
- }
- RWLock* create_read_write_lock(HANDLE hRead, HANDLE hWrite)
- {
- RWLock* pRwLock = NULL;
-
- assert(NULL != hRead && NULL != hWrite);
- pRwLock = (RWLock*)malloc(sizeof(RWLock));
-
- pRwLock->hRead = hRead;
- pRwLock->hWrite = hWrite;
- pRwLock->count = 0;
- pRwLock->state = STATE_EMPTY;
- return pRwLock;
- }
(3)获取读锁
- void read_lock(RWLock* pRwLock)
- {
- assert(NULL != pRwLock);
-
- WaitForSingleObject(pRwLock->hRead, INFINITE);
- pRwLock->counnt ++;
- if(1 == pRwLock->count){
- WaitForSingleObject(pRwLock->hWrite, INFINITE);
- pRwLock->state = STATE_READ;
- }
- ReleaseMutex(pRwLock->hRead);
- }
- void read_lock(RWLock* pRwLock)
- {
- assert(NULL != pRwLock);
-
- WaitForSingleObject(pRwLock->hRead, INFINITE);
- pRwLock->counnt ++;
- if(1 == pRwLock->count){
- WaitForSingleObject(pRwLock->hWrite, INFINITE);
- pRwLock->state = STATE_READ;
- }
- ReleaseMutex(pRwLock->hRead);
- }
(4)获取写锁
- void write_lock(RWLock* pRwLock)
- {
- assert(NULL != pRwLock);
-
- WaitForSingleObject(pRwLock->hWrite, INFINITE);
- pRwLock->state = STATE_WRITE;
- }
- void write_lock(RWLock* pRwLock)
- {
- assert(NULL != pRwLock);
-
- WaitForSingleObject(pRwLock->hWrite, INFINITE);
- pRwLock->state = STATE_WRITE;
- }
(5)释放读写锁
- void read_write_unlock(RWLock* pRwLock)
- {
- assert(NULL != pRwLock);
-
- if(STATE_READ == pRwLock->state){
- WaitForSingleObject(pRwLock->hRead, INFINITE);
- pRwLock->count --;
- if(0 == pRwLock->count){
- pRwLock->state = STATE_EMPTY;
- ReleaseMutex(pRwLock->hWrite);
- }
- ReleaseMutex(pRwLock->hRead);
- }else{
- pRwLock->state = STATE_EMPTY;
- ReleaseMutex(pRwLock->hWrite);
- }
-
- return;
- }
- void read_write_unlock(RWLock* pRwLock)
- {
- assert(NULL != pRwLock);
-
- if(STATE_READ == pRwLock->state){
- WaitForSingleObject(pRwLock->hRead, INFINITE);
- pRwLock->count --;
- if(0 == pRwLock->count){
- pRwLock->state = STATE_EMPTY;
- ReleaseMutex(pRwLock->hWrite);
- }
- ReleaseMutex(pRwLock->hRead);
- }else{
- pRwLock->state = STATE_EMPTY;
- ReleaseMutex(pRwLock->hWrite);
- }
-
- return;
- }
文章总结:
(1)读写锁的优势只有在多读少写、代码段运行时间长这两个条件下才会效率达到最大化;
(2)任何公共数据的修改都必须在锁里面完成;
(3)读写锁有自己的应用场所,选择合适的应用环境十分重要;
(4)编写读写锁很容易出错,朋友们应该多加练习;
(5)读锁和写锁一定要分开使用,否则达不到效果。
生产者-消费者是很有意思的一种算法。它的存在主要是两个目的,第一就是满足生产者对资源的不断创造;第二就是满足消费者对资源的不断索取。当然,因为空间是有限的,所以资源既不能无限存储,也不能无限索取。
生产者的算法,
- WaitForSingleObject(hEmpty, INFINITE);
- WaitForSingleObject(hMutex, INIFINITE);
-
- ReleaseMutex(hMutex);
- ReleaseSemaphore(hFull, 1, NULL);
- WaitForSingleObject(hEmpty, INFINITE);
- WaitForSingleObject(hMutex, INIFINITE);
-
- ReleaseMutex(hMutex);
- ReleaseSemaphore(hFull, 1, NULL);
消费者的算法,
- WaitForSingleObject(hFull, INFINITE);
- WaitForSingleObject(hMutex, INIFINITE);
-
- ReleaseMutex(hMutex);
- ReleaseSemaphore(hEmpty, 1, NULL);
- WaitForSingleObject(hFull, INFINITE);
- WaitForSingleObject(hMutex, INIFINITE);
-
- ReleaseMutex(hMutex);
- ReleaseSemaphore(hEmpty, 1, NULL);
那么,有的朋友可能会说了,这么一个生产者-消费者算法有什么作用呢。我们可以看看它在多线程通信方面是怎么发挥作用的?首先我们定义一个数据结构,
- typedef struct _MESSAGE_QUEUE
- {
- int threadId;
- int msgType[MAX_NUMBER];
- int count;
- HANDLE hFull;
- HANDLE hEmpty;
- HANDLE hMutex;
- }MESSAGE_QUEUE;
- typedef struct _MESSAGE_QUEUE
- {
- int threadId;
- int msgType[MAX_NUMBER];
- int count;
- HANDLE hFull;
- HANDLE hEmpty;
- HANDLE hMutex;
- }MESSAGE_QUEUE;
那么,此时如果我们需要对一个线程发送消息,该怎么发送呢,其实很简单。我们完全可以把它看成是一个生产者的操作。
- void send_mseesge(int threadId, MESSAGE_QUEUE* pQueue, int msg)
- {
- assert(NULL != pQueue);
-
- if(threadId != pQueue->threadId)
- return;
-
- WaitForSingleObject(pQueue->hEmpty, INFINITE);
- WaitForSingleObject(pQueue->hMutex, INFINITE);
- pQueue->msgType[pQueue->count ++] = msg;
- ReleaseMutex(pQueue->hMutex);
- ReleaseSemaphore(pQueue->hFull, 1, NULL);
- }
- void send_mseesge(int threadId, MESSAGE_QUEUE* pQueue, int msg)
- {
- assert(NULL != pQueue);
-
- if(threadId != pQueue->threadId)
- return;
-
- WaitForSingleObject(pQueue->hEmpty, INFINITE);
- WaitForSingleObject(pQueue->hMutex, INFINITE);
- pQueue->msgType[pQueue->count ++] = msg;
- ReleaseMutex(pQueue->hMutex);
- ReleaseSemaphore(pQueue->hFull, 1, NULL);
- }
既然前面说到发消息,那么线程自身就要对这些消息进行处理了。
- void get_message(MESSAGE_QUEUE* pQueue, int* msg)
- {
- assert(NULL != pQueue && NULL != msg);
-
- WaitForSingleObject(pQueue->hFull, INFINITE);
- WaitForSingleObject(pQueue->hMutex, INFINITE);
- *msg = pQueue->msgType[pQueue->count --];
- ReleaseMutex(pQueue->hMutex);
- ReleaseSemaphore(pQueue->hEmpty, 1, NULL);
- }
- void get_message(MESSAGE_QUEUE* pQueue, int* msg)
- {
- assert(NULL != pQueue && NULL != msg);
-
- WaitForSingleObject(pQueue->hFull, INFINITE);
- WaitForSingleObject(pQueue->hMutex, INFINITE);
- *msg = pQueue->msgType[pQueue->count --];
- ReleaseMutex(pQueue->hMutex);
- ReleaseSemaphore(pQueue->hEmpty, 1, NULL);
- }
总结:
(1)生产者-消费者只能使用semphore作为锁
(2)编写代码的时候需要判断hFull和hEmpty的次序
(3)掌握生产者-消费者的基本算法很重要,但更重要的是自己的实践
(之死锁)
相信有过多线程编程经验的朋友,都吃过死锁的苦。除非你不使用多线程,否则死锁的可能性会一直存在。为什么会出现死锁呢?我想原因主要有下面几个方面:
(1)个人使用锁的经验差异
(2)模块使用锁的差异
(3)版本之间的差异
(4)分支之间的差异
(5)修改代码和重构代码带来的差异
不管什么原因,死锁的危机都是存在的。那么,通常出现的死锁都有哪些呢?我们可以一个一个看过来,
(1)忘记释放锁
- void data_process()
- {
- EnterCriticalSection();
-
- if()
- return;
-
- LeaveCriticalSection();
- }
- void data_process()
- {
- EnterCriticalSection();
-
- if()
- return;
-
- LeaveCriticalSection();
- }
(2)单线程重复申请锁
- void sub_func()
- {
- EnterCriticalSection();
- do_something();
- LeaveCriticalSection();
- }
-
- void data_process()
- {
- EnterCriticalSection();
- sub_func();
- LeaveCriticalSection();
- }
- void sub_func()
- {
- EnterCriticalSection();
- do_something();
- LeaveCriticalSection();
- }
-
- void data_process()
- {
- EnterCriticalSection();
- sub_func();
- LeaveCriticalSection();
- }
(3)双线程多锁申请
- void data_process1()
- {
- EnterCriticalSection(&cs1);
- EnterCriticalSection(&cs2);
- do_something1();
- LeaveCriticalSection(&cs2);
- LeaveCriticalSection(&cs1);
- }
-
- void data_process2()
- {
- EnterCriticalSection(&cs2);
- EnterCriticalSection(&cs1);
- do_something2();
- LeaveCriticalSection(&cs1);
- LeaveCriticalSection(&cs2);
- }
- void data_process1()
- {
- EnterCriticalSection(&cs1);
- EnterCriticalSection(&cs2);
- do_something1();
- LeaveCriticalSection(&cs2);
- LeaveCriticalSection(&cs1);
- }
-
- void data_process2()
- {
- EnterCriticalSection(&cs2);
- EnterCriticalSection(&cs1);
- do_something2();
- LeaveCriticalSection(&cs1);
- LeaveCriticalSection(&cs2);
- }
(4)环形锁申请
假设有A、B、C、D四个人在一起吃饭,每个人左右各有一只筷子。所以,这其中要是有一个人想吃饭,他必须首先拿起左边的筷子,再拿起右边的筷子。现在,我们让所有的人同时开始吃饭。那么就很有可能出现这种情况。每个人都拿起了左边的筷子,或者每个人都拿起了右边的筷子,为了吃饭,他们现在都在等另外一只筷子。此时每个人都想吃饭,同时每个人都不想放弃自己已经得到的一那只筷子。所以,事实上大家都吃不了饭。
总结:
(1)死锁的危险始终存在,但是我们应该尽量减少这种危害存在的范围
(2)解决死锁花费的代价是异常高昂的
(3)最好的死锁处理方法就是在编写程序的时候尽可能检测到死锁
(4)多线程是一把双刃剑,有了效率的提高当然就有死锁的危险
(5)某些程序的死锁是可以容忍的,大不了重启机器,但是有些程序不行
(之避免死锁)
预防死锁的注意事项:
(1)在编写多线程程序之前,首先编写正确的程序,然后再移植到多线程
(2)时刻检查自己写的程序有没有在跳出时忘记释放锁
(3)如果自己的模块可能重复使用一个锁,建议使用嵌套锁
(4)对于某些锁代码,不要临时重新编写,建议使用库里面的锁,或者自己曾经编写的锁
(5)如果某项业务需要获取多个锁,必须保证锁的按某种顺序获取,否则必定死锁
(6)编写简单的测试用例,验证有没有死锁
(7)编写验证死锁的程序,从源头避免死锁
首先,定义基本的数据结构和宏,
- typedef struct _LOCK_INFO
- {
- char lockName[32];
- HANDLE hLock;
- }LOCK_INFO:
-
- typedef struct _THREAD_LOCK
- {
- int threadId;
- LOCK_INFO* pLockInfo[32];
- }THREAD_LOCK;
-
- #define CRITICAL_SECTION_TYPE 1
- #define MUTEX_LOCK_TYPE 2
- #define SEMAPHORE_LOCK_TYPE 3
- #define NORMAL_LOCK_TYPE 4
-
- #define WaitForSingleObject(a, b) \
- WaitForSingleObject_stub((void*)a, NORMAL_LOCK_TYPE)
-
- #define EnterCriticalSection(a) \
- WaitForSingleObject_stub((void*)a, CRITICAL_SECTION_TYPE)
-
- #define ReleaseMutex(a) \
- ReleaseLock_stub((void*)a, MUTEX_LOCK_TYPE))
-
- #define ReleaseSemaphore(a, b, c) \
- ReleaseLock_stub((void*)a, SEMAPHORE_LOCK_TYPE))
-
- #define LeaveCriticalSection(a) \
- ReleaseLock_stub((void*)a, CRITICAL_SECTION_TYPE))
- typedef struct _LOCK_INFO
- {
- char lockName[32];
- HANDLE hLock;
- }LOCK_INFO:
-
- typedef struct _THREAD_LOCK
- {
- int threadId;
- LOCK_INFO* pLockInfo[32];
- }THREAD_LOCK;
-
- #define CRITICAL_SECTION_TYPE 1
- #define MUTEX_LOCK_TYPE 2
- #define SEMAPHORE_LOCK_TYPE 3
- #define NORMAL_LOCK_TYPE 4
-
- #define WaitForSingleObject(a, b) \
- WaitForSingleObject_stub((void*)a, NORMAL_LOCK_TYPE)
-
- #define EnterCriticalSection(a) \
- WaitForSingleObject_stub((void*)a, CRITICAL_SECTION_TYPE)
-
- #define ReleaseMutex(a) \
- ReleaseLock_stub((void*)a, MUTEX_LOCK_TYPE))
-
- #define ReleaseSemaphore(a, b, c) \
- ReleaseLock_stub((void*)a, SEMAPHORE_LOCK_TYPE))
-
- #define LeaveCriticalSection(a) \
- ReleaseLock_stub((void*)a, CRITICAL_SECTION_TYPE))
然后,改写锁的申请函数,
- void WaitForSingleObject_stub(void* hLock, int type)
- {
-
- WaitForSingleObject(hDbgLock);
-
- ReleaseMutex(hDbgLock);
-
-
- if(NORMAL_LOCK_TYPE == type)
- WaitForSingleObject((HANDLE)hLock, INFINITE);
- else if(CRITICAL_SECTION_TYPE == type)
- EnterCriticalSection((LPCRITICAL_SECTION)hLock);
- else
- assert(0);
-
-
- WaitForSingleObject(hDbgLock);
-
- ReleaseMutex(hDbgLock);
- }
- void WaitForSingleObject_stub(void* hLock, int type)
- {
-
- WaitForSingleObject(hDbgLock);
-
- ReleaseMutex(hDbgLock);
-
-
- if(NORMAL_LOCK_TYPE == type)
- WaitForSingleObject((HANDLE)hLock, INFINITE);
- else if(CRITICAL_SECTION_TYPE == type)
- EnterCriticalSection((LPCRITICAL_SECTION)hLock);
- else
- assert(0);
-
-
- WaitForSingleObject(hDbgLock);
-
- ReleaseMutex(hDbgLock);
- }
最后,需要改写锁的释放函数。
- void ReleaseLock_stub(void* hLock, int type)
- {
-
- WaitForSingleObject(hDbgLock);
-
- ReleaseMutex(hDbgLock);
-
-
- if(MUTEX_LOCK_TYPE))== type)
- ReleaseMutex(HANDLE)hLock);
- else if(SEMAPHORE_LOCK_TYPE == type)
- ReleaseSemaphore((HANDLE)hLock, 1, NULL);
- else if(CRITICAL_SECTION_TYPE == type)
- LeaveCriticalSection((LPCRITICAL_SECTION)hLock);
- assert(0);
- }
- void ReleaseLock_stub(void* hLock, int type)
- {
-
- WaitForSingleObject(hDbgLock);
-
- ReleaseMutex(hDbgLock);
-
-
- if(MUTEX_LOCK_TYPE))== type)
- ReleaseMutex(HANDLE)hLock);
- else if(SEMAPHORE_LOCK_TYPE == type)
- ReleaseSemaphore((HANDLE)hLock, 1, NULL);
- else if(CRITICAL_SECTION_TYPE == type)
- LeaveCriticalSection((LPCRITICAL_SECTION)hLock);
- assert(0);
- }
(之多线程调试)
软件调试是我们软件开发过程中的重要一课。在前面,我们也讨论过程序调试,比如说这里。今天,我们还可以就软件调试多讲一些内容。比如说条件断点,数据断点,多线程断点等等。
- #include <stdio.h>
- int value = 0;
-
- void test()
- {
- int total;
- int index;
-
- total = 0;
- for(index = 0; index < 100; index ++)
- total += index * index;
-
- value = total;
- return ;
- }
-
- int main()
- {
- test();
- return 1;
- }
- #include <stdio.h>
- int value = 0;
-
- void test()
- {
- int total;
- int index;
-
- total = 0;
- for(index = 0; index < 100; index ++)
- total += index * index;
-
- value = total;
- return ;
- }
-
- int main()
- {
- test();
- return 1;
- }
(1)数据断点
所谓数据断点,就是全局变量或者函数中的数计算的过程中,如果数据值本身发生了改变,就会触发断点。这里的数据有两种,一个是全局数据,一个函数内部的数据。
以全局数据value为例:
a)按F10,运行程序,获取value的地址;
b)Alt+F9,选择【DATA】->【Advanced】;
c)在【Expression】中输入DW(0x0043178),【ok】回车;
d)F5继续运行程序,则程序会在value发生改变的时候停住。
以局部数据total为例,
a)按F10,运行程序,获取value的地址;
b)Alt+F9,选择【DATA】->【Advanced】;
c)在【Expression】中输入total,在【Function】输入test,【ok】回车;
d)F5继续运行程序,则程序同样会在total发生改变的时候停住。
(2)条件断点
条件断点和数据断点差不多。只不过,数据断点在数据发生改变的时候就会断住,而条件断点只有在满足一定的条件下才会有断住。比如说,我们可以让test子程序在index==5的时候断住。
a)按F10,运行程序,获取value的地址;
b)Alt+F9,选择【DATA】->【Advanced】;
c)在【Expression】中输入index==5,在【Function】输入test,【ok】回车;
d)F5继续运行程序,则程序同样会在index==5的时候停住。
(3)多线程调试
在VC上面对多程序的调试比较简单。如果想要对程序进行调试的话,首先F10,开始运行程序。其次,我们需要等线程创建之后才能设置断点,不然我们看到的程序只有main函数一个thread。
a)单击【Debug】,选择【threads】,那么我们就可以开始多线程调试了;
b)如果需要对某一个thread挂起,单击对应的thread,选择【suspend】即可;
c)如果需要对某一个thread重新调度,单击对应的thread,选择【resume】即可;
d)如果需要查看特定thread的堆栈,那么选择那个thread,然后【Set Focus】,关闭threads对话框,在堆栈窗口中即可看到;
e)如果某个线程被挂住,那么此时所有的线程都挂住了,如果你step运行,所有的threads都会参与运行;
f)如果需要对某一个thread进行调试,那么需要对其他的thread进行suspend处理 。
总结:
1)看内存、看堆栈、条件断点、数据断点需要综合使用,
2)编程越早调试,越好,
3)先编写好单线程程序,再编写好多线程程序,
4)对于多线程来说,模块设计 > 编程预防 > 调试 > 事后补救。
(之无锁队列)
对于编写多线程的朋友来说,队列具有天生的互斥性。在队列里面,一个负责添加数据,一个负责处理数据。谁也不妨碍谁,谁也离不开谁。所以,队列具有天生的并行性。
- #define MAX_NUMBER 1000L
- #define STATUS int
- #define OK 0
- #define FALSE -1
-
- typedef struct _QUEUE_DATA
- {
- int data[MAX_NUMBER];
- int head;
- int tail;
- }QUEUE_DATA;
- #define MAX_NUMBER 1000L
- #define STATUS int
- #define OK 0
- #define FALSE -1
-
- typedef struct _QUEUE_DATA
- {
- int data[MAX_NUMBER];
- int head;
- int tail;
- }QUEUE_DATA;
此时,一个线程压入数据,操作为push_data,
- STATUS push_data(QUEUE_DATA* pQueue, int data)
- {
- if(NULL == pQueue)
- return ERROR;
-
- if(pQueue->head == ((pQueue->tail) + 1)% MAX_NUMBER)
- return ERROR;
-
- pQueue->data[pQueue->tail] = data;
- pQueue->tail = (pQueue->tail + 1)% MAX_NUMBER;
- return OK;
- }
- STATUS push_data(QUEUE_DATA* pQueue, int data)
- {
- if(NULL == pQueue)
- return ERROR;
-
- if(pQueue->head == ((pQueue->tail) + 1)% MAX_NUMBER)
- return ERROR;
-
- pQueue->data[pQueue->tail] = data;
- pQueue->tail = (pQueue->tail + 1)% MAX_NUMBER;
- return OK;
- }
那么,还有一个线程就负责处理数据pop_data,
- STATUS pop_data(QUEUE_DATA* pQueue, int* pData)
- {
- if(NULL == pQueue || NULL == pData)
- return ERROR;
-
- if(pQueue->head == pQueue->tail)
- return ERROR;
-
- *pData = pQueue->data[pQueue->head];
- pQueue->head = (pQueue->head + 1)% MAX_NUMBER;
- return OK;
- }
- STATUS pop_data(QUEUE_DATA* pQueue, int* pData)
- {
- if(NULL == pQueue || NULL == pData)
- return ERROR;
-
- if(pQueue->head == pQueue->tail)
- return ERROR;
-
- *pData = pQueue->data[pQueue->head];
- pQueue->head = (pQueue->head + 1)% MAX_NUMBER;
- return OK;
- }
总结:
(1)队列只适合两个线程并行使用,一个压入数据,一个弹出数据
(2)队列是没有锁的并行,没有死锁的危险
(3)队列中head和tail只有在计算结束之前的时候才能进行自增运算
(之顺序锁)
在互斥数据访问中有一种多读少写的情况。正对这么一种情形,我们也提出了读写锁的方案。但是呢,这个锁有些缺陷。什么缺陷呢?那就是,这个写锁需要在所有的读锁完成之后才能写。否则的话,写锁需要这么一直等下去。
那么,有没有什么办法能使得写操作快速一点进行呢?那就是顺序锁。
- typedef struct _SEQUENCE_LOCK
- {
- unsigned int sequence;
- HANDLE hLock;
-
- }SEQUENCE_LOCK;
- typedef struct _SEQUENCE_LOCK
- {
- unsigned int sequence;
- HANDLE hLock;
-
- }SEQUENCE_LOCK;
有了这么一个数据结构之后。那么读锁怎么开始呢,
- unsigned int get_lock_begin(SEQUENCE_LOCK* hSeqLock)
- {
- assert(NULL != hSeqLock);
-
- return hSeqLock->sequence;
- }
-
- int get_lock_retry(SEQUENCE_LOCK* hSeqLock, unsigned int value)
- {
- unsigned int new_value;
- assert(NULL != hSeqLock);
-
- new_value = hSeqLock->sequence;
- return (new_value & 0x1) || (new_value ^ value);
- }
- unsigned int get_lock_begin(SEQUENCE_LOCK* hSeqLock)
- {
- assert(NULL != hSeqLock);
-
- return hSeqLock->sequence;
- }
-
- int get_lock_retry(SEQUENCE_LOCK* hSeqLock, unsigned int value)
- {
- unsigned int new_value;
- assert(NULL != hSeqLock);
-
- new_value = hSeqLock->sequence;
- return (new_value & 0x1) || (new_value ^ value);
- }
自然写锁也需要修改了,
- void get_write_lock(SEQUENCE_LOCK* hSeqLock)
- {
- assert(NULL != hSeqLock);
-
- WaitForSingleObject(hSeqLock->hLock);
- hSeqLock->sequence ++;
- }
-
- void release_write_lock(SEQUENCE_LOCK* hSeqLock)
- {
- assert(NULL != hSeqLock);
-
- hSeqLock->sequence ++;
- ReleaseMutex(hSeqLock->hLock);
- }
- void get_write_lock(SEQUENCE_LOCK* hSeqLock)
- {
- assert(NULL != hSeqLock);
-
- WaitForSingleObject(hSeqLock->hLock);
- hSeqLock->sequence ++;
- }
-
- void release_write_lock(SEQUENCE_LOCK* hSeqLock)
- {
- assert(NULL != hSeqLock);
-
- hSeqLock->sequence ++;
- ReleaseMutex(hSeqLock->hLock);
- }
如果应用呢,其实也不难,
- void read_process(SEQUENCE_LOCK* hSeqLock)
- {
- unsigned int sequence;
-
- do{
- sequence = get_lock_begin(hSeqLock);
-
- }while(get_lock_retry(hSeqLock, sequence));
- }
-
- void write_process(SEQUENCCE_LOCK* hSeqLock)
- {
- get_write_lock(hSeqLock);
-
- release_write_lock(hSeqLock);
- }
- void read_process(SEQUENCE_LOCK* hSeqLock)
- {
- unsigned int sequence;
-
- do{
- sequence = get_lock_begin(hSeqLock);
-
- }while(get_lock_retry(hSeqLock, sequence));
- }
-
- void write_process(SEQUENCCE_LOCK* hSeqLock)
- {
- get_write_lock(hSeqLock);
-
- release_write_lock(hSeqLock);
- }
总结:
(1)读锁退出有两个条件,要么写操作正在进行呢,要么没有写锁
(2)写锁之间需要互斥操作
(3)互斥操作的数据不能是指针,否则有可能在访问的时候会造成异常,因为有可能边写边读
(4)顺序锁代替不了读写锁,因为读写锁可以保证所有的数据操作,而顺序锁不行
(之无锁链表)
前面,为了使得写操作快速进行,我们定义了顺序锁。但是顺序锁有个缺点,那就是处理的数据不能是指针,否则可能会导致exception。那么有没有办法使得处理的数据包括指针呢?当然要是这个链表没有锁,那就更好了。
针对这种无锁链表,我们可以初步分析一下,应该怎么设计呢?
(1)读操作没有锁,那么怎么判断读操作正在进行呢,只能靠标志位了;
(2)写操作没有锁,那么读操作只能一个线程完成;
(3)写操作中如果是添加,那么直接加在末尾即可;
(4)写操作中如果是删除,那么应该先删除数据,然后等到当前没有操作访问删除数据时,释放内存,但是首节点不能删除。
普通链表的结构为,
- typedef struct _LINK
- {
- int data;
- struct _LINK* next;
- }LINK;
- typedef struct _LINK
- {
- int data;
- struct _LINK* next;
- }LINK;
假设此时有32个线程在访问链表,那么可以定义一个全局变量value,每一个bit表示一个thread,读操作怎么进行呢,
- void read_process()
- {
- int index = get_index_from_threadid(GetThreadId());
- InterLockedOr(&value, 1 << index);
-
- InterLockedAnd(&value, ~(1<< index));
- }
- void read_process()
- {
- int index = get_index_from_threadid(GetThreadId());
- InterLockedOr(&value, 1 << index);
-
- InterLockedAnd(&value, ~(1<< index));
- }
那么,写操作怎么进行呢,
- void write_process_add(LINK* pHead, LINK* pLink)
- {
-
- }
-
- void write_process_del(LINK* pHead, LINK* pLink)
- {
- delete_link_from_list(pHead, pLink);
- while(1){
- if(0 == value)
- break;
- Sleep(100);
- }
-
- free(pLink);
- }
- void write_process_add(LINK* pHead, LINK* pLink)
- {
-
- }
-
- void write_process_del(LINK* pHead, LINK* pLink)
- {
- delete_link_from_list(pHead, pLink);
- while(1){
- if(0 == value)
- break;
- Sleep(100);
- }
-
- free(pLink);
- }
其中链表的删除操作为,