多线程通讯问题

简介程序员

  线程之间通讯的两个基本问题是互斥和同步。编程

  线程同步是指线程之间所具备的一种制约关系,一个线程的执行依赖另外一个线程的消息,当它没有获得另外一个线程的消息时应等待,直到消息到达时才被唤醒。安全

  线程互斥是指对于共享的操做系统资源(指的是广义的"资源",而不是Windows的.res文件,譬如全局变量就是一种共享资源),在各线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任什么时候刻最多只容许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。多线程

  线程互斥是一种特殊的线程同步。函数

  实际上,互斥和同步对应着线程间通讯发生的两种状况:性能

  
(1)
当有多个线程访问共享资源而不使资源被破坏时;操作系统

  (2)当一个线程须要将某个任务已经完成的状况
通知
另一个或多个线程时。线程

  在
WIN32
中,同步机制主要有如下几种:指针

  (1)事件(Event);对象

  (2)信号量(semaphore);

  (3)互斥量(mutex);

  (4)临界区(
Critical
section)。

  全局变量

  由于进程中的全部线程都可以访问全部的全局变量,于是全局变量成为Win32多线程通讯的最简单方式。例如:

int var; //全局变量
UINT ThreadFunction(LPVOIDpParam)
{
 var = 0;
 while (var < MaxValue)
 {
  //线程处理
  ::InterlockedIncrement(long*) &var);
 }
 return 0;
}
请看下列程序:
int globalFlag = false;
DWORD WINAPI ThreadFunc(LPVOID n)
{
 Sleep(2000);
 globalFlag = true;

 return 0;
}

int main()
{
 HANDLE hThrd;
 DWORD threadId;

 hThrd = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &threadId);
 if (hThrd)
 {
  printf("Thread launched/n");
  CloseHandle(hThrd);
 }

 while (!globalFlag)
 ;
 printf("exit/n");
}

  上述程序中使用全局变量和while循环查询进行线程间同步,实际上,这是一种应该避免的方法,由于:

  (1)当主线程必须使本身与ThreadFunc函数的完成运行实现同步时,它并无使本身进入睡眠状态。因为主线程没有进入睡眠状态,所以操做系统继续为它调度C P U时间,这就要占用其余线程的宝贵时间周期;

  (2)当主线程的优先级高于执行ThreadFunc函数的线程时,就会发生globalFlag永远不能被赋值为true的状况。由于在这种状况下,系统决不会将任什么时候间片分配给ThreadFunc线程。

  事件

  事件(Event)是WIN32提供的最灵活的线程间同步方式,事件能够处于激发状态(signaled or true)或未激发状态(unsignal or false)。根据状态变迁方式的不一样,事件可分为两类:

  (1)手动设置:这种对象只可能用程序手动设置,在须要该事件或者事件发生时,采用SetEvent及ResetEvent来进行设置。

  (2)自动恢复:一旦事件发生并被处理后,自动恢复到没有事件状态,不须要再次设置。

  建立事件的函数原型为:

HANDLE CreateEvent(
 LPSECURITY_ATTRIBUTES lpEventAttributes,
 //
SECURITY
_ATTRIBUTES结构指针,可为NULL
 BOOL bManualReset,
 // 手动/自动
 // TRUE:在WaitForSingleObject后必须手动调用ResetEvent清除信号
 // FALSE:在WaitForSingleObject后,系统自动清除事件信号
 BOOL bInitialState, //初始状态
 LPCTSTR lpName //事件的名称
);

  使用"事件"机制应注意如下事项:

  (1)若是跨进程访问事件,必须对事件命名,在对事件命名的时候,要注意不要与系统命名空间中的其它全局命名对象冲突;

  (2)事件是否要自动恢复;

  (3)事件的初始状态设置。

  因为event对象属于内核对象,故进程B能够调用OpenEvent函数经过对象的名字得到进程A中event对象的句柄,而后将这个句柄用于ResetEvent、SetEvent和WaitForMultipleObjects等函数中。此法能够实现一个进程的线程控制另外一进程中线程的运行,例如:

HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent");
ResetEvent(hEvent);

临界区

  定义临界区变量

CRITICAL_SECTION gCriticalSection;

  一般状况下,CRITICAL_SECTION结构体应该被定义为全局变量,以便于进程中的全部线程方便地按照变量名来引用该结构体。

  初始化临界区

VOID WINAPI InitializeCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向程序员定义的CRITICAL_SECTION变量
);

  该函数用于对pcs所指的CRITICAL_SECTION结构体进行初始化。该函数只是设置了一些成员变量,它的运行通常不会失败,所以它采用了VOID类型的返回值。该函数必须在任何线程调用EnterCriticalSection函数以前被调用,若是一个线程试图进入一个未初始化的CRTICAL_SECTION,那么结果将是很难预计的。

  删除临界区

VOID WINAPI DeleteCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向一个再也不须要的CRITICAL_SECTION变量
);

  进入临界区

VOID WINAPI EnterCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向一个你即将锁定的CRITICAL_SECTION变量
);

  离开临界区

VOID WINAPI LeaveCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向一个你即将离开的CRITICAL_SECTION变量
);

  使用临界区编程的通常方法是:

void UpdateData()
{
 EnterCriticalSection(&gCriticalSection);
 ...//do something
 LeaveCriticalSection(&gCriticalSection);
}

  关于临界区的使用,有下列注意点:

  (1)每一个共享资源使用一个CRITICAL_SECTION变量;

  (2)不要长时间运行关键代码段,当一个关键代码段长时间运行时,其余线程就会进入等待状态,这会下降应用程序的运行性能;

  (3)若是须要同时访问多个资源,则可能连续调用EnterCriticalSection;

  (4)Critical Section不是OS核心对象,若是进入临界区的线程"挂"了,将没法释放临界资源。这个缺点在Mutex中获得了弥补。

  互斥

  互斥量的做用是保证每次只能有一个线程得到互斥量而得以继续执行,使用CreateMutex函数建立:

HANDLE CreateMutex(
 LPSECURITY_ATTRIBUTES lpMutexAttributes,
 // 安全属性结构指针,可为NULL
 BOOL bInitialOwner,
 //是否占有该互斥量,TRUE:占有,FALSE:不占有
 LPCTSTR lpName
 //信号量的名称
);

  Mutex是核心对象,能够跨进程访问,下面的代码给出了从另外一进程访问命名Mutex的例子:

HANDLE hMutex;
hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, L"mutexName");
if (hMutex){
 …

else{
 …
}

  相关API:

BOOL WINAPI ReleaseMutex(
 HANDLE hMutex
);

  使用互斥编程的通常方法是:

void UpdateResource()
{
 WaitForSingleObject(hMutex,…);
 ...//do something
 ReleaseMutex(hMutex);
}

  互斥(mutex)内核对象可以确保线程拥有对单个资源的互斥访问权。互斥对象的行为特性与临界区相同,可是互斥对象属于内核对象,而临界区则属于用户方式对象,所以这致使mutex与Critical Section的以下不一样:

  (1) 互斥对象的运行速度比关键代码段要慢;

  (2) 不一样进程中的多个线程可以访问单个互斥对象;

  (3) 线程在等待访问资源时能够设定一个超时值。

  下图更详细地列出了互斥与临界区的不一样:

临界区

  定义临界区变量

CRITICAL_SECTION gCriticalSection;

  一般状况下,CRITICAL_SECTION结构体应该被定义为全局变量,以便于进程中的全部线程方便地按照变量名来引用该结构体。

  初始化临界区

VOID WINAPI InitializeCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向程序员定义的CRITICAL_SECTION变量
);

  该函数用于对pcs所指的CRITICAL_SECTION结构体进行初始化。该函数只是设置了一些成员变量,它的运行通常不会失败,所以它采用了VOID类型的返回值。该函数必须在任何线程调用EnterCriticalSection函数以前被调用,若是一个线程试图进入一个未初始化的CRTICAL_SECTION,那么结果将是很难预计的。

  删除临界区

VOID WINAPI DeleteCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向一个再也不须要的CRITICAL_SECTION变量
);

  进入临界区

VOID WINAPI EnterCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向一个你即将锁定的CRITICAL_SECTION变量
);

  离开临界区

VOID WINAPI LeaveCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向一个你即将离开的CRITICAL_SECTION变量
);

  使用临界区编程的通常方法是:

void UpdateData()
{
 EnterCriticalSection(&gCriticalSection);
 ...//do something
 LeaveCriticalSection(&gCriticalSection);
}

  关于临界区的使用,有下列注意点:

  (1)每一个共享资源使用一个CRITICAL_SECTION变量;

  (2)不要长时间运行关键代码段,当一个关键代码段长时间运行时,其余线程就会进入等待状态,这会下降应用程序的运行性能;

  (3)若是须要同时访问多个资源,则可能连续调用EnterCriticalSection;

  (4)Critical Section不是OS核心对象,若是进入临界区的线程"挂"了,将没法释放临界资源。这个缺点在Mutex中获得了弥补。

  互斥

  互斥量的做用是保证每次只能有一个线程得到互斥量而得以继续执行,使用CreateMutex函数建立:

HANDLE CreateMutex(
 LPSECURITY_ATTRIBUTES lpMutexAttributes,
 // 安全属性结构指针,可为NULL
 BOOL bInitialOwner,
 //是否占有该互斥量,TRUE:占有,FALSE:不占有
 LPCTSTR lpName
 //信号量的名称
);

  Mutex是核心对象,能够跨进程访问,下面的代码给出了从另外一进程访问命名Mutex的例子:

HANDLE hMutex;
hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, L"mutexName");
if (hMutex){
 …

else{
 …
}

  相关API:

BOOL WINAPI ReleaseMutex(
 HANDLE hMutex
);

  使用互斥编程的通常方法是:

void UpdateResource()
{
 WaitForSingleObject(hMutex,…);
 ...//do something
 ReleaseMutex(hMutex);
}

  互斥(mutex)内核对象可以确保线程拥有对单个资源的互斥访问权。互斥对象的行为特性与临界区相同,可是互斥对象属于内核对象,而临界区则属于用户方式对象,所以这致使mutex与Critical Section的以下不一样:

  (1) 互斥对象的运行速度比关键代码段要慢;

  (2) 不一样进程中的多个线程可以访问单个互斥对象;

  (3) 线程在等待访问资源时能够设定一个超时值。

相关文章
相关标签/搜索