c++多线程入门与提高 blogdown整理

14 篇文章 0 订阅
11 篇文章 0 订阅

今天我给大家讲一讲C++中的多线程编程技术,C++本身并没有提供任何多线程机制,但是在windows下,我们可以调用SDK win32 api来编写多线程的程序,下面我就此简单的讲一下:

创建线程的函数

  HANDLE CreateThread(
    LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
    SIZE_T dwStackSize,                       // initial stack size
    LPTHREAD_START_ROUTINE lpStartAddress,    // thread function
    LPVOID lpParameter,                       // thread argument
    DWORD dwCreationFlags,                    // creation option
    LPDWORD lpThreadId                        // thread identifier
  );
 
在这里我们只用到了第三个和第四个参数,第三个参数传递了一个函数的地址,也是我们要指定的新的线程。第四个参数是传给新线程的参数指针
  eg1:

  #include <iostream>
  #include <windows.h>
  using namespace std;

  DWORD WINAPI Fun(LPVOID lpParamter)
  {
        while(1) { cout<<”Fun display!”<<endl; }
  }

  int main()
  {
      HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
      CloseHandle(hThread);
      while(1) { cout<<”main display!”<<endl;  }
      return 0;
  }

我们可以看到主线程(main函数)和我们自己的线程(Fun函数)是随机地交替执行的,但是两个线程输出太快,使我们很难看清楚,我们可以使用函数
  VOID Sleep(
    DWORD dwMilliseconds   // sleep time
  );
来暂停线程的执行,dwMilliseconds表示千分之一秒,所以
  Sleep(1000);
表示暂停1秒

  eg2:
 
  #include <iostream>
  #include <windows.h>
  using namespace std;

  DWORD WINAPI Fun(LPVOID lpParamter)
  {
       
        while(1) { cout<<”Fun display!”<<endl; Sleep(1000);}
  }

  int main()
  {
      HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
      CloseHandle(hThread);
      while(1) { cout<<”main display!”<<endl;  Sleep(2000);}
      return 0;
  }
  执行上述代码,这次我们可以清楚地看到在屏幕上交错地输出Fun display!和main display!,我们发现这两个函数确实是并发运行的,细心的读者可能会发现我们的程序是每当Fun函数和main函数输出内容后就会输出换行,但是我们看到的确是有的时候程序输出换行了,有的时候确没有输出换行,甚至有的时候是输出两个换行。这是怎么回事?下面我们把程序改一下看看:
  eg3:
 
  #include <iostream>
  #include <windows.h>
  using namespace std;

  DWORD WINAPI Fun(LPVOID lpParamter)
  {
       
        while(1) { cout<<”Fun display!\n”; Sleep(1000);}
  }

  int main()
  {
      HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
      CloseHandle(hThread);
      while(1) { cout<<”main display!\n”;  Sleep(2000);}
      return 0;
  }
  我们再次运行这个程序,我们发现这时候正如我们预期的,正确地输出了我们想要输出的内容并且格式也是正确的。下面我就来讲一下此前我们的程序为什么没有正确的运行。多线程的程序时并发地运行的,多个线程之间如果公用了一些资源的话,我们并不能保证这些资源都能正确地被利用,因为这个时候资源并不是独占的,举个例子吧:
  eg4:
  加入有一个资源 int a = 3
  有一个线程函数 selfAdd() 该函数是使a = a+a
  又有一个线程函数 selfSub() 该函数是使a = a-a
 
  我们假设上面两个线程正在并发欲行,如果selfAdd在执行的时候,我们的目的是想让a编程6,但此时selfSub得到了运行的机会,所以a变成了0,等到selfAdd的到执行的机会后,a = a+a ,但是此时a确是0,并没有如我们所预期的那样的到6,我们回到前面eg2,在这里,我们可以把屏幕看成是一个资源,这个资源被两个线程所共用,加入当Fun函数输出了Fun display!后,将要输出endl(也就是清空缓冲区并换行,在这里我们可以不用理解什么事缓冲区),但此时main函数确得到了运行的机会,此时Fun函数还没有来得及输出换行就把CPU让给了main函数,而这时main函数就直接在Fun display!后输出main display!,至于为什么有的时候程序会连续输出两个换行,读者可以采用同样的分析方法来分析,在这里我就不多讲了,留给读者自己思考了。
那么为什么我们把eg2改成eg3就可以正确的运行呢?原因在于,多个线程虽然是并发运行的,但是有一些操作是必须一气呵成的,不允许打断的,所以我们看到eg2和eg3的运行结果是不一样的。

  那么,是不是eg2的代码我们就不可以让它正确的运行呢?答案当然是否,下面我就来讲一下怎样才能让eg2的代码可以正确运行。这涉及到多线程的同步问题。对于一个资源被多个线程共用会导致程序的混乱,我们的解决方法是只允许一个线程拥有对共享资源的独占,这样就能够解决上面的问题了。
  HANDLE CreateMutex(
    LPSECURITY_ATTRIBUTES lpMutexAttributes,  // SD
    BOOL bInitialOwner,                       // initial owner
    LPCTSTR lpName                            // object name
  );
该函数用于创造一个独占资源,第一个参数我们没有使用,可以设为NULL,第二个参数指定该资源初始是否归属创建它的进程,第三个参数指定资源的名称。
  HANDLE hMutex = CreateMutex(NULL,TRUE,”screen”);
这条语句创造了一个名为screen并且归属于创建它的进程的资源
 
  BOOL ReleaseMutex(
    HANDLE hMutex   // handle to mutex
  );
该函数用于释放一个独占资源,进程一旦释放该资源,该资源就不再属于它了,如果还要用到,需要重新申请得到该资源。申请资源的函数如下

  DWORD WaitForSingleObject(
    HANDLE hHandle,        // handle to object
    DWORD dwMilliseconds   // time-out interval
  );
第一个参数指定所申请的资源的句柄,第二个参数一般指定为INFINITE,表示如果没有申请到资源就一直等待该资源,如果指定为0,表示一旦得不到资源就返回,也可以具体地指定等待多久才返回,单位是千分之一秒。好了,该到我们来解决eg2的问题的时候了,我们可以把eg2做一些修改,如下
  eg5:
 
  #include <iostream>
  #include <windows.h>
  using namespace std;
  HANDLE hMutex;
  DWORD WINAPI Fun(LPVOID lpParamter)
  {
       
        while(1) {
                 WaitForSingleObject(hMutex, INFINITE);
                 cout<<”Fun display!”<<endl;
                 Sleep(1000);
                 ReleaseMutex(hMutex);
        }
  }

  int main()
  {
      HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
      hMutex = CreateMutex(NULL, FALSE, “screen”);
      CloseHandle(hThread);
      while(1) {
               WaitForSingleObject(hMutex, INFINITE);
               cout<<”main display!”<<endl; 
               Sleep(2000);
               ReleaseMutex(hMutex);
      }
      return 0;
  }

 

 

多线程使用总结

基础:

1)最好使用C++ runtime的函数创建线程,即调用_beginthreadex创建线程。CreateThread()函数并不会执行C运行时数据块的变量的每线程初始化,因此在任何使用C运行时库的应用中,不能使用CrateThread()函数。

2)最好不要显示的调用ExitThread()或TerminateThread(),因为调用这些不进行清理工作。

3)SuspendThread()挂起线程时,要考虑该线程是否拥有Mutex或Semaphore,如果拥有的话可能会导致死锁。

4)信号量Semaphore,是一个可以限制对指定的临界段进行访问的线程的数目的数据结构。

5)互斥量Mutex和关键代码段CriticalSection,他们的作用是相同的,都是用来保证某时刻只有一个线程能够访问全局或静态的资源。区别是:Mutex是内核对象,可以设置等待超时时间,可以在不同的进程的线程中使用,但是所消耗的时间也比较多。CriticalSection与Mutex相反。

6)互锁函数,可以用来实现原子操作。对于一些简单的问题,比如说int变量的自加,交换。。。

7)线程局部存储(TLS),可以为全局或静态变量对不同的线程有不同的拷贝。

高级:

8)线程池,可以实现在程序中根据需要动态的创建线程,比如在server端,根据访问的用户的多少来创建线程的多少。在windows2000以后增加了创建线程池的API,比如 QueueUserWorkItem()。

9)线程的优先级,用来保证重要的事情先被完成。不能使用线程的优先级来解决线程的同步问题。

10) 处理器亲和,就是将线程固定在某cpu上执行,这样在某些情况下有助于提高性能,例如我们有双核的且支持超线程技术的cpu,我们有4个线程,有2个是IO操作,有2个是大量的计算,对于上面的问题,我们就可以使用处理器亲和,使用API设置,来达到cpu使用的均和,更好的提高性能。

11) 纤程,用户级的线程机制,比线程小的单位,开发人员负责管理纤程的调度,同时负责确定纤程何时在线程时间片上下文中运行,一般不会带来性能的提高,主要的目的是为开发人员调度多个不需要并行执行的任务提供一个便捷的机制。

12) 是否需要使用多线程,最总是需要测试来决定的,而且对于不同的CPU结果也不相同。避免使用过多的线程而带来性能下降。
其他多线程技术:

13) MFC多线程,多Windows多线程API的封装。

14) Boost多线程。

15) POSIX,Pthreads,是一个可以移植的多线程库。一般是Linux和Unix是商用较多。

16) Intel Open MP+,是一种可移植的多线程解决方案。(www.openmp.org)。

多线程调试:

17) 支持多线程的Log。

18) VS2005中,可以查看Thread窗口,挂起或恢复线程,切换当前的线程和检查线程状态。或是使用TracePoint来输出消息到output。

19)使用SetThreadName()来对线程命名,使用线程信息block (http://www.codeproject.com/threads/xtib.asp)。

20) 使用Intel多线程线程检测器,Intel调试器。。。

----------------------------------------------------------------------------------------------------------

1.线程的创建

线程的创建是通过函数CreateThread来实现的,调用成功返回句柄和一个id。


HANDLE CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes,   //线程的安全属性
DWORD dwStackSize,                                            //线程堆栈的大小
LPTHREAD_START_ROUTINE lpStartAddress,     //线程函数的起始地址
LPVOID lpParameter,                                                //传递个线程函数的参数
DWORD dwCreationFlags,                                        //线程创建后是否立即启动
LPDWORD lpThreadId                                            //线程的ID号

);

每个线程都要都要有个入口函数(也就是线程函数),

下面是一个线程函数定义的例子:DWORD WINAPI ThreadProc(LPVOID lpParam);

WINAPI是个宏 #define WINAPI _stdcall;

其中lpThreadAttributes参数为一个指向SECURITY_ATTRIBUTES结构的指针。如果想让对象为缺省安全属性的话,可以传一个NULL,如果想让任一个子进程都可继承一个该线程对象句柄,必须指定一个SECURITY_ATTRIBUTES结构,其中bInheritHandle成员初始化为TRUE。
参数dwStackSize表示线程为自己所用堆栈分配的地址空间大小,0表示采用系统缺省值。
参数lpStartAddress用来表示新线程开始执行时代码所在函数的地址,即为线程函数。
lpParameter为传入线程函数的参数,
dwCreationFlags参数指定控制线程创建的附加标志,可以取两种值。如果该参数为0,线程就会立即开始执行,如果该参数为CREATE_SUSPENDED,则系统产生线程后,初始化CPU,登记CONTEXT结构的成员,准备好执行该线程函数中的第一条指令,但并不马上执行,而是挂起该线程。
最后一个参数lpThreadId 是一个DWORD类型地址,返回赋给该新线程的ID值。

2.线程的内核对象

线程内核对象就是一个数据结构,包含了线程的一些信息。当成功调用CreateThread后,系统都会在内部为新线程分配一个内核对象。

内核对象的基本成员:

Context

Context也就是线程上下文,包括了一组CPU寄存器,反映了线程上次运行时CPU寄存器的状态。

Usage Count

Usage Count就是线程的使用计数。线程只要没有结束,Usage Count就至少为1。当这个值为0时,系统就认为已经没有任何进程在引用此内核对象了,于是线程内核对象就要从内存中撤销。在创建一个新的线程时,CreateThread函数返回内核对象的句柄,相当于打开一次新创建的内核对象,这会促使Usage Count加1,所以创建一个新的线程后,Usage Count就是2。调用函数OpenThread会使Usage Count再次加1,而CloseHandle会使Usage Count减1。

Suspend Count

Suspend Count暂停次数。用ResumeThread函数可以唤醒一个线程,会减少Suspend Count,当Suspend Count为0时线程被恢复运行。用SuspendThread函数挂起一个线程,这个函数会增加Suspend Count。

Exit Code

Exit Code就是线程退出代码(也就是线程函数的返回值),当线程函数还没有执行完时,Exit Code的值就是STILL_ACTIVE。

Signaled

Signaled指示线程对象是否为”受信”状态。线程运行时Signaled永远是False也就是未受信。只有线程结束后才是受信状态。

3.线程的优先级

首先得说下Windows调度线程的原则:只有优先级最高的线程是可调度的,操作系统就不会将CPU分配给优先级低的。但是有些方法会使线程暂停或者不可调度,如WaitForSingleObject或者GetMessage,这样优先级低的就可以分配到CPU的时间片,一旦优先级高的线程被唤醒了,优先级低的线程将被阻塞。Windows支持6个优先级类:idle、below normal、normal、above normal、high、real-time。

设定线程的相对优先级

当一个线程被首次创建时,它的优先级等同于它所属进程的优先级。在单个进程内可以通过调用SetThreadPriority函数改变线程的相对优先级。一个线程的优先级是相对于其所属的进程的优先级而言的。

BOOLSetThreadPriority(HANDLE hThread,intnPriority);

其中参数hThread是指向待修改 优先级线程的句柄,nPriority可以是以下的值:

THREAD_PRIORITY_LOWEST,
THREAD_PRIORITY_BELOW_NORMAL,
THREAD_PRIORITY_NORMAL,
THREAD_PRIORITY_ABOVE_NORMAL,
THREAD_PRIORITY_HIGHEST

4.线程的终止

终止线程有4种方法:

1.       使用ExitThread函数终止线程
采用这种方法终止线程,会使系统释放掉此线程使用的所有资源但是c/c++资源却不能得到正确的清除。
2.       使用TerminateThread函数在一个线程中强制终止另一个线程的执行
使用TerminateThread函数终止线程,系统不会释放线程使用的堆栈,所以除非迫不得已,尽量避免使用这个函数终止线程。
3.       使用ExitProcess函数结束进程
使用这个方法相当与对进程中的每个线程使用TerminateThread函数。
4.       线程函数自然退出
这种方法是最好的,线程函数返回时Windows将终止线程的执行。
函数说明:
Handle OpenThread(
DWORD dwDesiredAccess,    //想要访问的权限
BOOL bInheritHandle,             //此函数返回的句柄是否可以被子进程继承
DWORD deThreadId               //目标线程的ID
)
Void ExitThread(DWORD dwExitCode);//dwExitCode线程的退出代码
BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);//hThread目标线程句柄

5、挂起及恢复线程

先前我提到过可以创建挂起状态的线程(通过传递CREATE_SUSPENDED标志给函数CreateThread来实现)。当你这样做时,系统创建指定线程的核心对象,创建线程的栈,在CONTEXT结构中初始化线程CPU注册成员。然而,线程对象被分配了一个初始挂起计数值1,这表明了系统将不再分配CPU去执行线程。要开始执行一个线程,另一个线程必须调用ResumeThread并传递给它调用CreateThread时返回的线程句柄。

DWORD ResumeThread(HANDLEhThread);

一个线程可以被挂起多次。如果一个线程被挂起3次, 则该线程在它被分配CPU之前必须被恢复3次。除了在创建线程时使用CREATE_SUSPENDED标志,你还可以用SuspendThread函数挂起线程。

DWORDSuspendThread(HANDLE hThread);

----------------------------------------------------------------------------------------------------------------------------------------------------

正文
  使线程同步
  在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作。更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解。正常情况下对这种处理结果的了解应当在其处理任务完成后进行。
  如果不采取适当的措施,其他线程往往会在线程处理任务结束前就去访问处理结果,这就很有可能得到有关处理结果的错误了解。例如,多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修改后的。
  为了确保读线程读取到的是经过修改的变量,就必须在向变量写入数据时禁止其他线程对其的任何访问,直至赋值过程结束后再解除对其他线程的访问限制。象这种保证线程能了解其他线程任务处理结束后的处理结果而采取的保护措施即为线程同步。
  线程同步是一个非常大的话题,包括方方面面的内容。从大的方面讲,线程的同步可分用户模式的线程同步和内核对象的线程同步两大类。用户模式中线程的同步方法主要有原子访问和临界区等方法。其特点是同步速度特别快,适合于对线程运行速度有严格要求的场合。
  内核对象的线程同步则主要由事件、等待定时器、信号量以及信号灯等内核对象构成。由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。

  临界区
  临界区(Critical Section)是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
  临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界区。所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。
Visual C++线程同步技术剖析 - 咖啡哲学 - 下雨了吗......
图1 使用临界区保持线程同步
  下面通过一段代码展示了临界区在保护多线程访问的共享资源中的作用。通过两个线程来分别对全局变量g_cArray[10]进行写入操作,用临界区结构对象g_cs来保持线程的同步,并在开启线程前对其进行初始化。为了使实验效果更加明显,体现出临界区的作用,在线程函数对共享资源g_cArray[10]的写入时,以Sleep()函数延迟1毫秒,使其他线程同其抢占CPU的可能性增大。如果不使用临界区对其进行保护,则共享资源数据将被破坏(参见图1(a)所示计算结果),而使用临界区对线程保持同步后则可以得到正确的结果(参见图1(b)所示计算结果)。代码实现清单附下:

// 临界区结构对象
CRITICAL_SECTION g_cs;
// 共享资源
char g_cArray[10];
UINT ThreadProc10(LPVOID pParam)
{
 // 进入临界区
 EnterCriticalSection(&g_cs);
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 离开临界区
 LeaveCriticalSection(&g_cs);
 return 0;
}
UINT ThreadProc11(LPVOID pParam)
{
 // 进入临界区
 EnterCriticalSection(&g_cs);
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 离开临界区
 LeaveCriticalSection(&g_cs);
 return 0;
}
……
void CSample08View::OnCriticalSection()
{
 // 初始化临界区
 InitializeCriticalSection(&g_cs);
 // 启动线程
 AfxBeginThread(ThreadProc10, NULL);
 AfxBeginThread(ThreadProc11, NULL);
 // 等待计算完毕
 Sleep(300);
 // 报告计算结果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}


  在使用临界区时,一般不允许其运行时间过长,只要进入临界区的线程还没有离开,其他所有试图进入此临界区的线程都会被挂起而进入到等待状态,并会在一定程度上影响。程序的运行性能。尤其需要注意的是不要将等待用户输入或是其他一些外界干预的操作包含到临界区。如果进入了临界区却一直没有释放,同样也会引起其他线程的长时间等待。换句话说,在执行了EnterCriticalSection()语句进入临界区后无论发生什么,必须确保与之匹配的LeaveCriticalSection()都能够被执行到。可以通过添加结构化异常处理代码来确保LeaveCriticalSection()语句的执行。虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
  MFC为临界区提供有一个CCriticalSection类,使用该类进行线程同步处理是非常简单的,只需在线程函数中用CCriticalSection类成员函数Lock()和UnLock()标定出被保护代码片段即可。对于上述代码,可通过CCriticalSection类将其改写如下:

// MFC临界区类对象
CCriticalSection g_clsCriticalSection;
// 共享资源
char g_cArray[10];
UINT ThreadProc20(LPVOID pParam)
{
 // 进入临界区
 g_clsCriticalSection.Lock();
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 离开临界区
 g_clsCriticalSection.Unlock();
 return 0;
}
UINT ThreadProc21(LPVOID pParam)
{
 // 进入临界区
 g_clsCriticalSection.Lock();
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 离开临界区
 g_clsCriticalSection.Unlock();
 return 0;
}
……
void CSample08View::OnCriticalSectionMfc()
{
 // 启动线程
 AfxBeginThread(ThreadProc20, NULL);
 AfxBeginThread(ThreadProc21, NULL);
 // 等待计算完毕
 Sleep(300);
 // 报告计算结果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

  管理事件内核对象
  在前面讲述线程通信时曾使用过事件内核对象来进行线程间的通信,除此之外,事件内核对象也可以通过通知操作的方式来保持线程的同步。对于前面那段使用临界区保持线程同步的代码可用事件对象的线程同步方法改写如下:

// 事件句柄
HANDLE hEvent = NULL;
// 共享资源
char g_cArray[10];
……
UINT ThreadProc12(LPVOID pParam)
{
 // 等待事件置位
 WaitForSingleObject(hEvent, INFINITE);
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 处理完成后即将事件对象置位
 SetEvent(hEvent);
 return 0;
}
UINT ThreadProc13(LPVOID pParam)
{
 // 等待事件置位
 WaitForSingleObject(hEvent, INFINITE);
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 处理完成后即将事件对象置位
 SetEvent(hEvent);
 return 0;
}
……
void CSample08View::OnEvent()
{
 // 创建事件
 hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
 // 事件置位
 SetEvent(hEvent);
 // 启动线程
 AfxBeginThread(ThreadProc12, NULL);
 AfxBeginThread(ThreadProc13, NULL);
 // 等待计算完毕
 Sleep(300);
 // 报告计算结果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

 

 

在创建线程前,首先创建一个可以自动复位的事件内核对象hEvent,而线程函数则通过WaitForSingleObject()等待函数无限等待hEvent的置位,只有在事件置位时WaitForSingleObject()才会返回,被保护的代码将得以执行。对于以自动复位方式创建的事件对象,在其置位后一被WaitForSingleObject()等待到就会立即复位,也就是说在执行ThreadProc12()中的受保护代码时,事件对象已经是复位状态的,这时即使有ThreadProc13()对CPU的抢占,也会由于WaitForSingleObject()没有hEvent的置位而不能继续执行,也就没有可能破坏受保护的共享资源。在ThreadProc12()中的处理完成后可以通过SetEvent()对hEvent的置位而允许ThreadProc13()对共享资源g_cArray的处理。这里SetEvent()所起的作用可以看作是对某项特定任务完成的通知。
  使用临界区只能同步同一进程中的线程,而使用事件内核对象则可以对进程外的线程进行同步,其前提是得到对此事件对象的访问权。可以通过OpenEvent()函数获取得到,其函数原型为:

HANDLE OpenEvent(
 DWORD dwDesiredAccess, // 访问标志
 BOOL bInheritHandle, // 继承标志
 LPCTSTR lpName // 指向事件对象名的指针
);


  如果事件对象已创建(在创建事件时需要指定事件名),函数将返回指定事件的句柄。对于那些在创建事件时没有指定事件名的事件内核对象,可以通过使用内核对象的继承性或是调用DuplicateHandle()函数来调用CreateEvent()以获得对指定事件对象的访问权。在获取到访问权后所进行的同步操作与在同一个进程中所进行的线程同步操作是一样的。
  如果需要在一个线程中等待多个事件,则用WaitForMultipleObjects()来等待。WaitForMultipleObjects()与WaitForSingleObject()类似,同时监视位于句柄数组中的所有句柄。这些被监视对象的句柄享有平等的优先权,任何一个句柄都不可能比其他句柄具有更高的优先权。WaitForMultipleObjects()的函数原型为:

DWORD WaitForMultipleObjects(
 DWORD nCount, // 等待句柄数
 CONST HANDLE *lpHandles, // 句柄数组首地址
 BOOL fWaitAll, // 等待标志
 DWORD dwMilliseconds // 等待时间间隔
);


  参数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。fWaitAll对指定的这nCount个内核对象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。dwMilliseconds在这里的作用与在WaitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回WAIT_TIMEOUT。如果返回WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1中的某个值,则说明所有指定对象的状态均为已通知状态(当fWaitAll为TRUE时)或是用以减去WAIT_OBJECT_0而得到发生通知的对象的索引(当fWaitAll为FALSE时)。如果返回值在WAIT_ABANDONED_0与WAIT_ABANDONED_0+nCount-1之间,则表示所有指定对象的状态均为已通知,且其中至少有一个对象是被丢弃的互斥对象(当fWaitAll为TRUE时),或是用以减去WAIT_OBJECT_0表示一个等待正常结束的互斥对象的索引(当fWaitAll为FALSE时)。 下面给出的代码主要展示了对WaitForMultipleObjects()函数的使用。通过对两个事件内核对象的等待来控制线程任务的执行与中途退出:

// 存放事件句柄的数组
HANDLE hEvents[2];
UINT ThreadProc14(LPVOID pParam)
{
 // 等待开启事件
 DWORD dwRet1 = WaitForMultipleObjects(2, hEvents, FALSE, INFINITE);
 // 如果开启事件到达则线程开始执行任务
 if (dwRet1 == WAIT_OBJECT_0)
 {
  AfxMessageBox("线程开始工作!");
  while (true)
  {
   for (int i = 0; i < 10000; i++);
   // 在任务处理过程中等待结束事件
   DWORD dwRet2 = WaitForMultipleObjects(2, hEvents, FALSE, 0);
   // 如果结束事件置位则立即终止任务的执行
   if (dwRet2 == WAIT_OBJECT_0 + 1)
    break;
  }
 }
 AfxMessageBox("线程退出!");
 return 0;
}
……
void CSample08View::OnStartEvent()
{
 // 创建线程
 for (int i = 0; i < 2; i++)
  hEvents[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
  // 开启线程
  AfxBeginThread(ThreadProc14, NULL);
  // 设置事件0(开启事件)
  SetEvent(hEvents[0]);
}
void CSample08View::OnEndevent()
{
 // 设置事件1(结束事件)
 SetEvent(hEvents[1]);
}


  MFC为事件相关处理也提供了一个CEvent类,共包含有除构造函数外的4个成员函数PulseEvent()、ResetEvent()、SetEvent()和UnLock()。在功能上分别相当与Win32 API的PulseEvent()、ResetEvent()、SetEvent()和CloseHandle()等函数。而构造函数则履行了原CreateEvent()函数创建事件对象的职责,其函数原型为:

CEvent(BOOL bInitiallyOwn = FALSE, BOOL bManualReset = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );


  按照此缺省设置将创建一个自动复位、初始状态为复位状态的没有名字的事件对象。封装后的CEvent类使用起来更加方便,图2即展示了CEvent类对A、B两线程的同步过程:
Visual C++线程同步技术剖析 - 咖啡哲学 - 下雨了吗......
图2 CEvent类对线程的同步过程示意
  B线程在执行到CEvent类成员函数Lock()时将会发生阻塞,而A线程此时则可以在没有B线程干扰的情况下对共享资源进行处理,并在处理完成后通过成员函数SetEvent()向B发出事件,使其被释放,得以对A先前已处理完毕的共享资源进行操作。可见,使用CEvent类对线程的同步方法与通过API函数进行线程同步的处理方法是基本一致的。前面的API处理代码可用CEvent类将其改写为:

// MFC事件类对象
CEvent g_clsEvent;
UINT ThreadProc22(LPVOID pParam)
{
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 事件置位
 g_clsEvent.SetEvent();
 return 0;
}
UINT ThreadProc23(LPVOID pParam)
{
 // 等待事件
 g_clsEvent.Lock();
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 return 0;
}
……
void CSample08View::OnEventMfc()
{
 // 启动线程
 AfxBeginThread(ThreadProc22, NULL);
 AfxBeginThread(ThreadProc23, NULL);
 // 等待计算完毕
 Sleep(300);
 // 报告计算结果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

 

  信号量内核对象
  信号量(Semaphore)内核对象对线程的同步方式与前面几种方法不同,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。
Visual C++线程同步技术剖析 - 咖啡哲学 - 下雨了吗......
图3 使用信号量对象控制资源
  下面结合图例3来演示信号量对象对资源的控制。在图3中,以箭头和白色箭头表示共享资源所允许的最大资源计数和当前可用资源计数。初始如图(a)所示,最大资源计数和当前可用资源计数均为4,此后每增加一个对资源进行访问的线程(用黑色箭头表示)当前资源计数就会相应减1,图(b)即表示的在3个线程对共享资源进行访问时的状态。当进入线程数达到4个时,将如图(c)所示,此时已达到最大资源计数,而当前可用资源计数也已减到0,其他线程无法对共享资源进行访问。在当前占有资源的线程处理完毕而退出后,将会释放出空间,图(d)已有两个线程退出对资源的占有,当前可用计数为2,可以再允许2个线程进入到对资源的处理。可以看出,信号量是通过计数来对线程访问资源进行控制的,而实际上信号量确实也被称作Dijkstra计数器。
  使用信号量内核对象进行线程同步主要会用到CreateSemaphore()、OpenSemaphore()、ReleaseSemaphore()、WaitForSingleObject()和WaitForMultipleObjects()等函数。其中,CreateSemaphore()用来创建一个信号量内核对象,其函数原型为:

HANDLE CreateSemaphore(
 LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性指针
 LONG lInitialCount, // 初始计数
 LONG lMaximumCount, // 最大计数
 LPCTSTR lpName // 对象名指针
);


  参数lMaximumCount是一个有符号32位值,定义了允许的最大资源计数,最大取值不能超过4294967295。lpName参数可以为创建的信号量定义一个名字,由于其创建的是一个内核对象,因此在其他进程中可以通过该名字而得到此信号量。OpenSemaphore()函数即可用来根据信号量名打开在其他进程中创建的信号量,函数原型如下:

HANDLE OpenSemaphore(
 DWORD dwDesiredAccess, // 访问标志
 BOOL bInheritHandle, // 继承标志
 LPCTSTR lpName // 信号量名
);


  在线程离开对共享资源的处理时,必须通过ReleaseSemaphore()来增加当前可用资源计数。否则将会出现当前正在处理共享资源的实际线程数并没有达到要限制的数值,而其他线程却因为当前可用资源计数为0而仍无法进入的情况。ReleaseSemaphore()的函数原型为:

BOOL ReleaseSemaphore(
 HANDLE hSemaphore, // 信号量句柄
 LONG lReleaseCount, // 计数递增数量
 LPLONG lpPreviousCount // 先前计数
);


  该函数将lReleaseCount中的值添加给信号量的当前资源计数,一般将lReleaseCount设置为1,如果需要也可以设置其他的值。WaitForSingleObject()和WaitForMultipleObjects()主要用在试图进入共享资源的线程函数入口处,主要用来判断信号量的当前可用资源计数是否允许本线程的进入。只有在当前可用资源计数值大于0时,被监视的信号量内核对象才会得到通知。
  信号量的使用特点使其更适用于对Socket(套接字)程序中线程的同步。例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,这时可以为没一个用户对服务器的页面请求设置一个线程,而页面则是待保护的共享资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某一页面进行访问,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。下面给出的示例代码即展示了类似的处理过程:

// 信号量对象句柄
HANDLE hSemaphore;
UINT ThreadProc15(LPVOID pParam)
{
 // 试图进入信号量关口
 WaitForSingleObject(hSemaphore, INFINITE);
 // 线程任务处理
 AfxMessageBox("线程一正在执行!");
 // 释放信号量计数
 ReleaseSemaphore(hSemaphore, 1, NULL);
 return 0;
}
UINT ThreadProc16(LPVOID pParam)
{
 // 试图进入信号量关口
 WaitForSingleObject(hSemaphore, INFINITE);
 // 线程任务处理
 AfxMessageBox("线程二正在执行!");
 // 释放信号量计数
 ReleaseSemaphore(hSemaphore, 1, NULL);
 return 0;
}
UINT ThreadProc17(LPVOID pParam)
{
 // 试图进入信号量关口
 WaitForSingleObject(hSemaphore, INFINITE);
 // 线程任务处理
 AfxMessageBox("线程三正在执行!");
 // 释放信号量计数
 ReleaseSemaphore(hSemaphore, 1, NULL);
 return 0;
}
……
void CSample08View::OnSemaphore()
{
 // 创建信号量对象
 hSemaphore = CreateSemaphore(NULL, 2, 2, NULL);
 // 开启线程
 AfxBeginThread(ThreadProc15, NULL);
 AfxBeginThread(ThreadProc16, NULL);
 AfxBeginThread(ThreadProc17, NULL);
}


Visual C++线程同步技术剖析 - 咖啡哲学 - 下雨了吗......
图4 开始进入的两个线程
Visual C++线程同步技术剖析 - 咖啡哲学 - 下雨了吗......
图5 线程二退出后线程三才得以进入
  上述代码在开启线程前首先创建了一个初始计数和最大资源计数均为2的信号量对象hSemaphore。即在同一时刻只允许2个线程进入由hSemaphore保护的共享资源。随后开启的三个线程均试图访问此共享资源,在前两个线程试图访问共享资源时,由于hSemaphore的当前可用资源计数分别为2和1,此时的hSemaphore是可以得到通知的,也就是说位于线程入口处的WaitForSingleObject()将立即返回,而在前两个线程进入到保护区域后,hSemaphore的当前资源计数减少到0,hSemaphore将不再得到通知,WaitForSingleObject()将线程挂起。直到此前进入到保护区的线程退出后才能得以进入。图4和图5为上述代脉的运行结果。从实验结果可以看出,信号量始终保持了同一时刻不超过2个线程的进入。
  在MFC中,通过CSemaphore类对信号量作了表述。该类只具有一个构造函数,可以构造一个信号量对象,并对初始资源计数、最大资源计数、对象名和安全属性等进行初始化,其原型如下:

CSemaphore( LONG lInitialCount = 1, LONG lMaxCount = 1, LPCTSTR pstrName = NULL, LPSECURITY_ATTRIBUTES lpsaAttributes = NULL );


  在构造了CSemaphore类对象后,任何一个访问受保护共享资源的线程都必须通过CSemaphore从父类CSyncObject类继承得到的Lock()和UnLock()成员函数来访问或释放CSemaphore对象。与前面介绍的几种通过MFC类保持线程同步的方法类似,通过CSemaphore类也可以将前面的线程同步代码进行改写,这两种使用信号量的线程同步方法无论是在实现原理上还是从实现结果上都是完全一致的。下面给出经MFC改写后的信号量线程同步代码:

// MFC信号量类对象
CSemaphore g_clsSemaphore(2, 2);
UINT ThreadProc24(LPVOID pParam)
{
 // 试图进入信号量关口
 g_clsSemaphore.Lock();
 // 线程任务处理
 AfxMessageBox("线程一正在执行!");
 // 释放信号量计数
 g_clsSemaphore.Unlock();
 return 0;
}
UINT ThreadProc25(LPVOID pParam)
{
 // 试图进入信号量关口
 g_clsSemaphore.Lock();
 // 线程任务处理
 AfxMessageBox("线程二正在执行!");
 // 释放信号量计数
 g_clsSemaphore.Unlock();
 return 0;
}
UINT ThreadProc26(LPVOID pParam)
{
 // 试图进入信号量关口
 g_clsSemaphore.Lock();
 // 线程任务处理
 AfxMessageBox("线程三正在执行!");
 // 释放信号量计数
 g_clsSemaphore.Unlock();
 return 0;
}
……
void CSample08View::OnSemaphoreMfc()
{
 // 开启线程
 AfxBeginThread(ThreadProc24, NULL);
 AfxBeginThread(ThreadProc25, NULL);
 AfxBeginThread(ThreadProc26, NULL);
}


  互斥内核对象
  互斥(Mutex)是一种用途非常广泛的内核对象。能够保证多个线程对同一共享资源的互斥访问。同临界区有些类似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。与其他几种内核对象不同,互斥对象在操作系统中拥有特殊代码,并由操作系统来管理,操作系统甚至还允许其进行一些其他内核对象所不能进行的非常规操作。为便于理解,可参照图6给出的互斥内核对象的工作模型:
Visual C++线程同步技术剖析 - 咖啡哲学 - 下雨了吗......
图6 使用互斥内核对象对共享资源的保护
  图(a)中的箭头为要访问资源(矩形框)的线程,但只有第二个线程拥有互斥对象(黑点)并得以进入到共享资源,而其他线程则会被排斥在外(如图(b)所示)。当此线程处理完共享资源并准备离开此区域时将把其所拥有的互斥对象交出(如图(c)所示),其他任何一个试图访问此资源的线程都有机会得到此互斥对象。
  以互斥内核对象来保持线程同步可能用到的函数主要有CreateMutex()、OpenMutex()、ReleaseMutex()、WaitForSingleObject()和WaitForMultipleObjects()等。在使用互斥对象前,首先要通过CreateMutex()或OpenMutex()创建或打开一个互斥对象。CreateMutex()函数原型为:

HANDLE CreateMutex(
 LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全属性指针
 BOOL bInitialOwner, // 初始拥有者
 LPCTSTR lpName // 互斥对象名
);


  参数bInitialOwner主要用来控制互斥对象的初始状态。一般多将其设置为FALSE,以表明互斥对象在创建时并没有为任何线程所占有。如果在创建互斥对象时指定了对象名,那么可以在本进程其他地方或是在其他进程通过OpenMutex()函数得到此互斥对象的句柄。OpenMutex()函数原型为:

HANDLE OpenMutex(
 DWORD dwDesiredAccess, // 访问标志
 BOOL bInheritHandle, // 继承标志
 LPCTSTR lpName // 互斥对象名
);


  当目前对资源具有访问权的线程不再需要访问此资源而要离开时,必须通过ReleaseMutex()函数来释放其拥有的互斥对象,其函数原型为:

BOOL ReleaseMutex(HANDLE hMutex);


  其唯一的参数hMutex为待释放的互斥对象句柄。至于WaitForSingleObject()和WaitForMultipleObjects()等待函数在互斥对象保持线程同步中所起的作用与在其他内核对象中的作用是基本一致的,也是等待互斥内核对象的通知。但是这里需要特别指出的是:在互斥对象通知引起调用等待函数返回时,等待函数的返回值不再是通常的WAIT_OBJECT_0(对于WaitForSingleObject()函数)或是在WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1之间的一个值(对于WaitForMultipleObjects()函数),而是将返回一个WAIT_ABANDONED_0(对于WaitForSingleObject()函数)或是在WAIT_ABANDONED_0到WAIT_ABANDONED_0+nCount-1之间的一个值(对于WaitForMultipleObjects()函数)。以此来表明线程正在等待的互斥对象由另外一个线程所拥有,而此线程却在使用完共享资源前就已经终止。除此之外,使用互斥对象的方法在等待线程的可调度性上同使用其他几种内核对象的方法也有所不同,其他内核对象在没有得到通知时,受调用等待函数的作用,线程将会挂起,同时失去可调度性,而使用互斥的方法却可以在等待的同时仍具有可调度性,这也正是互斥对象所能完成的非常规操作之一。
  在编写程序时,互斥对象多用在对那些为多个线程所访问的内存块的保护上,可以确保任何线程在处理此内存块时都对其拥有可靠的独占访问权。下面给出的示例代码即通过互斥内核对象hMutex对共享内存快g_cArray[]进行线程的独占访问保护。下面给出实现代码清单:

// 互斥对象
HANDLE hMutex = NULL;
char g_cArray[10];
UINT ThreadProc18(LPVOID pParam)
{
 // 等待互斥对象通知
 WaitForSingleObject(hMutex, INFINITE);
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 释放互斥对象
 ReleaseMutex(hMutex);
 return 0;
}
UINT ThreadProc19(LPVOID pParam)
{
 // 等待互斥对象通知
 WaitForSingleObject(hMutex, INFINITE);
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 释放互斥对象
 ReleaseMutex(hMutex);
 return 0;
}
……
void CSample08View::OnMutex()
{
 // 创建互斥对象
 hMutex = CreateMutex(NULL, FALSE, NULL);
 // 启动线程
 AfxBeginThread(ThreadProc18, NULL);
 AfxBeginThread(ThreadProc19, NULL);
 // 等待计算完毕
 Sleep(300);
 // 报告计算结果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}


  互斥对象在MFC中通过CMutex类进行表述。使用CMutex类的方法非常简单,在构造CMutex类对象的同时可以指明待查询的互斥对象的名字,在构造函数返回后即可访问此互斥变量。CMutex类也是只含有构造函数这唯一的成员函数,当完成对互斥对象保护资源的访问后,可通过调用从父类CSyncObject继承的UnLock()函数完成对互斥对象的释放。CMutex类构造函数原型为:

CMutex( BOOL bInitiallyOwn = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );


  该类的适用范围和实现原理与API方式创建的互斥内核对象是完全类似的,但要简洁的多,下面给出就是对前面的示例代码经CMutex类改写后的程序实现清单:

// MFC互斥类对象
CMutex g_clsMutex(FALSE, NULL);
UINT ThreadProc27(LPVOID pParam)
{
 // 等待互斥对象通知
 g_clsMutex.Lock();
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 释放互斥对象
 g_clsMutex.Unlock();
 return 0;
}
UINT ThreadProc28(LPVOID pParam)
{
 // 等待互斥对象通知
 g_clsMutex.Lock();
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 释放互斥对象
 g_clsMutex.Unlock();
 return 0;
}
……
void CSample08View::OnMutexMfc()
{
 // 启动线程
 AfxBeginThread(ThreadProc27, NULL);
 AfxBeginThread(ThreadProc28, NULL);
 // 等待计算完毕
 Sleep(300);
 // 报告计算结果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}


  小结
  线程的使用使程序处理更够更加灵活,而这种灵活同样也会带来各种不确定性的可能。尤其是在多个线程对同一公共变量进行访问时。虽然未使用线程同步的程序代码在逻辑上或许没有什么问题,但为了确保程序的正确、可靠运行,必须在适当的场合采取线程同步措施

 

 

线程互斥
本节介绍如下内容
1       主动对象
2       调度与原子操作
3       竞争条件和数据一致性
4       为何需要互斥
5       互斥类接口定义
6       示例程序
7       互斥类的Unix和Windows实现

主动对象(Active Object)
第二节介绍Thread类的时候曾经提到,每个子线程一旦被创建,就被赋予了自己的生命。当一个线程类创建并启动之后,它将会以自己的步调主动执行其独立线程,它和其他线程(包括主线程)的执行是并行关系。

为了详细说明这个问题,先介绍一下什么是控制流(control flow)。计算机科学中,控制流指指令式(imperative)或函数式(functional)编程语言中语句、指令、函数调用的执行(或求值)序列。

单线程程序中,控制流始终是一线串珠,一旦控制流达到某个对象,由程序主动调用对象函数,在函数执行完毕后,控制流返回主程序继续执行。对于被访问对象来说,访问、修改、成员函数调用都是一个被动的过程,此过程受控于该程序的控制流。所以,称单线程程序的对象为被动对象(Passive Object)。

与被动对象相对应的是主动对象。主动对象和被动对象的区别在于,主动对象可以发起自己的线程,创建新的执行序列,产生独立于主线程的控制流分支。简而言之,主动对象可以独立于主线程,自主制定自己的控制流。如果愿意,你可以实现一个和主线程没有任何协调关系,天马行空式的独立线程。当然,这样的线程往往也意味着毫无用处(娱乐作用除外)。

调度和原子操作
从理论模型上说,多线程环境中,线程之间是独立、对等关系。一个线程完全可以无视其他线程的存在。但实际多线程执行环境中,线程数往往远大于CPU数量。为了共享和分配资源,线程必需遵守一定规则,进行必要的协调。操作系统中具体的规则执行者是调度程序。因此,要想掌握多线程编程,就必需了解线程调度的基本概念和特点。在此基础上,才能了解为什么需要在线程之间进行协调,进而才能透彻理解如何协调多线程的并发执行。

现代操作系统中,存在许多不同的线程调度模型,这些调度模型的基本共同点就是线程执行顺序具有随机性和不确定性。调度模型既无法事先知道某一时刻会存在多少线程,也无法知道哪个线程会正在运行,甚至也不知道某个线程确切会到什么时刻执行结束,更不用说预先安排在特定时刻执行特定线程的特定语句。

前面提到,控制流就是语句,指令或函数调用的顺序序列。反映到时间轴上,就是一系列离散分布的执行序列。线程调度作用于多个控制流的客观效果,就是多个控制流按同一条时间轴排列时,是一个近似随机的执行序列;按每个控制流各自的时间轴排列时,则是一个具有先后顺序的执行序列。

在具体的程序执行上,这个特点就表现为线程A的一个语句执行完毕后,紧接着执行的可能是另一个线程B的一个语句。甚至可能是线程A的一个语句还没有执行完毕,就接着执行线程B的语句。

对于后一点不用感到奇怪。因为操作系统中,调度程序作为系统底层实现的一部分,参与调度的操作指令可能比高级编程语言的基本语句要底层的多。同一个调度程序,其调度的众多线程可能由多种高级语言编写,所以调度程序基本上不可能以某种高级编程语言的单条语句为单位安排执行序列。

通常,一条高级语言语句可能对应多条汇编指令,而一条汇编指令可能对应多条CPU微码指令。而一条微码指令,则可能对应逻辑电路的一个具体电路逻辑。顺便提一下,这也是Verilog, VHDL等高级语言能够综合出具体的逻辑电路的基本原理。所不同的是,高级语言编译器编译的最终单位是汇编,而硬件描述语言综合的最终单位是和微码对应的电路器件单元(集成电路前端设计的内容。记得以前暑期实习,我窝在学校做的就是这么一个元件库,做的很不像样子居然还得到了一定的肯定,多年后想起来还是汗颜)。

至于系统调度程序具体会定义怎样的原子操作集合,会以什么粒度的指令为调度基本单位,这就是系统设计者各显神通的地方了。个人认为,作为高级语言的编程者,记住什么操作是原子操作意义并不明显。大多数场合,只要认为,多线程的控制
流在同一条时间轴上看来是完全随机的就可以了。要记住,墨菲定律生效的时候,看似不可能的事情都能成为现实。

多线程程序设计中,一种朴素的设计思想是将线程具体化为主动对象,主动对象之间通过共享环境(全局资源)来维护当前应用的运行状态;主动对象间通过一套约定的互斥和同步规则来实现通信;线程的具体执行时序则由调度程序安排。主动对象之间,应尽量避免一个主动对象直接调用另一个主动对象的内部方法(例如suspend, stop, exit)直接控制另一个线程的执行步调。主动对象的行为应该完全由其本身控制,和其他主动对象的交互尽量只通过环境以及同步语句来实现。

实际实现中,如果多个主动对象都可以直接调用某个主动对象的stop方法终止其运行,这个程序是非常脆弱的,因为在调度程序之外,不借助同步机制,主观假定线程执行时序,人为更改线程执行状态是非常不明智的。即使这样的程序可用,对于维护者来说其难度也不亚于维护goto语句。

这也就是前一篇文章所定义线程类中detach, start, stop没有加锁的原因,因为并不希望在多个线程中调用同一个线程对象的这些方法。

竞争条件和数据一致性
共享环境是进程全局资源的同义词。在多线程并发执行中,最常遇到的问题就是共享环境被污染。具体体现就是全局数据被破坏,全局文件内容被破坏 … 。

例如:
有一个64位全局变量long globalVar = 0;主动对象A想把它置为0x000A000B000C000D;假设这个操作分两步执行,先将高32位置为000A000B,再把低32位置为000C000D。但不巧的是,对象A刚刚将高位置位,调度程序安排另一主动对象B执行。这时,全局变量globalVar内部的值是一个非法值,它是无意义的。在B拿着这个值做进一步处理时,得出的也是错误结果。这时,称为数据的一致性被破坏。

线程调度的不确定性也可能导致程序执行结果的不确定性。有时候这种不确定性会导致程序得到错误的运行结果。
例如:
为了对Thread类所生成的对象总数计数,定义一个全局变量Unsigned int counter = 0; 在Thread类的构造函数中,执行++counter。现在假设有2个Thread对象objA和objB并发运行,考虑如下两个场景:
Scenario1.操作序列如下:
1.      counter = 0;
2.      objA将counter值0从内存读入寄存器A;
3.      objA将寄存器A值加1;
4.      objA将寄存器A值写回内存,counter值为1;
5.      objB将counter值1从内存读入寄存器B;
6.      objB将寄存器B值加1;
7.      objA将寄存器B值写回内存,counter值为2;
8.      最终counter值为2。
Scenario2.操作序列如下:
1.      counter = 0;
2.      objA将counter值0从内存读入寄存器A;
3.      objB将counter值0从内存读入寄存器B
4.      objA将寄存器A值加1;
5.      objB将寄存器B值加1;
6.      objA将寄存器A值写回内存,counter值为1;
7.      objA将寄存器B值写回内存,counter值为1;
8.      最终counter值为1。
场景1的结果是设计的本意,场景2的结果对我们而言是错误的。

一个线程的执行结果正确性取决于于其他线程执行时序的条件,称为竞争条件。中文这样翻译不伦不类,但从英文字面理解非常容易。Race一般是计时赛,某位选手跑得快一点,竞赛结果就有变化,最终结果由race condition决定,还是非常形象的。

为何需要互斥
线程间协作问题,通常可以归为互斥和同步两类。其中互斥又主要解决两类问题:
维护数据一致性、避免竞争条件的出现。

解决一致性问题,通俗说就是,修改数据的线程通告其他线程“我正在修改你要访问的对象X,操作过程中不能保证这个数据的有效性,请不要使用此对象”。

避免竞争条件,通俗说就是,某线程通告其他线程“我将要进行涉及某对象的一系列操作A,在我未完成这一系列操作之前,如果有人和我同时执行涉及此对象的操作序列B(B也可能就是A),将会影响我执行结果的正确性,请不要进行涉及此对
象的操作”。

这种操作序列A有时候也被称为“原子性操作”,因为它不允许操作序列B在时间轴上和它交叉分布,必需保证在时间轴上看来,操作序列A是一个不可分割的整体。
(物理学早期,原子也被认为是不可分割的)。

以上冗长的解释,精简成一句话,就是“线程间需要互斥执行”。需要互斥的操作对应的代码也有一个很学术的名称-“关键域(或关键区)”。

那么如何实现互斥呢?一个简单的思路就是,设立一个权威仲裁者,给那些需要互斥执行的线程颁发一个共用的通行证。某个线程想要进入一个关键域执行,需要先申请可以进入该关键域的通行证,如果别的线程已经拿走了该通行证,则本线程等待,进入休眠状态,退出调度。如果本线程的通行证使用完毕,则应该将它归还给仲裁者,重新唤醒等待线程,参与调度,竞争此通行证。

比如,下列伪码中,threadFuncA和threadFuncB就需要申请同一张通行证:
例一:
int globalCounter = 0;

void threadFuncA (通行证类型 * 通行证)
{
  获取通行证;
  globalCounter++;
  归还通行证;
}

Void threadFuncB (通行证类型 * 通行证)
{
  获取通行证;
  globalCounter *= 2;
  归还通行证;
}

又比如,下列伪码中,需要为ResourceClass类的对象引用计数器制定一张通行证

例二:
class ResourceClass
{
public:
  resource & reference()
  {
    获取通行证;
++refcounter;
printf(“当前对象被引用了%u次”, refCounter);
释放通行证;
  }
private:
  通行证类型 通行证;
  unsigned int refCounter;
};

ResourceClass rescObj
Void threadFuncA()
{
  rescObj-> reference();
}

Void threadFuncB()
{
  rescObj-> reference();
}

最后一个例子,是为ResourceClass类的对象计数器制定一张通行证。
例三:
class ResourceClass
{
public:
  ResourceClass ()
  {
    获取通行证;
++objcounter;
printf(“当前类创建了%u个对象”, objCounter);
释放通行证;
  }
private:
  static通行证类型 通行证;
  unsigned int objCounter;
};

Void threadFuncA()
{
  ResourceClass * rescObj = new ResourceClass ();
}

Void threadFuncB()
{
  ResourceClass * rescObj = new ResourceClass ();
}
这三个例子中,例一是不同函数之间互斥,所以通行证的作用域要对两个函数都可见。
例二是同一个对象的内部函数多次调用之间的互斥,所以只要保证该函数多次调用时共用的都是当前对象内部同一份通行证即可。例三是同一个类的所有对象在创建时都要互斥,所以必需保证这个类的所有对象构造时共用的时同一份通行证,从而通行证被声明为静态成员。

这里所说的“通行证”在多线程编程中对应一个专门的术语mutex,由“mutualexclusion”拼接而来。为什么不直接用“锁”的概念呢?因为“锁”并不能很好的表达互斥的含义。锁是指一定条件下不允许当前代码段执行的概念。如上述例二或例三,不允许多个线程同时执行同一函数,这时说这个函数被锁定是很形象。但在例一中,A函数被锁定,为什么B函数不能执行呢?这就较难理解了。

而且经常有人感到疑惑,为什么“加锁后,被锁定的关键域只能串行执行”。这个其实是指在各自的时间轴上,并行的控制流在经过互斥执行的代码段时,必需以先后顺序串行执行。在今后的介绍中,mutex的申请,用acquire()操作表示,mutex的归还,用release()表示。舍弃lock(), unlock()的表示。

为了深入理解,先来看一段使用忙等待实现互斥的代码,用的是系统内核中使用较
多的“spin lock”互斥方法。

例4.忙等待实现互斥
//声明为volatile,防止被编译器优化。
volatile bool dataProcessNotDone = true;
int criticalData = 0;

unsigned threadFuncA( void* para )
{
   //如果编译器不支持volatile关键字,
//打开优化选项时,此句可能直接变成死循环。
   while (dataProcessNotDone);   // spin lock,锁定的是后续数据

   //被锁定的代码区
   printf(“critical data is %d\n”, CriticalData);
   return 0;
}

unsigned threadFuncB( void* para )
{
   sleep(1000);
   criticalData++;
   dataProcessNotDone = false; //修改互斥变量
   return 0;
}
在高级语言中,利用spin lock实现复杂互斥条件非常困难,单单处理竞争条件就令人望而生畏。Spin lock在每次等待解锁的时间都很短时,具有无需线程切换,无需再调度等独特优势。但是在绝大多数应用中,由于互斥等待时间不确定(可能很长),多个线程等待spin lock解锁的过程中,spinning的行为可能导致系统处于半瘫痪状态,会严重影响程序性能。

除了忙等待之外,很多操作系统或线程库都提供了互斥原语来实现互斥。如果有可能,应当尽量使用系统提供的接口实现互斥。否则,要考虑编译器优化中的常量代入,语句执行顺序重排,cpu指令序列优化等依赖于具体软硬件环境的复杂问题(关键是这样的付出没有太大意义)。

下面根据上述概念,抽象出互斥类的概念,定义如下Mutex类接口互斥类接口定义
文件mutex.h
#ifndef __MUTEX_H__
#define __MUTEX_H__
// C++标准中以_或__开头的名字不符合标准,
// 这一特点可以让这样定义的宏不会错误覆盖其他名字。

class Mutex
{
public:
  Mutex();
  ~Mutex();
  bool acquire (bool block = true);
  void release();
private:
  //依赖于具体实现,后面再说。
};
#endif
其中,
Mutex::acquire(),获取互斥量,有阻塞和非阻塞两种调用方式,阻塞方式,获取互斥量失败时线程休眠。非阻塞方式下,获取失败时直接返回。
Mutex::release(),释放互斥量。

示例程序
下面的例子说明了如何实现多线程环境下的Singleton模式。
文件Singleton.h:
#ifndef __SINGLETON_H__
#define __SINGLETON_H__

#include <stdio.h>
#include “thread.h”
#include “mutex.h”

// Dummy class.
class Helper {};

// A wrapper class for Mutex class
// It is exception safe.
class Guard
{
public:
  Guard(Mutex & lock):mutex(lock)
  {
    mutex.acquire();
  }

  ~Guard()
  {
    mutex.release();
  }

private:
  Mutex & mutex;
};

// Correct but possibly expensive multithreaded version
class Singleton1
{
public:
  Helper * getInstance() {
    Guard guard(mutex);
    if (helper == NULL)
    {
      helper = new Helper();
    }
    return helper;
  }

private:
  static Mutex mutex;
  static Helper * helper;
};

// Broken multithreaded version
// “Double-Checked Locking” idiom
class Singleton2
{
public:
  Helper * getInstance() {
    
    if (helper == NULL)
    {
      Guard guard(mutex);
      if (helper == NULL)
      {
        helper = new Helper();
      }
    }
    
    return helper;
  }

private:
  static Mutex mutex;
  static Helper * helper;
};

//Thread class for test.
template <typename T>
class TestThread: public Thread
{
public:
  TestThread<typename T>(T & resource,  Helper *& res, Helper *&
res2Cmp)
    :singleton(resource), instance(res), instance2Cmp(res2Cmp) {}

protected:
  void * run (void *)
  {
    for (int i=0; i<100000; i++)
    {
      instance = singleton.getInstance();
      if (instance != instance2Cmp  
          && instance != NULL
          &&instance2Cmp != NULL
         )
      {
        printf(“Fail! %p <> %p.\n”, instance, instance2Cmp);
      }
    }
    return NULL;
  }
private:
  T & singleton;
   Helper * & instance;
   Helper * & instance2Cmp;
};

#endif

文件main.cpp
#include <stdio.h>
#include “singleton.h”

#define SINGLETON Singleton1

Mutex SINGLETON::mutex;
Helper * SINGLETON::helper = NULL;

int main(int argc, char** argv)
{
  Helper * instance1= NULL;
  Helper * instance2 = NULL;
  SINGLETON singleton;

  TestThread<SINGLETON> thread1(singleton, instance1, instance2);
  TestThread<SINGLETON> thread2(singleton, instance2, instance1);
  thread1.start();
  thread2.start();
  thread1.wait();
  thread2.wait();
  printf(“Finished!\n”);
  return 0;
}

对此示例程序,说明如下几点。
1.定义了一个新的Guard类,这样做的好处是做到异常安全。比如:
try
{
  Mutex mutex;
  mutex.acquire();
  
// Some operations
  if (errorHappened)
    throw Exception();
  
mutex.release();
}
catch (Exception & e)
{
  // Print error message;
}
这段代码中,抛出异常时,互斥量不能释放,容易造成死锁。使用Guard类重写,
可以实现异常安全:
try
{
  Mutex mutex;
  Guard guard(mutex);
  // Some operations
  if (errorHappened)
    throw Exception();
}
catch (Exception & e)
{
  // Print error message;
}

2.Singleton1的实现可以确保多线程安全。但它是一个低效实现。假如有100次访问,只有1次会修改instance指针,其余99次都是只读操作。但是每次访问都需要进行互斥量的获取和释放操作。

取决于系统实现方式,互斥操作可能比整型变量的++操作慢一个数量级。有的实现提供的mutex其实是进程级别的互斥,一次互斥操作,会进入内核态,然后再返回用户态。而有的线程库提供的是Process local mutex,要稍快一些。但无论那种实现,代价都较大。

因此,为了改进Singleton1的实现效率,”Double-Checked Locking” idiom被提了出来。其思路是如果instance指针为空再进入互斥操作。由于获取互斥量过程中,可能别的线程已经将instance指针赋值,所以需要在获得互斥量所有权之后,再次检查instance指针值。这就是所谓”double check”中double的来历。

Double check的设计很聪明,但可惜无论在C++中还是在Java中,这么做其实都不能保证线程安全。考虑如下序列:
Step1. 线程A检查instance指针,发现其为空。获取互斥量成功,运行至语句
helper = new Helper();
在打开优化选项时,这个语句在优化后可能变成2步子操作, 而且编译器自动调整了原语句的执行顺序(reordering):
1)      分配内存,将地址赋值给helper变量。此时helper值非空。
2)      开始初始化此内存。
在运行完子语句1后,线程发生切换,此时内存尚未初始化。
Step2. 线程B检查instance指针,发现其非空。对instance所指对象进行进一步操作,由于此时对象是初始化还未完成无效数据,程序崩溃。

那么如何实现安全的double check呢?vc2005以后的版本,以及java 1.6以后的版本中,可以通过为helper加上volatile限定符,防止编译优化时调整指令执行顺序。最新的g++对volatile如何处理,没有查到相关资料。不过在C++中,只要本地汇编支持memoryBarrier指令,也可以通过在C++代码中内嵌汇编指令实现线程安全。
在此不再详细讨论。

除此之外,instance类型是否是基本类型,是否多核环境,都对不安全的doublecheck版本运行结果有微妙影响。

3.无论编译器如何实现,无论硬件环境如何,即使它最慢,也应该尽量使用系统提供的互斥原语。只有它是能够确保安全的。通过系统接口实现互斥,可以避免考虑编译优化等复杂情况。一种观点说volatile可以确保上述double check有效,但是intel有技术人员专门从硬件的角度批驳了这个说法,他告诉大家,即使编译器不做这个reordering, 处理器也可能在指令级别做。唯一能确保安全的,还是由系统实现的互斥接口。(照此说法,MS 和 Intel结成wintel联盟还是必要的,呵呵)双方说法似乎都一样权威时,作为程序开发者,很难取舍,因此在此类问题上还是应该适当保守。

4.Mutex, volatile变量,普通变量在使用中的具体效率对比,是一个非常复杂的问题。涉及到内存,缓存,寄存器之间各种情况的同步问题。不过大家可以针对一些简单的例子,测试一下执行时间上的差异。

Unix实现
下面是借助pthread的Mutex类实现。
文件Mutex.h
#ifndef __MUTEX_H__
#define __MUTEX_H__

#include <pthread.h>

class Mutex
{
public:
  Mutex();
  virtual ~Mutex();
  virtual bool acquire (bool block = true);
  virtual void release();
private:
  pthread_mutex_t handle;
};
#endif

文件 Mutex.cpp
#include “mutex.h”

Mutex::Mutex()
{
  pthread_mutex_init(&handle, NULL);
}

Mutex::~Mutex()
{
  pthread_mutex_destroy(&handle);
}

bool Mutex::acquire(bool block)
{
  if (block)
  {
     return pthread_mutex_lock(&handle) == 0;
  }
  else
  {
    return pthread_mutex_trylock(&handle) == 0;
  }
}

void Mutex::release()
{
  ReleaseMutex(handle);
}

Windows实现
文件mutex.h
#ifndef __MUTEX_H__
#define __MUTEX_H__

#include <windows.h>

class Mutex
{
public:
  Mutex();
  virtual ~Mutex();
  virtual bool acquire (bool block = true);
  virtual void release();
private:
  HANDLE handle;
};
#endif

文件mutex.cpp
#include “mutex.h”

Mutex::Mutex()
{
  handle = CreateMutex(NULL, false, NULL);
}

Mutex::~Mutex()
{
  CloseHandle(handle);
}

bool Mutex::acquire(bool block)
{
  //Use caution when calling the wait functions
  //and code that directly or indirectly creates windows.
  return WaitForSingleObject(handle, block ? INFINITE : 0) ==
WAIT_OBJECT_0;
}

void Mutex::release()
{
  ReleaseMutex(handle);
}

小结
本节从控制流的角度进一步介绍了什么是多线程执行中的并发,在此基础上介绍了主动对象的概念。在多线程编程中,需要考虑线程协作的地方,如果执行顺序理不清,为每一个线程画一条时间轴,标出各自时序,对分析问题往往能有帮助。

本节也介绍了多线程设计的一个基本思想,就是主动对象要具有一定的独立性,和其他线程的交互尽量只通过进程环境、系统或线程库提供的同步原语实现。

为什么要互斥,这个基本问题不能透彻理解的话,锁的概念很容易把自己弄糊涂。
互斥的粒度大小也就根本无法谈起。

互斥的效率问题,在实际设计中是一个值得考虑的问题。但是程序的正确性问题是一个更重要的问题。不能保证正确性,就不要用。保证正确性到实现高效性的过程,类似于学会走路到能够飞奔的过程。对于初学者来说,欲速则不达。

为了对互斥量的使用,“通行证”所有权的转换,以及不同系统中Mutex的实现效率等有一个充分的感性认识,大家请多动手实现才能真正有所收益,最终超越我本人的一己知见。

早期的计算机系统都只允许一个程序独占系统资源,一次只能执行一个程序。在大型机年代,计算能力是一种宝贵资源。对于资源拥有方来说,最好的生财之道自然是将同一资源同时租售给尽可能多的用户。最理想的情况是垄断全球计算市场。所以不难理解为何当年IBM预测“全球只要有4台计算机就够了”。

这种背景下,一个计算机能够支持多个程序并发执行的需求变得十分迫切。由此产生了进程的概念。进程在多数早期多任务操作系统中是执行工作的基本单元。进程是包含程序指令和相关资源的集合。每个进程和其他进程一起参与调度,竞争CPU,内存等系统资源。每次进程切换,都存在进程资源的保存和恢复动作,这称为上下文切换。

进程的引入可以解决支持多用户的问题,但是多进程系统也在如下方面产生了新的问题:
?       进程频繁切换引起的额外开销可能会严重影响系统性能。
?       进程间通信要求复杂的系统级实现。

在程序功能日趋复杂的情况下,上述缺陷也就凸现出来。比如,一个简单的GUI程序,为了有更好的交互性,通常用一个任务支持界面交互,另一个任务支持后台运算。如果每个任务均由一个进程来实现,那会相当低效。对每个进程来说,系统资源看上去都是其独占的。比如内存空间,每个进程认为自己的内存空间是独有的。一次切换,这些独立资源都需要切换。

由此就演化出了利用分配给同一个进程的资源,尽量实现多个任务的方法。这也就引入了线程的概念。同一个进程内部的多个线程,共享的是同一个进程的所有资源。

比如,与每个进程独有自己的内存空间不同,同属一个进程的多个线程共享该进程的内存空间。例如在进程地址空间中有一个全局变量globalVar,若A线程将其赋值为1,则另一线程B可以看到该变量值为1。两个线程看到的全局变量globalVar是同一个变量。

通过线程可以支持同一个应用程序内部的并发,免去了进程频繁切换的开销,另外并发任务间通信也更简单。

目前多线程应用主要用于两大领域:网络应用和嵌入式应用。为什么在这两个领域应用较多呢?因为多线程应用能够解决两大问题:
?       并发。网络程序具有天生的并发性。比如网络数据库可能需要同时处理数以千计的请求。而由于网络连接的时延不确定性和不可靠性,一旦等待一次网络交互,可以让当前线程进入睡眠,退出调度,处理其他线程。这样就能够有效利用系统资源,充分发挥系统处理能力。
?       实时。线程的切换是轻量级的,所以可以保证足够快。每当有事件发生,状态改变,都能有线程及时响应,而且每次线程内部处理的计算强度和复杂度都不大。在这种情况下,多线程实现的模型也是高效的。

在有些语言中,对多线程或者并发的支持是直接内建在语言中的,比如Ada和VHDL。在C++里面,对多线程的支持由具体操作系统提供的函数接口支持。不同的系统中具体实现方法不同。后面所有例子只给出windows和Unix/Linux的实现。

在后面的实现中,考虑的是尽量封装隔离底层的多线程函数接口,屏蔽操作系统底层的线程实现具体细节,介绍的重点是多线程编程中较通用的概念。同时也尽量体现C++面向对象的一面。

最后,由于空闲时间有限,我只求示例代码能够明确表达自己的意思即可。至于代码的尽善尽美就只能有劳各位尽力以为之了。
?

第2节   线程的创建
本节介绍如下内容
?       线程状态
?       线程运行环境
?       线程类定义
?       示例程序
?       线程类的Windows和Unix实现
线程状态
在一个线程的生存期内,可以在多种状态之间转换。不同操作系统可以实现不同的线程模型,定义许多不同的线程状态,每个状态还可以包含多个子状态。但大体说来,如下几种状态是通用的:
?       就绪:参与调度,等待被执行。一旦被调度选中,立即开始执行。
?       运行:占用CPU,正在运行中。
?       休眠:暂不参与调度,等待特定事件发生。
?       中止:已经运行完毕,等待回收线程资源(要注意,这个很容易误解,后面解释)。
线程环境
线程存在于进程之中。进程内所有全局资源对于内部每个线程均是可见的。
进程内典型全局资源有如下几种:
?       代码区。这意味着当前进程空间内所有可见的函数代码,对于每个线程来说也是可见的。
?       静态存储区。全局变量。静态变量。
?       动态存储区。也就是堆空间。
线程内典型的局部资源有:
?       本地栈空间。存放本线程的函数调用栈,函数内部的局部变量等。
?       部分寄存器变量。例如本线程下一步要执行代码的指针偏移量。

一个进程发起之后,会首先生成一个缺省的线程,通常称这个线程为主线程。C/C++程序中主线程就是通过main函数进入的线程。由主线程衍生的线程称为从线程,从线程也可以有自己的入口函数,作用相当于主线程的main函数。

这个函数由用户指定。Pthread和winapi中都是通过传入函数指针实现。在指定线程入口函数时,也可以指定入口函数的参数。就像main函数有固定的格式要求一样,线程的入口函数一般也有固定的格式要求,参数通常都是void *类型,返回类型在pthread中是void *, winapi中是unsigned int,而且都需要是全局函数。

最常见的线程模型中,除主线程较为特殊之外,其他线程一旦被创建,相互之间就是对等关系 (peer to peer), 不存在隐含的层次关系。每个进程可以创建的最大线程数由具体实现决定。

为了更好的理解上述概念,下面通过具体代码来详细说明。
线程类接口定义
一个线程类无论具体执行什么任务,其基本的共性无非就是
?       创建并启动线程
?       停止线程
?       另外还有就是能睡,能等,能分离执行(有点拗口,后面再解释)。
?       还有其他的可以继续加…
将线程的概念加以抽象,可以为其定义如下的类:
文件 thread.h
#ifndef __THREAD__H_
#define __THREAD__H_
class Thread
{
public:
  Thread();
  virtual ~Thread();
  int start (void * = NULL);
  void stop();
  void sleep (int);
  void detach();
  void * wait();
protected:
  virtual void * run(void *) = 0;
private:
//这部分win和unix略有不同,先不定义,后面再分别实现。
//顺便提一下,我很不习惯写中文注释,这里为了更明白一
//点还是选用中文。
  …  
};
#endif

Thread::start()函数是线程启动函数,其输入参数是无类型指针。
Thread::stop()函数中止当前线程。
Thread::sleep()函数让当前线程休眠给定时间,单位为秒。
Thread::run()函数是用于实现线程类的线程函数调用。
Thread::detach()和thread::wait()函数涉及的概念略复杂一些。在稍后再做解释。

Thread类是一个虚基类,派生类可以重载自己的线程函数。下面是一个例子。

示例程序

代码写的都不够精致,暴力类型转换比较多,欢迎有闲阶级美化,谢过了先。
文件create.h
#ifndef __CREATOR__H_
#define __CREATOR__H_

#include <stdio.h>
#include “thread.h”

class Create: public Thread
{
protected:
  void * run(void * param)
  {
    char * msg = (char*) param;
    printf (“%s\n”, msg);
    //sleep(100); 可以试着取消这行注释,看看结果有什么不同。
    printf(“One day past.\n”);
    return NULL;
  }
};
#endif
然后,实现一个main函数,来看看具体效果:
文件Genesis.cpp
#include <stdio.h>
#include “create.h”

int main(int argc, char** argv)
{
  Create monday;
  Create tuesday;
  
  printf(“At the first God made the heaven and the earth.\n”);
  monday.start(“Naming the light, Day, and the dark, Night, the first day.”);
  tuesday.start(“Gave the arch the name of Heaven, the second day.”);
  printf(“These are the generations of the heaven and the earth.\n”);

  return 0;
}
编译运行,程序输出如下:
At the first God made the heaven and the earth.
These are the generations of the heaven and the earth.
令人惊奇的是,由周一和周二对象创建的子线程似乎并没有执行!这是为什么呢?别急,在最后的printf语句之前加上如下语句:
monday.wait();
tuesday.wait();
重新编译运行,新的输出如下:
At the first God made the heaven and the earth.
Naming the light, Day, and the dark, Night, the first day.
One day past.
Gave the arch the name of Heaven, the second day.
One day past.
These are the generations of the heaven and the earth.

为了说明这个问题,需要了解前面没有解释的Thread::detach()和Thread::wait()两个函数的含义。

无论在windows中,还是Posix中,主线程和子线程的默认关系是:
无论子线程执行完毕与否,一旦主线程执行完毕退出,所有子线程执行都会终止。这时整个进程结束或僵死(部分线程保持一种终止执行但还未销毁的状态,而进程必须在其所有线程销毁后销毁,这时进程处于僵死状态),在第一个例子的输出中,可以看到子线程还来不及执行完毕,主线程的main()函数就已经执行完毕,从而所有子线程终止。

需要强调的是,线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态(请回顾上面说的线程状态),但千万要记住的是,进入终止态后,为线程分配的系统资源并不一定已经释放,而且可能在系统重启之前,一直都不能释放。终止态的线程,仍旧作为一个线程实体存在与操作系统中。(这点在win和unix中是一致的。)而什么时候销毁线程,取决于线程属性。

通常,这种终止方式并非我们所期望的结果,而且一个潜在的问题是未执行完就终止的子线程,除了作为线程实体占用系统资源之外,其线程函数所拥有的资源(申请的动态内存,打开的文件,打开的网络端口等)也不一定能释放。所以,针对这个问题,主线程和子线程之间通常定义两种关系:
?       可会合(joinable)。这种关系下,主线程需要明确执行等待操作。在子线程结束后,主线程的等待操作执行完毕,子线程和主线程会合。这时主线程继续执行等待操作之后的下一步操作。主线程必须会合可会合的子线程,Thread类中,这个操作通过在主线程的线程函数内部调用子线程对象的wait()函数实现。这也就是上面加上三个wait()调用后显示正确的原因。必须强调的是,即使子线程能够在主线程之前执行完毕,进入终止态,也必需显示执行会合操作,否则,系统永远不会主动销毁线程,分配给该线程的系统资源(线程id或句柄,线程管理相关的系统资源)也永远不会释放。
?       相分离(detached)。顾名思义,这表示子线程无需和主线程会合,也就是相分离的。这种情况下,子线程一旦进入终止态,系统立即销毁线程,回收资源。无需在主线程内调用wait()实现会合。Thread类中,调用detach()使线程进入detached状态。这种方式常用在线程数较多的情况,有时让主线程逐个等待子线程结束,或者让主线程安排每个子线程结束的等待顺序,是很困难或者不可能的。所以在并发子线程较多的情况下,这种方式也会经常使用。
缺省情况下,创建的线程都是可会合的。可会合的线程可以通过调用detach()方法变成相分离的线程。但反向则不行。

UNIX实现

文件 thread.h
#ifndef __THREAD__H_
#define __THREAD__H_
class Thread
{
public:
  Thread();
  virtual ~Thread();
  int start (void * = NULL);
  void stop();
  void sleep (int);
  void detach();
  void * wait();
protected:
  virtual void * run(void *) = 0;
private:
  pthread_t handle;
  bool started;
  bool detached;
  void * threadFuncParam;
friend void * threadFunc(void *);
};

//pthread中线程函数必须是一个全局函数,为了解决这个问题
//将其声明为静态,以防止此文件之外的代码直接调用这个函数。
//此处实现采用了称为Virtual friend function idiom 的方法。
Static void * threadFunc(void *);
#endif

文件thread.cpp
#include <pthread.h>
#include <sys/time.h>
#include “thread.h”

static void * threadFunc (void * threadObject)
{
  Thread * thread = (Thread *) threadObject;
  return thread->run(thread->threadFuncParam);
}

Thread::Thread()
{
  started = detached = false;
}

Thread::~Thread()
{
  stop();
}

bool Thread::start(void * param)
{
  pthread_attr_t attributes;
  pthread_attr_init(&attributes);
  if (detached)
  {
    pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED);
  }

  threadFuncParam = param;

  if (pthread_create(&handle, &attributes, threadFunc, this) == 0)
  {
    started = true;
  }

  pthread_attr_destroy(&attribute);
}

void Thread::detach()
{
  if (started && !detached)
  {
    pthread_detach(handle);
  }
  detached = true;
}

void * Thread::wait()
{
  void * status = NULL;
  if (started && !detached)
  {
    pthread_join(handle, &status);
  }
  return status;
}

void Thread::stop()
{
  if (started && !detached)
  {
    pthread_cancel(handle);
    pthread_detach(handle);
    detached = true;
  }
}

void Thread::sleep(unsigned int milliSeconds)
{
  timeval timeout = { milliSeconds/1000, millisecond%1000};
  select(0, NULL, NULL, NULL, &timeout);
}

Windows实现

文件thread.h
#ifndef _THREAD_SPECIFICAL_H__
#define _THREAD_SPECIFICAL_H__

#include <windows.h>

static unsigned int __stdcall threadFunction(void *);

class Thread {
        friend unsigned int __stdcall threadFunction(void *);
public:
        Thread();
        virtual ~Thread();
        int start(void * = NULL);
        void * wait();
        void stop();
        void detach();
        static void sleep(unsigned int);

protected:
        virtual void * run(void *) = 0;

private:
        HANDLE threadHandle;
        bool started;
        bool detached;
        void * param;
        unsigned int threadID;
};

#endif

文件thread.cpp
#include “stdafx.h”
#include <process.h>
#include “thread.h”

unsigned int __stdcall threadFunction(void * object)
{
        Thread * thread = (Thread *) object;
        return  (unsigned int ) thread->run(thread->param);
}

Thread::Thread()
{
        started = false;
        detached = false;
}

Thread::~Thread()
{
        stop();
}

int Thread::start(void* pra)
{
        if (!started)
        {
                param = pra;
                if (threadHandle = (HANDLE)_beginthreadex(NULL, 0, threadFunction, this, 0, &threadID))
                {
                        if (detached)
                        {
                                CloseHandle(threadHandle);
                        }
                        started = true;
                }
        }
        return started;
}

//wait for current thread to end.
void * Thread::wait()
{
        DWORD status = (DWORD) NULL;
        if (started && !detached)
        {
                WaitForSingleObject(threadHandle, INFINITE);
                GetExitCodeThread(threadHandle, &status);      
                CloseHandle(threadHandle);
                detached = true;
        }

        return (void *)status;
}

void Thread::detach()
{
  if (started && !detached)
  {
    CloseHandle(threadHandle);
  }
  detached = true;
}

void Thread::stop()
{
        if (started && !detached)
        {
                TerminateThread(threadHandle, 0);

                //Closing a thread handle does not terminate
                //the associated thread.
                //To remove a thread object, you must terminate the thread,
                //then close all handles to the thread.
                //The thread object remains in the system until
                //the thread has terminated and all handles to it have been
                //closed through a call to CloseHandle
                CloseHandle(threadHandle);
                detached = true;
        }
}

void Thread::sleep(unsigned int delay)
{
        ::Sleep(delay);
}

小结

本节的主要目的是帮助入门者建立基本的线程概念,以此为基础,抽象出一个最小接口的通用线程类。在示例程序部分,初学者可以体会到并行和串行程序执行的差异。有兴趣的话,大家可以在现有线程类的基础上,做进一步的扩展和尝试。如果觉得对线程的概念需要进一步细化,大家可以进一步扩展和完善现有Thread类。

想更进一步了解的话,一个建议是,可以去看看其他语言,其他平台的线程库中,线程类抽象了哪些概念。比如Java, perl等跨平台语言中是如何定义的,微软从winapi到dotnet中是如何支持多线程的,其线程类是如何定义的。这样有助于更好的理解线程的模型和基础概念。

另外,也鼓励大家多动手写写代码,在此基础上尝试写一些代码,也会有助于更好的理解多线程程序的特点。比如,先开始的线程不一定先结束。线程的执行可能会交替进行。把printf替换为cout可能会有新的发现,等等。

每个子线程一旦被创建,就被赋予了自己的生命。管理不好的话,一只特例独行的猪是非常让人头痛的。

对于初学者而言,编写多线程程序可能会遇到很多令人手足无措的bug。往往还没到考虑效率,避免死锁等阶段就问题百出,而且很难理解和调试。这是非常正常的,请不要气馁,后续文章会尽量解释各种常见问题的原因,引导大家避免常见错误。目前能想到入门阶段常遇到的问题是:
?       内存泄漏,系统资源泄漏。
?       程序执行结果混乱,但是在某些点插入sleep语句后结果又正确了。
?       程序crash, 但移除或添加部分无关语句后,整个程序正常运行(假相)。
?       多线程程序执行结果完全不合逻辑,出于预期。

同步(Synchronization)与异步(Asynchronization)
首先对同步与异步的概念做一个简单的说明。
当程序1调用程序2时,程序1停下不动,直到程序2完成回到程序1来,程序1才继续下去,这就是所谓的同步。如果程序1调用程序2后,径自继续自己的下一个动作,那么两者之间 就是所谓的异步。
举个例子,在WIN32 API中,SendMessage()就是同步行为,而PostMessage()就是异步行为。在Windows系统中,PostMessage()是把消息放到对方的消息队列中,然后回到原调用点继续执行,所以这就是异步(asynchronous)行为。而SendMessage()根本就像是“直接调用窗口的窗口函数”,直到该窗口函数结束,然后才回到原调用点,所以它是同步(
synchronous)行为。

 
为何需要同步
撰写多线程程序的一个最具挑战性的问题就是:如何让一个线程和另一个线程合作。除非你让他们同心协力,否则必然会出现如第三节所说的竞争条件(race condition)和数据被破坏(data corruption)的情况。当多个线程共享同一内存区域的时候,我们需要确保每一个线程所看到的数据的一致性。假如对于每一个线程所使用的变量来说,其它任何线程都不会读取或使用该变量,那么根本不存在数据一致性的问题。同样地,对于一个有着只读属性的变量来说,多个线程同时读取它的值的话,也不会有数据一致性的问题存在。然而,当一个线程可以修改一个变量,同时其它线程也能够读取或修改该变量的话,我们就需要同步这些线程,以确保每一个线程在访问该变量的内存内容时所用到的值是有效的。
举个例子,假设有一块未初始化的内存块和两个线程,一个读线程,一个写线程。我们应该保证读线程在读取该内存块时,它已经被写线程初始化好了,否则读线程只能读到一块未初始化完成的无效数据。这就需要用到线程的同步机制(synchronous mechanism)。
线程间的协调工作是由同步机制来完成的。同步机制相当于线程之间的红绿灯。程序员可以设计让一组线程使用同一个红绿灯系统。这个红绿灯系统负责给某个线程绿灯而给其他线程红灯。这一组红绿灯系统必须确保每一个线程都有机会获得绿灯。
有好多种同步机制可以运用。使用哪一种完全视欲解决的问题而定。这些同步机制常常以各种方式组合在一起,以产生出更精密的机制。
什么是事件对象(Event Object)
事件对象(Event Object)是一种最具弹性的同步机制,它的唯一目的就是成为激发(Signaled)状态或未激发(Unsignaled)状态。这两种状态完全由程序控制。
我们通过上面介绍的读写线程的例子来说明事件对象的激发状态和未激发状态的含义。读线程和写线程拥有同一个事件对象。该事件对象的初始状态为非激发状态。当读线程需要读共享的那块内存时,它需要判断该事件对象的状态。如果该事件对象处于非激发状态,则读线程等待,直到该事件对象处于激发状态为止。写线程会在那块共享的内存被初始化好之后将该事件对象的状态设为激发状态。这时读线程得知了该事件对象的状态已经由非激发状态变为激发状态,于是它开始读取那块共享的内存,并执行后续的操作。
事件对象之所以有大用途,正是因为它们的状态完全在程序员的掌控之下。因此,程序员可以精确的告诉一个事件对象做什么事,以及什么时候去做。
事件对象可以分为自动重置的事件对象(Automatic-Reset Event Object)和手动重置的事件对象(Manual-Reset Event Object)。自动重置的事件对象会在事件对象变成激发状态(因而唤醒一个线程)之后,自动重置为非激发状态。而手动重置的事件对象,不会自动重置,必须靠程序操作才能将激发状态的事件对象重置为非激发状态。
事件对象所能完成的一切功能都可以通过互斥来完成。下面我们通过比较使用事件对象来实现读写线程的例子和使用互斥来实现读写线程的例子,以说明事件对象的作用和它存在的必要性。
例一:使用事件对象来实现读写线程
void threadRead(事件对象类型 *事件对象)
{
     阻塞事件对象;
     读取共享内存的数据;
}
void threadWrite(事件对象类型 *事件对象)
{
     将适当的数据写入共享内存;
     激发事件对象;
}
例二:使用互斥来实现读写线程
bool globalIsWritten = false;
void threadRead(通行证类型 *通行证)
{
     获取通行证;
     while (!globalIsWritten)
     {
         归还通行证;
         sleep(sometime);
         获取通行证;
     }
     归还通行证;
     读取共享内存的数据;
}
void threadWrite(通行证类型 *通行证)
{
     将适当的数据写入共享内存;
     获取通行证;
     globalIsWritten = true;
     归还通行证;
}
很明显,使用事件对象来实现读写线程的代码要比使用互斥来实现读写线程的代码优雅许多。使用事件对象来实现读写线程的代码显得更加干净整洁,而且可读性更高。使用互斥来实现读写线程时,在读线程中,需要轮询地互斥访问读写线程间的共享变量
globalIsWritten,因此其效率一定不如使用事件对象来实现读写线程的效率高。我将后面的“手动重置的事件对象”的示例程序改为完全使用互斥来实现后,发现其运行时间是使用事件对象来实现的1.21倍。这个测试结果和我们的预期相一致。因此,对于类似于读写线程这样的例子,事件对象相对于互斥提供了更加优雅和高效的解决方案。
事件对象类接口定义
文件event.h
#ifndef __EVENT_H__
#define __EVENT_H__
#include <windows.h>
class Event
{
public:
     Event(bool bManualUnsignal, bool bSignaled);
     virtual ~Event();
     virtual bool block();
     virtual bool signal();
     virtual bool unsignal();
private:
     // 依赖于具体实现,后面再说。
};
#endif
其中,
Event::Event(bool bManualUnsignal, bool bSignaled),事件对象类的构造函数。
bManualUnsignal用于指定事件对象的类型。如果其值为true,则该事件对象是手动重置的事件对象;如果其值为false,则该事件对象是自动重置的事件对象。bSignaled用于指定事件对象的初始状态。如果其值为true,则该事件对象的初始状态为激发状态;如果其值为false,则该事件对象的初始状态为非激发状态。
Event::~Event(),事件对象类的析构函数。用于摧毁事件对象。
Event::block(),根据事件对象的状态,对拥有该事件对象的线程进行控制。如果事件对象处于非激发状态,则拥有该事件对象的线程开始等待,直到该事件对象的状态变为激发状态。如果事件对象处于激发状态或者当事件对象的状态由非激发状态变为激发状态的时候,首先判断该事件对象是那种类型的,如果该事件对象是自动重置的,那么需要将该事件对象的状态设为非激发状态,然后唤醒等待该事件对象的线程。
Event::signal(),将事件对象的状态设为激发状态。如果事件对象是手动重置的事件对象,那么该事件对象会一直保持激发状态,直到Event::unsignal()被调用,该事件对象才会由激发状态变为非激发状态。在手动设置的事件对象保持激发状态的时候,所有等待该事件对象的线程都将被唤醒。如果事件对象是自动重置的事件对象,那么该事件对象会一直保持激发状态,直到一个等待该事件对象的线程被唤醒,这时该事件对象会由激发状态变为非激发状态(由Event::block()来完成)。
Event::unsignal(),将事件对象的状态设为非激发状态。该方法主要用于手动重置的事件对象,它必须显式地调用该方法以使得自己的状态变为非激发状态。而对于自动重置的事件对象来说,当一个等待线程被唤醒时,它会自动地将自己的状态由激发状态变为非激发状态。
在Windows操作系统中,还有一种对事件对象的操作,叫做PulseEvent()。在我们的事件对象模型中并没有引入该接口,因为PulseEvent()是一个不稳定的操作。Windows只是为了向后兼容才保留了PulseEvent()。
下面对PulseEvent()函数做一个简单的介绍,并且说明为什么该操作不稳定。
如果一个事件对象是手动重置的,那么对该事件对象进行PulseEvent()操作后,该事件对象会被设为激发状态,所有的等待该事件对象的线程都会被唤醒,之后该事件对象恢复为非激发状态。如果一个事件对象是自动重置的,那么对该事件对象进行PulseEvent()操作后,该事件对象会被设为激发状态,一个等待该事件对象的线程会被唤醒,之后该事件对象恢复为非激发状态。
注意,如果没有任何线程在等待事件对象(不管是手动重置的还是自动重置的),或者没有任何线程可以立即被唤醒的话,对该事件对象进行PulseEvent()操作后,唯一的结果是该事件对象的状态被设置为非激发状态。在这种情况下,这个事件对象会被遗失。这时,可能会引起死锁。
举个例子,假设一个程序由两个线程(线程A和线程B)组成。线程A累加一个计数器,之后调用Event::block()等待一个事件对象。如果在这两个操作之间发生了上下文切换(context switch),线程B开始执行,它检查计数器内容然后对着同一个事件对象进行PulseEvent()操作。这时候这个要求苏醒的请求会被遗失掉。而线程A会因为它等待的事件对象永远不会被设置为激发状态而永远等待下去,程序进入死锁状态。这时,线程A被称作饥饿线程。
因此,PulseEvent()是一个不稳定的操作,在我们的事件对象模型中将不包括该操作。

 
示例程序
自动重置的事件对象
文件common.h
#ifndef __COMMON_H__
#define __COMMON_H__
struct Param
{
     long threadID;
     int *count;
};
const int TCOUNT = 10;
const int COUNT_LIMIT = 12;
#endif  
文件watchcount.h
#ifndef __WATCH_COUNT_H__
#define __WATCH_COUNT_H__
#include “thread.h”
class Event;
class Mutex;
class WatchCount : public Thread
{
public:
     WatchCount(Event& e, Mutex& m);
protected:
     void* run(void *param);
private:
     Event& event;
     Mutex& mutex;
};
#endif
文件watchcount.cpp
#include “watchcount.h”
#include “common.h”
#include “mutex.h”
#include “event.h”
#include <iostream>
using std::cout;
using std::endl;
WatchCount::WatchCount(Event& e, Mutex& m) : event(e), mutex(m)
{
}
void* WatchCount::run(void *param)
{
     Param *prm = static_cast<Param *>(param);
     long id = prm->threadID;
     int *count = prm->count;
     mutex.acquire();
     cout << “Starting WatchCount: thread ”
          << id
          << “.”
          << endl;
     cout << “WatchCount: thread ”
          << id
          << ” going into wait…”
          << endl;
     mutex.release();
     event.block();
     mutex.acquire();
     cout << “WatchCount: thread ”
          << id
          << ” Event signaled.”
          << endl;
     *count += 125;
     cout << “WatchCount: thread ”
          << id
          << ” count now = ”
          << *count
          << “.”
          << endl;
     mutex.release();
     return NULL;
}
文件inccount.h
#ifndef __INC_COUNT_H__
#define __INC_COUNT_H__
#include “thread.h”
class Event;
class Mutex;
class IncCount : public Thread
{
public:
     IncCount(Event& e, Mutex& m);
protected:
     void* run(void *param);
private:
     Event& event;
     Mutex& mutex;
};
#endif
文件inccount.cpp
#include “inccount.h”
#include “common.h”
#include “mutex.h”
#include “event.h”
#include <iostream>
using std::cout;
using std::endl;
IncCount::IncCount(Event& e, Mutex& m) : event(e), mutex(m)
{
}
void* IncCount::run(void *param)
{
     Param *prm = static_cast<Param *>(param);
     long id = prm->threadID;
     int *count = prm->count;
     for (int i = 0; i < TCOUNT; ++i)
     {
         mutex.acquire();
         ++(*count);
         /*
          * Check the value of count and signal waiting thread when condition  
is
          * reached.
          */
         if (*count == COUNT_LIMIT)
         {
             cout << “IntCount: thread ”
                  << id
                  << “, count = ”
                  << *count
                  << “  Threshold reached.  ”;
             event.signal();
             cout << “Just sent signal.”
                  << endl;
         }
         cout << “IncCount: thread ”
              << id
              << “, count = ”
              << *count
              << “, unlocking mutex.”
              << endl;
         mutex.release();
         /* Do some work so threads can alternate on mutex lock */
         sleep(1000);
     }
     return NULL;
}
文件mainautounsignal.cpp
#include “inccount.h”
#include “watchcount.h”
#include “common.h”
#include “mutex.h”
#include “event.h”
#include <iostream>
using std::cout;
using std::endl;
int main(int argc, char* argv[])
{
     Event event(false, false);
     Mutex mutex;
     int count = 0;
     Param prm1 = {1, &count};
     Param prm2 = {2, &count};
     Param prm3 = {3, &count};
     WatchCount wc(event, mutex);
     IncCount ic1(event, mutex);
     IncCount ic2(event, mutex);
     wc.start(&prm1);
     ic1.start(&prm2);
     ic2.start(&prm3);
     /* Wait for all thread to complete */
     wc.wait();
     ic1.wait();
     ic2.wait();
     cout << “Main(): Waited on 3 thread. Final value of count = ”
          << count
          << “. Done.”
          << endl;
     return 0;
}
在此示例程序中,主线程创造了三个线程。其中,两个线程(IncCount)对一个“count”变量执行递增操作,第三个线程(WatchCount)观察那个“count”变量的值。当“count”变量达到一个预定义的值(COUNT_LIMIT)时,等待线程(WatchCount)被两个递增线程(IncCount)中的一个唤醒。等待线程(WatchCount)被唤醒后会立即修改“count”变量的值。两个递增线程(IncCount)会一直执行,直到达到TCOUNT为止。最后,主线程会打印出“count”变量的最终值。
手动重置的事件对象
文件common.h
#ifndef __COMMON_H__
#define __COMMON_H__
#include <string>
using std::string;
struct Param
{
     long threadID;
     string *data;
};
#endif
文件readfrombuffer.h
#ifndef __READ_FROM_BUFFER_H__
#define __READ_FROM_BUFFER_H__
#include “thread.h”
class Event;
class Mutex;
class ReadFromBuffer : public Thread
{
public:
     ReadFromBuffer(Event& e, Mutex& m);
protected:
     void* run(void *param);
private:
     Event& event;
     Mutex& mutex;
};
#endif
文件readfrombuffer.cpp
#include “readfrombuffer.h”
#include “common.h”
#include “event.h”
#include “mutex.h”
#include <iostream>
using std::cout;
using std::endl;
ReadFromBuffer::ReadFromBuffer(Event& e, Mutex& m) : event(e), mutex(m)
{
}
void* ReadFromBuffer::run(void *param)
{
     Param *prm = static_cast<Param *>(param);
     long id = prm->threadID;
     string *data = prm->data;
     mutex.acquire();
     cout << “ReadFromBuffer: thread ”
          << id
          << ” waiting for event signaled…”
          << endl;
     mutex.release();
     event.block();
     mutex.acquire();
     cout << “ReadFromBuffer: thread ”
          << id
          << ” reading from buffer (”
          << *data
          << “)”
          << endl;
     mutex.release();
     return NULL;
}
文件writetobuffer.h
#ifndef __WRITE_TO_BUFFER__
#define __WRITE_TO_BUFFER__
#include “thread.h”
class Event;
class Mutex;
class WriteToBuffer : public Thread
{
public:
     WriteToBuffer(Event& e, Mutex& m);
protected:
     void* run(void *param);
private:
     Event& event;
     Mutex& mutex;
};
#endif
文件writetobuffer.cpp
#include “writetobuffer.h”
#include “common.h”
#include “event.h”
#include “mutex.h”
#include <iostream>
using std::cout;
using std::endl;
WriteToBuffer::WriteToBuffer(Event& e, Mutex& m) : event(e), mutex(m)
{
}
void* WriteToBuffer::run(void *param)
{
     Param *prm = static_cast<Param *>(param);
     long id = prm->threadID;
     string *data = prm->data;
     *data = “Hello World!”;
     mutex.acquire();
     cout << “WriteToBuffer: thread ”
          << id
          << ” writing to the shared buffer…”
          << endl;
     mutex.release();
     event.signal();
     return NULL;
}
文件mainmanualunsignal.cpp
#include “writetobuffer.h”
#include “readfrombuffer.h”
#include “common.h”
#include “event.h”
#include “mutex.h”
#include <iostream>
using std::cout;
using std::endl;
int main(int argc, char* argv[])
{
     Event event(true, false);
     Mutex mutex;
     string data;
     Param prm1 = {1, &data};
     Param prm2 = {2, &data};
     Param prm3 = {3, &data};
     Param prm4 = {4, &data};
     Param prm5 = {5, &data};
     ReadFromBuffer read1(event, mutex);
     ReadFromBuffer read2(event, mutex);
     ReadFromBuffer read3(event, mutex);
     ReadFromBuffer read4(event, mutex);
     WriteToBuffer write(event, mutex);
     read1.start(&prm1);
     read2.start(&prm2);
     read3.start(&prm3);
     read4.start(&prm4);
     write.start(&prm5);
     mutex.acquire();
     cout << “Main thread waiting for threads to exit…”
          << endl;
     mutex.release();
     read1.wait();
     read2.wait();
     read3.wait();
     read4.wait();
     write.wait();
     cout << “All threads ended, cleaning up for application exit…”
          << endl;
     return 0;
}
在此示例程序中,主线程创造了五个线程。其中,四个线程(ReadFromBuffer)读取“data”变量的内容,第五个线程(WriteToBuffer)初始化“data”变量。四个读线程( ReadFromBuffer)会在写线程(WriteToBuffer)完成对“data”变量的初始化之前一直 保持等待状态。当写线程(WriteToBuffer)将“data”变量初始化好之后,四个读线程(ReadFromBuffer)才会被一一唤醒。最后,主线程会在这四个读线程(ReadFromBuffer )和一个写线程(WriteToBuffer)都执行完成后退出,从而结束整个程序。
事件对象类的UNIX和Windows实现
UNIX实现
文件event.h
#ifndef __EVENT_H__
#define __EVENT_H__
#include <pthread.h>
class Event
{
public:
     Event(bool bManualUnsignal, bool bSignaled);
     virtual ~Event();
     virtual bool block();
     virtual bool signal();
     virtual bool unsignal();
private:
     const bool bManUnsig;
     pthread_cond_t cv;
     pthread_mutex_t mutex;
     bool bSig;
};
#endif
文件event.cpp
#include “event.h”
Event::Event(bool bManualUnsignal, bool bSignaled) : bManUnsig(bManualUnsignal
), bSig(bSignaled)
{
     pthread_mutex_init(&mutex, NULL);
     pthread_cond_init(&cv, NULL);
}
Event::~Event()
{
     pthread_mutex_destroy(&mutex);
     pthread_cond_destroy(&cv);
}
bool Event::block()
{
     int ret = 0;
     ret += pthread_mutex_lock(&mutex);
     if (bSig)
     {
         if (!bManUnsig)
         {
             bSig = false;
         }
     }
     else
     {
         pthread_cond_wait(&cv, &mutex);
         if (!bManUnsig)
         {
             bSig = false;
         }
     }
     ret += pthread_mutex_unlock(&mutex);
     return ret == 0;
}
bool Event::signal()
{
     int ret = 0;
     ret += pthread_mutex_lock(&mutex);
     if (!bSig)
     {
         if (bManUnsig)
         {
             ret += pthread_cond_broadcast(&cv);
         }
         else
         {
             ret += pthread_cond_signal(&cv);
         }
         bSig = true;
     }
     ret += pthread_mutex_unlock(&mutex);
     return ret == 0;
}
bool Event::unsignal()
{
     int ret = 0;
     ret += pthread_mutex_lock(&mutex);
     if (bSig)
     {
         bSig = false;
     }
     ret += pthread_mutex_unlock(&mutex);
     return ret == 0;
}
Windows实现
文件event.h
#ifndef __EVENT_H__
#define __EVENT_H__
#include <windows.h>
class Event
{
public:
     Event(bool bManualUnsignal, bool bSignaled);
     virtual ~Event();
     virtual bool block();
     virtual bool signal();
     virtual bool unsignal();
private:
     HANDLE handle;
};
#endif
文件event.cpp
#include “event.h”
Event::Event(bool bManualUnsignal, bool bSignaled)
{
     handle = CreateEvent(NULL, bManualUnsignal, bSignaled, NULL);
}
Event::~Event()
{
     CloseHandle(handle);
}
bool Event::block()
{
     return WaitForSingleObject(handle, INFINITE) == WAIT_OBJECT_0;
}
bool Event::signal()
{
     return SetEvent(handle) == TRUE;
}
bool Event::unsignal()
{
     return ResetEvent(handle) == TRUE;
}
小结
本节首先介绍了同步与异步的基本概念,进而说明了同步在多线程编程中的作用。
事件对象(Event Object)是一种最具弹性的同步机制。事件对象在某些条件满足之前将 一直保持非激发状态。程序员可以完全控制事件对象的状态(激发状态和非激发状态)。
事件对象使得程序员可以以最大的灵活性来定义复杂的同步对象。有两种类型的事件对象 (自动重置的事件对象和手动重置的事件对象)。一个手动重置的事件对象需要程序员显 式地将其状态从激发状态返回到非激发状态。然而一个自动重置的事件对象会在一个 Event::block()操作完成后自动地返回到非激发状态。
虽然事件对象所能完成的一切功能都可以通过互斥来完成,但是使用事件对象的解决方案 显得更加优雅,并且效率更高。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值