呵呵呵呵多线程

来源:互联网 发布:网络安全技术分析论文 编辑:程序博客网 时间:2024/05/16 18:25
应用层我们经常调用WaitForSingle(Multiple)Object, 操作系统内部是怎么实现的?


每个可同步的对象, 内部都有一个分发器头(dispatch_header), 分发器头包含了对象类型,状态,以及等待该对象的线程列表;每个处于等待状态的线程, 都有一个等待块列表(wait block list), 每个等待块代表一个等待线程。这样就很好理解了,当我们把一个同步对象设置成有信号状态时, 系统沿着分发器头的等待线程列表遍历,找到可激活的线程,将它转入就绪状态参与线程调度。




Critical Section是如何实现用户态等待的?


我们知道CRITICAL_SECTION是同一进程内我们最常用的同步机制, 号称不用转入内核,以高效闻名, 它是怎么实现的?
单纯在用户态等待, 我们只能死循环,不停的检测, 也就是所谓的自旋锁(SpinLock), critical section如果用这种方式实现,何来高效可言。

实际上critical section的大概实现是这样的: 它内部包含一个标志位以及一个event object, 进入critical section时首先尝试设置标志位,如果设置成功,表示成功获得资源;如果标志位已经被设置, 则等待event事件。其中标志位的设置是用类似interlockedexchange这样的原子API操作的。这样只要没有资源竞争,大部分情况下都能满足我们的高效需求, 如果有资源竞争,实际上还是会转入内核态挂起线程。






   进程是系统中的重要概念,简单来说字面的意思就是一个运行中的程序,但是程序代表的是静态的指令代码。进程由系统管理的内核对象和存放程序运行资源的地址空间组成。内核对象由系统管理,因此应用程序是无法直接访问的;地址空间中则包含着程序运行所需的所有资源,如可执行模块、DLL、代码和数据,以及动态分配的栈与堆。可以说,其实进程就是程序运行的资源的容器。但是进程只是为程序的执行提供了一个场所,真正实现执行流程的是线程。每个进程启动时都会自动建立一个线程作为主线程,由它去创建其他的线程。线程与进程相比更为轻量级,二者的主要联系在于:
1. 线程创建在进程的地址空间中,对于进程资源有着完全访问权限,多线程间共享资源,可自由通信;
2. 线程建立时也有自己的内核对象和线程栈,而非地址空间;
3. 一般需要实现多任务时我们更推荐使用线程实现,因为创建一个进程需要分配地址空间,而且进程间的切换也很不方便,即使用进程开销很高;


一、创建多线程
     简单了解了进程与线程的概念之后,我们来看看如何在程序中创建线程。Windows SDK为我们提供了创建线程的专用函数:CreateThread()

点击(此处)折叠或打开

  1. HANDLE CreateThread(
  2.   LPSECURITY_ATTRIBUTES lpsa, 
  3.   DWORD cbStack, 
  4.   LPTHREAD_START_ROUTINE lpStartAddr, 
  5.   LPVOID lpvThreadParam, 
  6.   DWORD fdwCreate, 
  7.   LPDWORD lpIDThread
  8. );
-1-第一个参数是安全属性结构,主要控制该线程句柄是否可为进程的子进程继承使用,默认使用NULL时表示不能继承;若想继承线程句柄,则需要设置该结构体,将结构体的bInheritHandle成员初始化为TRUE;
-2-cbStack表示的线程初始栈的大小,若使用0则表示采用默认大小初始化;
-3-lpStartAddr表示线程开始的位置,即线程要执行的函数代码,这点有点类似于回调函数的使用;
-4-lpvThreadParam用来接收线程过程函数的参数,不需要时可以设置为NULL;
-5-fdwCreate表示创建线程时的标志,CREATE_SUSPENDED表示线程创建后挂起暂不执行,必须调用ResumeThread才可以执行,0表示线程创建之后立即执行
-6-lpIDThread用来保存线程的ID;
     了解了CreateThread函数的用法,我们来看一个例子实际体验一下。下面的例子会开启一个线程循环输出信息,我们可以查看结果:

点击(此处)折叠或打开

  1. //MultiThread

  2. #include <iostream>
  3. #include <cstdlib>
  4. #include <windows.h>
  5. using namespace std;

  6. DWORD WINAPI Fun1Proc(LPVOID lpParameter); 


  7. int main()
  8. {

  9.     int j = 0;
  10.     
  11.     HANDLE hThread_1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
  12.     CloseHandle(hThread_1);
  13.     
  14.     while (j++ < 1000)
  15.      cout << "MainThread is running for" << " the "<< j <<" times "<<endl;
  16.         
  17.     system("pause");
  18.     return 0;
  19.         
  20.     
  21. }


  22. DWORD WINAPI Fun1Proc(LPVOID lpParameter)
  23. {
  24.       int i = 0;
  25.       while (i++ < 1000)
  26.           cout << "Thread 1 is running for" <<" the "<< i <<" times "<<endl;
  27.        
  28.       return 0;
  29.       }
      由于我们使用system("pause")命令,因此我们不需要Sleep()命令同样可以看到主线程和分线程的交替执行:



二、多线程的问题
     上面的程序我们只是建立一个新线程,如果建立两个呢?下面我们模拟一个火车售票的模型,线程1和线程2同时负责售票,主线程负责平台搭建。线程1和线程2分别访问全局变量tickets,输出其值作为票号然后将其值减一,直至“售完”所有票:

点击(此处)折叠或打开

  1. //MultiThread

  2. #include <iostream>
  3. #include <cstdlib>
  4. #include <windows.h>
  5. using namespace std;

  6. DWORD WINAPI Fun1Proc(LPVOID lpParameter); 
  7. DWORD WINAPI Fun2Proc(LPVOID lpParameter);

  8. int tickets = 100;

  9. int main()
  10. {
  11.            
  12.     HANDLE hThread_1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
  13.     HANDLE hThread_2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
  14.     CloseHandle(hThread_1);
  15.     CloseHandle(hThread_2);
  16.            
  17.     system("pause");
  18.     return 0;
  19.         
  20.     
  21. }


  22. DWORD WINAPI Fun1Proc(LPVOID lpParameter)
  23. {
  24.      
  25.       while (true)
  26.       {        
  27.             if (tickets > 0)
  28.             {
  29.                  Sleep(10);
  30.                  cout << "Thread 1 sell ticket : "<<tickets--<<endl;
  31.                  }
  32.             else
  33.                  break;       
  34.             }
  35.       return 0;
  36.       }
  37.       
  38. DWORD WINAPI Fun2Proc(LPVOID lpParameter)
  39. {

  40.        while (true)
  41.       {
  42.         
  43.             if (tickets > 0)
  44.             {
  45.                  Sleep(10);
  46.                  cout << "Thread 2 sell ticket : "<<tickets--<<endl;
  47.                  }
  48.             else
  49.                  break;
  50.          
  51.             }
  52.       return 0;
  53.       }
      再次运行之后结果:

     分析下上面的结果,首先线程1和线程2确实交替运行“购票”了,其次看到不规则的输出,1314连到一起。关于这点我们要知道由于CPU是执行分片处理的,即不同的线程会得到不同的时间片来执行程序,尤其是对于单CPU来说更是如此,因此当线程1执行到"Thread 1 sell ticket"时结束分片,由线程2继续执行;线程2执行到同样位置再次交还给线程1继续执行显示“13”,然后线程2执行显示“14”。这样看好像也没有问题,但是问题在于最后出现了“0”,我们的代码中是不允许出现0的,之所以这样,同上面的元婴一样,在线程1执行到ticket = 1时交接给了线程2执行,并且输出了其值1,然后线程1继续输出了当前值0.此时的if条件就失去作用了。虽然这种情况不一定总是发生,但是在实际的操作中是很有可能出现的,而且排查起来也很困难。那么我们该如何解决呢?
     问题的关键在于线程1和线程2同时访问了全局变量tickets,导致了错误;那么我们的解决方案就是将全局变量的访问控制起来就可以了,不允许同时有多个线程同时访问该变量。我们使用互斥量来实现。


三、互斥量的应用
     使用互斥量并不困难,核心步骤如下:
-1-CreateMutex创建一个互斥量:

点击(此处)折叠或打开

  1. HANDLE CreateMutex( 
  2.   LPSECURITY_ATTRIBUTES lpMutexAttributes, 
  3.   BOOL bInitialOwner, 
  4.   LPCTSTR lpName 
  5. );
      第一个参数同样是安全结构,默认是NULL不能继承句柄;第二个参数为FALSE时创建Mutex时不指定所有权,若为TRUE则指定为当前的创建线程ID为所有者,其他线程访问需要先ReleaseMutex;第三个参数用于设置Mutex名,后续我们会说明,为NULL时表示是匿名互斥量。
-2-WaitForSingleObject():请求一个互斥量的访问权;
-3-ReleaseMutex():释放一个互斥量的访问权;
     好了,我们再来看看应用了互斥量的改进程序:

点击(此处)折叠或打开

  1. //MultiThread

  2. #include <iostream>
  3. #include <cstdlib>
  4. #include <windows.h>
  5. using namespace std;

  6. DWORD WINAPI Fun1Proc(LPVOID lpParameter); 
  7. DWORD WINAPI Fun2Proc(LPVOID lpParameter);

  8. int tickets = 100;
  9. HANDLE hMutex;

  10. int main()
  11. {
  12.    

  13.     hMutex = CreateMutex(NULL, FALSE, NULL);
  14.     
  15.     HANDLE hThread_1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
  16.     HANDLE hThread_2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
  17.     CloseHandle(hThread_1);
  18.     CloseHandle(hThread_2);

  19.         
  20.     system("pause");
  21.     return 0;
  22.         
  23.     
  24. }


  25. DWORD WINAPI Fun1Proc(LPVOID lpParameter)
  26. {

  27.       while (true)
  28.       {
  29.             WaitForSingleObject(hMutex, INFINITE);
  30.             if (tickets > 0)
  31.             {
  32.                  Sleep(10);
  33.                  cout << "Thread 1 sell ticket : "<<tickets--<<endl;
  34.                  }
  35.             else
  36.                  break;
  37.             ReleaseMutex(hMutex);
  38.             }
  39.       return 0;
  40.       }
  41.       
  42. DWORD WINAPI Fun2Proc(LPVOID lpParameter)
  43. {

  44.        while (true)
  45.       {
  46.             WaitForSingleObject(hMutex, INFINITE);
  47.             if (tickets > 0)
  48.             {
  49.                 Sleep(10);
  50.                  cout << "Thread 2 sell ticket : "<<tickets--<<endl;
  51.                  }
  52.             else
  53.                  break;
  54.             ReleaseMutex(hMutex);
  55.             }
  56.       return 0;
  57.       }
      这次运行之后的结果不会出现上面的错误了:

      互斥量还有一个小应用,利用命名互斥量来保证只有一个程序实例运行。我们可以创建一个命名互斥量,当程序要重复运行时,检查互斥量的返回值,若为ERROR_ALREADY_EXISTS则表示已经有一个实例运行了,直接return即可。在源程序中添加以下代码:

点击(此处)折叠或打开

  1. //确保只有一个实例运行
  2.     HANDLE hMutex_1 = CreateMutex(NULL, TRUE, "tickets");
  3.     if (hMutex_1)
  4.     {
  5.                if (ERROR_ALREADY_EXISTS == GetLastError())
  6.                {
  7.                        cout << "Only one instance can run !" << endl;
  8.                        system("pause");
  9.                        return 0;
  10.                                         }
  11.                }







WaitForSingleObject函数用来检测hHandle事件的信号状态,当函数的执行时间超过dwMilliseconds就返回,但如果参数dwMillisecondsINFINITE时函数将直到相应时间事件变成有信号状态才返回,否则就一直等待下去,直到WaitForSingleObject有返回直才执行后面的代码。在这里举个例子:

先创建一个全局Event对象g_event:

    CEvent g_event;

在程序中可以通过调用CEvent::SetEvent设置事件为有信号状态。

下面是一个线程函数MyThreadPro()

UINT CFlushDlg::MyThreadProc( LPVOID pParam )

{

     WaitForSingleObject(g_event,INFINITE);

     For(;;)

        {

         ………….

        }

     return 0;

}

在这个线程函数中只有设置g_event为有信号状态时才执行下面的for循环,因为g_event是全局变量,所以我们可以在别的线程中通过g_event. SetEvent控制这个线程。

 

 

还有一种用法就是我们可以通过WaitForSingleObject函数来间隔的执行一个线程函数的函数体

     UINT CFlushDlg::MyThreadProc( LPVOID pParam )

{

     while(WaitForSingleObject(g_event,MT_INTERVAL)!=WAIT_OBJECT_0)

     {

         ………………

     }

     return 0;

}

在这个线程函数中可以可以通过设置MT_INTERVAL来控制这个线程的函数体多久执行一次,当事件为无信号状态是函数体隔MT_INTERVAL执行一次,当设置事件为有信号状态时,线程就执行完毕了。




























编程思想之多线程与多进程(4):C++ 中的多线程

  • Node.js七天搞定微信公众号
  • 飞速上手的跨平台App开发
  • Android专项测试-Python篇10年测试经验讲师
  • Vue.js高仿饿了么外卖App 2016最火前端框架

《编程思想之多线程与多进程(1)——以操作系统的角度述说线程与进程》一文详细讲述了线程、进程的关系及在操作系统中的表现,《编程思想之多线程与多进程(2)——线程优先级与线程安全》一文讲了线程安全(各种同步锁)和优先级,这是多线程学习必须了解的基础。本文将接着讲一下C++中多线程程序的开发.这里主要讲Windows平台线程的用法,创建线程要调用windows API的CreateThread方法。

 

创建线程

在Windows平台,Windows API提供了对多线程的支持。前面进程和线程的概念中我们提到,一个程序至少有一个线程,这个线程称为主线程(main thread),如果我们不显示地创建线程,那我们产的程序就是只有主线程的间线程程序。
下面,我们看看Windows中线程相关的操作和方法:

CreateThread与CloseHandle

CreateThread用于创建一个线程,其函数原型如下:

**说明:**lpThreadAttributes:指向SECURITY_ATTRIBUTES结构的指针,决定返回的句柄是否可被子进程继承,如果为NULL则表示返回的句柄不能被子进程继承。

dwStackSize :线程栈的初始化大小,字节单位。系统分配这个值对

lpStartAddress:指向一个函数指针,该函数将被线程调用执行。因此该函数也被称为线程函数(ThreadProc),是线程执行的起始地址,线程函数是一个回调函数,由操作系统在线程中调用。线程函数的原型如下:

lpParameter:传入线程函数(ThreadProc)的参数,不需传递参数时为NULL

dwCreationFlags:控制线程创建的标志,有三个类型,0:线程创建后立即执行线程;CREATE_SUSPENDED:线程创建后进入就绪状态,直到线程被唤醒时才调用;STACK_SIZE_PARAM_IS_A_RESERVATION:dwStackSize 参数指定线程初始化栈的大小,如果STACK_SIZE_PARAM_IS_A_RESERVATION标志未指定,dwStackSize将会设为系统预留的值。

返回值:如果线程创建成功,则返回这个新线程的句柄,否则返回NULL。如果线程创建失败,可通过GetLastError函数获得错误信息。

可用这个函数关闭创建的线程句柄,如果函数执行成功则返回true(非0),如果失败则返回false(0),如果执行失败可调用GetLastError.函数获得错误信息。

【Demo1】:创建一个最简单的线程

结果如下:

【Demo2】:在线程函数中传入参数

结果:

CreateMutex、WaitForSingleObject、ReleaseMutex

从【Demo2】中可以看出,虽然创建的子线程都正常执行起来了,但输出的结果并不是我们预期的效果。我们预期的效果是每输出一条语句后自动换行,但结果却并非都是这样。这是因为在线程执行时没有做同步处理,比如第一行的输出,主线程输出“主线程 ===”后时间片已用完,这时轮到子线程1输出,在子线程输出“线程1 —”后时间片也用完了,这时又轮到主线程执行输出“0”,之后又轮到子线程1输出“0”。于是就出现了“主线程 === 线程1 — 0 0”的结果。

主线程:cout << “主线程 === ” << i << endl;
子线程:cout << pThreadData->strThreadName << ” — ” << i << endl;

为避免出现这种情况,我们对线程做一些简单的同步处理,这里我们用互斥量(Mutex),关于互斥量(Mutex)的概念,请看《编程思想之多线程与多进程(2)——线程优先级与线程安全》一文;更多C++线程同步的处理,请看下一节。

在使用互斥量进行线程同步时会用到以下几个函数:

**说明:**lpMutexAttributes也是表示安全的结构,与CreateThread中的lpThreadAttributes功能相同,表示决定返回的句柄是否可被子进程继承,如果为NULL则表示返回的句柄不能被子进程继承。bInitialOwner表示创建Mutex时的当前线程是否拥有Mutex的所有权,若为TRUE则指定为当前的创建线程为Mutex对象的所有者,其它线程访问需要先ReleaseMutex。lpName为Mutex的名称。

**说明:**WaitForSingleObject的作用是等待一个指定的对象(如Mutex对象),直到该对象处于非占用的状态(如Mutex对象被释放)或超出设定的时间间隔。除此之外,还有一个与它类似的函数WaitForMultipleObjects,它的作用是等待一个或所有指定的对象,直到所有的对象处于非占用的状态,或超出设定的时间间隔。

hHandle:要等待的指定对象的句柄。dwMilliseconds:超时的间隔,以毫秒为单位;如果dwMilliseconds为非0,则等待直到dwMilliseconds时间间隔用完或对象变为非占用的状态,如果dwMilliseconds 为INFINITE则表示无限等待,直到等待的对象处于非占用的状态。

说明:释放所拥有的互斥量锁对象,hMutex为释放的互斥量的句柄。

【Demo3】:线程同步

结果:

为进一步理解线程同步的重要性和互斥量的使用方法,我们再来看一个例子。

买火车票是大家春节回家最为关注的事情,我们就简单模拟一下火车票的售票系统(为使程序简单,我们就抽出最简单的模型进行模拟):有500张从北京到赣州的火车票,在8个窗口同时出售,保证系统的稳定性和数据的原子性。

【Demo4】:模拟火车售票系统

SaleTickets.cpp :

测试程序:

结果:


编程思想之多线程与多进程(2):线程优先级与线程安全

  • React Native贯穿全栈开发App
  • 6小时用 jQuery 实现小应用
  • Android专项测试-Python篇10年测试经验讲师
  • Laravel和 AngularJS开发全栈知乎

上文详细讲述了线程、进程的关系及在操作系统中的表现,这是多线程学习必须了解的基础。本文将接着讲一下线程优先级和线程安全。

 

线程优先级

现在主流操作系统(如Windows、Linux、Mac OS X)的任务调度除了具有前面提到的时间片轮转的特点外,还有优先级调度(Priority Schedule)的特点。优先级调度决定了线程按照什么顺序轮流执行,在具有优先级调度的系统中,线程拥有各自的线程优先级(Thread Priority)。具有高优先级的线程会更早地执行,而低优先级的线程通常要等没有更高优先级的可执行线程时才会被执行。

线程的优先级可以由用户手动设置,此外系统也会根据不同情形调整优先级。通常情况下,频繁地进入等待状态(进入等待状态会放弃之前仍可占用的时间份额)的线程(如IO线程),比频繁进行大量计算以至于每次都把所有时间片全部用尽的线程更受操作系统的欢迎。因为频繁进入等待的线程只会占用很少的时间,这样操作系统可以处理更多的任务。我们把频繁等待的线程称之为IO密集型线程(IO Bound Thread),而把很少等待的线程称之为CPU密集型线程(CPU Bound Thread)。IO密集型线程总是比CPU密集型线程更容易得到优先级的提升。

线程饿死:

在优先级调度下,容易出现一种线程饿死的现象。一个线程饿死是说它的优先级较低,在它执行之前总是有比它优先级更高的线程等待执行,因此这个低优先级的线程始终得不到执行。当CPU密集型的线程优先级较高时,其它低优先级的线程就很可能出现饿死的情况;当IO密集型线程优先级较高时,其它线程相对不容易造成饿死的善,因为IO线程有大量的等待时间。为了避免线程饿死,调度系统通常会逐步提升那些等待了很久而得不到执行的线程的优先级。这样,一个线程只要它等待了足够长的时间,其优先级总会被提升到可以让它执行的程度,也就是说这种情况下线程始终会得到执行,只是时间的问题。

在优先级调度环境下,线程优先级的改变有三种方式:

1. 用户指定优先级;
2. 根据进入等待状态的频繁程度提升或降低优先级(由操作系统完成);
3. 长时间得不到执行而被提升优先级。

 

线程安全与锁

在多个线程并发执行访问同一个数据时,如果不采取相应的措施,将会是非常危险的。假设你在工行有一个银行账户,两张银联卡(自己手里一张,女朋友手里一张),里面有100万。假设取钱就两个过程:1.检查账户余额,2.取出现金(如果要取出的金额 > 账户余额,则出现成功,否则取现失败)。有一天你要买房想把钱取出来,而此时你女朋友也想买一辆车(假设你们事先没有商量)。两个人都在取钱,你在A号ATM机取100万,女朋友在B号ATM机取80万。这时A号ATM检查账户余额发现有100万,可以取出;而与此同时,同一时刻B号ATM也在检查账户余额发现有100万,可以取出;这样,A、B都把钱取出来了。

100万的存款取出180万,银行就亏大发了(当然你就笑呵呵了……)!这就是线程并发的不安全性。为避免这种情况发生,我们要将多个线程对同一数据的访问同步,确保线程安全。

所谓同步(synchronization)就是指一个线程访问数据时,其它线程不得对同一个数据进行访问,即同一时刻只能有一个线程访问该数据,当这一线程访问结束时其它线程才能对这它进行访问。同步最常见的方式就是使用锁(Lock),也称为线程锁。锁是一种非强制机制,每一个线程在访问数据或资源之前,首先试图获取(Acquire)锁,并在访问结束之后释放(Release)锁。在锁被占用时试图获取锁,线程会进入等待状态,直到锁被释放再次变为可用。

二元信号量

二元信号量(Binary Semaphore)是一种最简单的锁,它有两种状态:占用和非占用。它适合只能被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量锁的线程会获得该锁,并将二元信号量锁置为占用状态,之后其它试图获取该二元信号量的线程会进入等待状态,直到该锁被释放。

信号量

多元信号量允许多个线程访问同一个资源,多元信号量简称信号量(Semaphore),对于允许多个线程并发访问的资源,这是一个很好的选择。一个初始值为N的信号量允许N个线程并发访问。线程访问资源时首先获取信号量锁,进行如下操作:

1. 将信号量的值减1;
2. 如果信号量的值小于0,则进入等待状态,否则继续执行;

访问资源结束之后,线程释放信号量锁,进行如下操作:

1. 将信号量的值加1;
2. 如果信号量的值小于1(等于0),唤醒一个等待中的线程;

互斥量

互斥量(Mutex)和二元信号量类似,资源仅允许一个线程访问。与二元信号量不同的是,信号量在整个系统中可以被任意线程获取和释放,也就是说,同一个信号量可以由一个线程获取而由另一线程释放。而互斥量则要求哪个线程获取了该互斥量锁就由哪个线程释放,其它线程越俎代庖释放互斥量是无效


临界区

临界区(Critical Section)是一种比互斥量更加严格的同步手段。互斥量和信号量在系统的任何进程都是可见的,也就是说一个进程创建了一个互斥量或信号量,另一进程试图获取该锁是合法的。而临界区的作用范围仅限于本进程,其它的进程无法获取该锁。除此之处,临界区与互斥量的性质相同。

读写锁

读写锁(Read-Write Lock)允许多个线程同时对同一个数据进行读操作,而只允许一个线程进行写操作。这是因为读操作不会改变数据的内容,是安全的;而写操作会改变数据的内容,是不安全的。对同一个读写锁,有两种获取方式:共享的(Shared)和独占的(Exclusive)。当锁处于自由状态时,试图以任何一种方式获取锁都能成功,并将锁置为对应的状态;如果锁处于共享状态,其它线程以共享方式获取该锁,仍然能成功,此时该锁分配给了多个线程;如果其它线程试图如独占的方式获取处于共享状态的锁,它必须等待所有线程释放该锁;处于独占状态的锁阻止任何线程获取该锁,不论它们以何种方式。获取读写锁的方式总结如下:

读写锁的状态以共享方式获取以独占方式获取自由成功成功共享成功等待独占等待等待

表 1 :获取读写锁的方式





















   前面《多线程二  多线程中的隐蔽问题揭秘》提出了一个经典的多线程同步互斥问题,这个问题包括了主线程与子线程的同步,子线程间的互斥,是一道非常经典的多线程同步互斥问题范例,后面分别用了四篇

多线程三 经典线程同步之关键段CS

多线程四 经典线程同步之互斥量Mutex

多线程五 经典线程同步之事件Event

多线程六 经典线程同步之信号量Semaphore

       来详细介绍常用的线程同步互斥机制——关键段、事件、互斥量、信号量。下面对它们作个总结,帮助大家梳理各个知识点。

       首先来看下关于线程同步互斥的概念性的知识,相信大家通过前面的文章,已经对线程同步互斥有一定的认识了,也能模糊的说出线程同步互斥的各种概念性知识,下面再列出从《计算机操作系统》一书中选取的一些关于线程同步互斥的描述。相信先有个初步而模糊的印象再看下权威的定义,应该会记忆的特别深刻。

1、线程(进程)同步的主要任务

       答:在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用同一台打印机,会使打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性。

2、线程(进程)之间的制约关系?

       当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系。

     (1)间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享CPU,共享I/O设备,所谓间接相互制约即源于这种资源共享,打印机就是最好的例子,线程A在使用打印机时,其它线程都要等待。

     (2)直接相互制约。这种制约主要是因为线程之间的合作,如有线程A将计算结果提供给线程B作进一步处理,那么线程B在线程A将数据送达之前都将处于阻塞状态。

       间接相互制约可以称为互斥,直接相互制约可以称为同步,对于互斥可以这样理解,线程A和线程B互斥访问某个资源则它们之间就会产个顺序问题——要么线程A等待线程B操作完毕,要么线程B等待线程操作完毕,这其实就是线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步

3、临界资源和临界区

       在一段时间内只允许一个线程访问的资源就称为临界资源或独占资源,计算机中大多数物理设备,进程中的共享变量等待都是临界资源,它们要求被互斥的访问。每个进程中访问临界资源的代码称为临界区。

       看完概念性知识,下面用几个表格来帮助大家更好的记忆和运用多线程同步互斥的四个实现方法——关键段、事件、互斥量、信号量。

关键段CS与互斥量Mutex

创建或初始化

销毁

进入互斥区域

离开互斥区域

关键段CS

Initialize-

CriticalSection

Delete-

CriticalSection

Enter-

CriticalSection

Leave-

CriticalSection

互斥量Mutex

CreateMutex

CloseHandle

等待系列函数如WaitForSingleObject

ReleaseMutex

       关键段与互斥量都有“线程所有权”概念,可以将“线程所有权”理解成旅馆的房卡,在旅馆前台登记名字拥有房卡后是可以多次进出房间的,其它人则无法进入直到你交出房卡。每个线程必须先通过EnterCriticalSectionWaitForSingleObject来尝试获得“线程所有权”才能调用LeaveCriticalSectionReleaseMutex。否则会调用失败,这就相当于伪造房卡去办理退房手续——由于登记本上没有你的名字所以会被拒绝。互斥量能很好的处理“遗弃”情况,因此在多进程之间可以放心的使用。

事件Event

创建

销毁

使事件触发

使事件未触发

事件Event

CreateEvent

CloseHandle

SetEvent

ResetEvent

       注意事件的手动置位和自动置位要分清楚,不要混淆了。

信号量Semaphore

创建

销毁

递减计数

递增计数

信号量

Semaphore

Create-

Semaphore

CloseHandle

等待系列函数如WaitForSingleObject

Release-

Semaphore

       信号量在计数大于0时表示触发状态,调用WaitForSingleObject不会阻塞,等于0表示未触发状态,调用WaitForSingleObject会阻塞直到有其它线程递增了计数。

        注意:互斥量,事件,信号量都是内核对象,可以跨进程使用(通过OpenMutexOpenEventOpenSemaphore)。












多线程五 经典线程同步之事件Event

标签: 多线程nullthreadfunaccess文档
 4773人阅读 评论(1) 收藏 举报
 分类:

1、首先介绍下如何使用事件。

    事件Event实际上是个内核对象,它的使用非常方便。下面列出一些常用的函数。

1)第一个 CreateEvent

函数功能:创建事件

函数原型:

HANDLE CreateEvent(

LPSECURITY_ATTRIBUTESlpEventAttributes,

BOOLbManualReset,

BOOLbInitialState,

LPCTSTRlpName

);

函数说明:

第一个参数表示安全控制,一般直接传入NULL。

第二个参数确定事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。如果为自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态。打个小小比方,手动置位事件相当于教室门,教室门一旦打开(被触发),所以有人都可以进入直到老师去关上教室门(事件变成未触发)。自动置位事件就相当于医院里拍X光的房间门,门打开后只能进入一个人,这个人进去后会将门关上,其它人不能进入除非门重新被打开(事件重新被触发)。

第三个参数表示事件的初始状态,传入TRUR表示已触发。

第四个参数表示事件的名称,传入NULL表示匿名事件。

2)第二个 OpenEvent

函数功能:根据名称获得一个事件句柄。

函数原型:

HANDLE OpenEvent(

DWORDdwDesiredAccess,

BOOLbInheritHandle,

LPCTSTRlpName     //名称

);

函数说明:

第一个参数表示访问权限,对事件一般传入EVENT_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示事件句柄继承性,一般传入TRUE即可。

第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个事件。

3)第三个SetEvent

函数功能:触发事件

函数原型:BOOL SetEvent(HANDLEhEvent);

函数说明:每次触发后,必有一个或多个处于等待状态下的线程变成可调度状态。

4)第四个ResetEvent

函数功能:将事件设为末触发

函数原型:BOOL ResetEvent(HANDLEhEvent);

5)最后一个事件的清理与销毁

由于事件是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。

2、在经典多线程问题中设置一个事件和一个关键段。用事件处理主线程与子线程的同步,用关键段来处理各子线程间的互斥。

详见代码:

[html] view plain copy
  1. #include <stdio.h>  
  2. #include <process.h>  
  3. #include <windows.h>  
  4. long g_nNum;  
  5. unsigned int __stdcall Fun(void *pPM);  
  6. const int THREAD_NUM = 10;  
  7. //事件与关键段  
  8. HANDLE  g_hThreadEvent;  
  9. CRITICAL_SECTION g_csThreadCode;  
  10. int main()  
  11. {  
  12.     printf("     经典线程同步 事件Event\n");  
  13.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  14.     //初始化事件和关键段 自动置位,初始无触发的匿名事件  
  15.     g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);   
  16.     InitializeCriticalSection(&g_csThreadCode);  
  17.   
  18.     HANDLE  handle[THREAD_NUM];   
  19.     g_nNum = 0;  
  20.     int i = 0;  
  21.     while (i < THREAD_NUM)   
  22.     {  
  23.         handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);  
  24.         WaitForSingleObject(g_hThreadEvent, INFINITE); //等待事件被触发  
  25.         i++;  
  26.     }  
  27.     WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  
  28.   
  29.     //销毁事件和关键段  
  30.     CloseHandle(g_hThreadEvent);  
  31.     DeleteCriticalSection(&g_csThreadCode);  
  32.     return 0;  
  33. }  
  34. unsigned int __stdcall Fun(void *pPM)  
  35. {  
  36.     int nThreadNum = *(int *)pPM;   
  37.     SetEvent(g_hThreadEvent); //触发事件  
  38.       
  39.     Sleep(50);//some work should to do  
  40.       
  41.     EnterCriticalSection(&g_csThreadCode);  
  42.     g_nNum++;  
  43.     Sleep(0);//some work should to do  
  44.     printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);   
  45.     LeaveCriticalSection(&g_csThreadCode);  
  46.     return 0;  
  47. }  

运行结果如下图:

       可以看出来,经典线线程同步问题已经圆满的解决了——线程编号的输出没有重复,说明主线程与子线程达到了同步。全局资源的输出是递增的,说明各子线程已经互斥的访问和输出该全局资源。

       现在我们知道了如何使用事件,但学习就应该要深入的学习,何况微软给事件还提供了PulseEvent()函数,所以接下来再继续深挖下事件Event,看看它还有什么秘密没。

先来看看这个函数的原形:

第五个PulseEvent

函数功能:将事件触发后立即将事件设置为未触发,相当于触发一个事件脉冲。

函数原型:BOOLPulseEvent(HANDLEhEvent);

函数说明:这是一个不常用的事件函数,此函数相当于SetEvent()后立即调用ResetEvent();此时情况可以分为两种:

1.对于手动置位事件,所有正处于等待状态下线程都变成可调度状态。

2.对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。

此后事件是末触发的。该函数不稳定,因为无法预知在调用PulseEvent ()时哪些线程正处于等待状态。

       下面对这个触发一个事件脉冲PulseEvent ()写一个例子,主线程启动7个子线程,其中有5个线程Sleep(10)后对一事件调用等待函数(称为快线程),另有2个线程Sleep(100)后也对该事件调用等待函数(称为慢线程)。主线程启动所有子线程后再Sleep(50)保证有5个快线程都正处于等待状态中。此时若主线程触发一个事件脉冲,那么对于手动置位事件,这5个线程都将顺利执行下去。对于自动置位事件,这5个线程中会有中一个顺利执行下去。而不论手动置位事件还是自动置位事件,那2个慢线程由于Sleep(100)所以会错过事件脉冲,因此慢线程都会进入等待状态而无法顺利执行下去。

代码如下:

[html] view plain copy
  1. //使用PluseEvent()函数  
  2. #include <stdio.h>  
  3. #include <conio.h>  
  4. #include <process.h>  
  5. #include <windows.h>  
  6. HANDLE  g_hThreadEvent;  
  7. //快线程  
  8. unsigned int __stdcall FastThreadFun(void *pPM)  
  9. {  
  10.     Sleep(10); //用这个来保证各线程调用等待函数的次序有一定的随机性  
  11.     printf("%s 启动\n", (PSTR)pPM);  
  12.     WaitForSingleObject(g_hThreadEvent, INFINITE);  
  13.     printf("%s 等到事件被触发 顺利结束\n", (PSTR)pPM);  
  14.     return 0;  
  15. }  
  16. //慢线程  
  17. unsigned int __stdcall SlowThreadFun(void *pPM)  
  18. {  
  19.     Sleep(100);  
  20.     printf("%s 启动\n", (PSTR)pPM);  
  21.     WaitForSingleObject(g_hThreadEvent, INFINITE);  
  22.     printf("%s 等到事件被触发 顺利结束\n", (PSTR)pPM);  
  23.     return 0;  
  24. }  
  25. int main()  
  26. {  
  27.     printf("  使用PluseEvent()函数\n");  
  28.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  29.   
  30.     BOOL bManualReset = FALSE;  
  31.     //创建事件 第二个参数手动置位TRUE,自动置位FALSE  
  32.     g_hThreadEvent = CreateEvent(NULL, bManualReset, FALSE, NULL);  
  33.     if (bManualReset == TRUE)  
  34.         printf("当前使用手动置位事件\n");  
  35.     else  
  36.         printf("当前使用自动置位事件\n");  
  37.   
  38.     char szFastThreadName[5][30] = {"快线程1000", "快线程1001", "快线程1002", "快线程1003", "快线程1004"};  
  39.     char szSlowThreadName[2][30] = {"慢线程196", "慢线程197"};  
  40.   
  41.     int i;  
  42.     for (i = 0; i < 5; i++)  
  43.         _beginthreadex(NULL, 0, FastThreadFun, szFastThreadName[i], 0, NULL);  
  44.     for (i = 0; i < 2; i++)  
  45.         _beginthreadex(NULL, 0, SlowThreadFun, szSlowThreadName[i], 0, NULL);  
  46.       
  47.     Sleep(50); //保证快线程已经全部启动  
  48.     printf("现在主线程触发一个事件脉冲 - PulseEvent()\n");  
  49.     PulseEvent(g_hThreadEvent);//调用PulseEvent()就相当于同时调用下面二句  
  50.     //SetEvent(g_hThreadEvent);  
  51.     //ResetEvent(g_hThreadEvent);  
  52.       
  53.     Sleep(3000);   
  54.     printf("时间到,主线程结束运行\n");  
  55.     CloseHandle(g_hThreadEvent);  
  56.     return 0;  
  57. }  

对自动置位事件,运行结果如下:

对手动置位事件,运行结果如下:

最后总结下事件Event

1.事件是内核对象,事件分为手动置位事件自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。

2.事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。

3.事件可以解决线程间同步问题,因此也能解决互斥问题。

文章转载于:http://blog.csdn.net/morewindows/article/details/7445233


0 0
原创粉丝点击