现在的位置: 首页 > 综合 > 正文

Windows程序设计(第2版)多线程四

2013年12月09日 ⁄ 综合 ⁄ 共 10272字 ⁄ 字号 评论关闭

2.应用举例

下面例子中,主线程通过将事件状态设为“受信”来通知子线程开始工作。这是事件内核对象一个很重要的用途,示例代码如下:

#include <stdio.h>                                             // 03EventDemo工程下

#include <windows.h>

#include <process.h>

HANDLE g_hEvent;

UINT __stdcall ChildFunc(LPVOID);

int main(int argc, char* argv[])

{       HANDLE hChildThread;

         UINT uId;

         // 创建一个自动重置的(auto-reset events),未受信的(nonsignaled)事件内核对象

         g_hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);

         hChildThread = (HANDLE)::_beginthreadex(NULL, 0, ChildFunc, NULL, 0, &uId);

         // 通知子线程开始工作

         printf("Please input a char to tell the Child Thread to work: /n");

         getchar();

         ::SetEvent(g_hEvent);

         // 等待子线程完成工作,释放资源

         ::WaitForSingleObject(hChildThread, INFINITE);

         printf("All the work has been finished. /n");

         ::CloseHandle(hChildThread);

         ::CloseHandle(g_hEvent);

         return 0;

}

UINT __stdcall ChildFunc(LPVOID)

{       ::WaitForSingleObject(g_hEvent, INFINITE);

         printf(" Child thread is working...... /n");

         ::Sleep(5*1000); // 暂停5秒,模拟真正的工作

         return 0;

}

运行程序,输入一个字符通知子线程开始工作,结果如图3.6所示。

图3.6 使用事件内核对象通信

主线程一开始,就创建了一个自动重置的(auto-reset),未受信的(nonsignaled)事件内核对象,并用全局变量g_hEvent保存对象的句柄。这样做会使本进程的其他线程访问此内核对象更加容易。接着子线程被创建,并等待主线程的通知来开始真正的工作。最后,子线程工作结束,主线程退出。

事件对象主要用于线程间通信,因为它是一个内核对象,所以也可以跨进程使用。依靠在线程间通信就可以使各线程的工作协调进行,达到同步的目的。

3.2.4 信号量内核对象

信号量(Semaphore)内核对象对线程的同步方式与前面几种方法不同,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore函数创建信号量时,即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能再允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。图3.7显示了信号量对象对资源的控制。

图3.7 使用信号量对象控制资源

在图3.7中,以箭头和白色箭头表示共享资源所允许的最大资源计数和当前可用资源计数。初始如图(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);
}

3.2.5 互斥内核对象

互斥(Mutex)是一种用途非常广泛的内核对象。能够保证多个线程对同一共享资源的互斥访问。同临界区有些类似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。与其他几种内核对象不同,互斥对象在操作系统中拥有特殊代码,并由操作系统来管理,操作系统甚至还允许其进行一些其他内核对象所不能进行的非常规操作。为便于理解,可参照图3.8给出的互斥内核对象的工作模型:

图3.8 使用互斥内核对象对共享资源的保护

图(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 ThreadProc1(LPVOID pParam)

{

 // 等待互斥对象通知

 WaitForSingleObject(hMutex, INFINITE);

 // 对共享资源进行写入操作

 for (int i = 0; i < 10; i++)

 {

  g_cArray[i] = 'a';

  Sleep(1);

 }

 // 释放互斥对象

 ReleaseMutex(hMutex);

 return 0;

}

UINT ThreadProc2(LPVOID pParam)

{

 // 等待互斥对象通知

 WaitForSingleObject(hMutex, INFINITE);

 // 对共享资源进行写入操作

 for (int i = 0; i < 10; i++)

 {

  g_cArray[10 - i - 1] = 'b';

  Sleep(1);

 }

 // 释放互斥对象

 ReleaseMutex(hMutex);

 return 0;

}

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

3.2.6 线程局部存储

线程局部存储(thread-local storage, TLS)是一个使用很方便的存储线程局部数据的系统。利用TLS机制可以为进程中所有的线程关联若干个数据,各个线程通过由TLS分配的全局索引来访问与自己关联的数据。这样,每个线程都可以有线程局部的静态存储数据。

用于管理TLS的数据结构是很简单的,Windows仅为系统中的每一个进程维护一个位数组,再为该进程中的每一个线程申请一个同样长度的数组空间,如图3.9所示。

图3.9 TSL机制在内部使用的数据结构

运行在系统中的每一个进程都有图3.9所示的一个位数组。位数组的成员是一个标志,每个标志的值被设为FREE或INUSE,指示了此标志对应的数组索引是否在使用中。Windodws保证至少有TLS_MINIMUM_AVAILABLE(定义在WinNT.h文件中)个标志位可用。

动态使用TLS的典型步骤如下。

(1)主线程调用TlsAlloc函数为线程局部存储分配索引,函数原型为:

DWORD TlsAlloc(void); // 返回一个TLS索引

如上所述,系统为每一个进程都维护着一个长度为TLS_MINIMUM_AVAILABLE的位数组,TlsAlloc的返回值就是数组的一个下标(索引)。这个位数组的惟一用途就是记忆哪一个下标在使用中。初始状态下,此位数组成员的值都是FREE,表示未被使用。当调用TlsAlloc的时候,系统会挨个检查这个数组中成员的值,直到找到一个值为FREE的成员。把找到的成员的值由FREE改为INUSE后,TlsAlloc函数返回该成员的索引。如果不能找到一个值为FREE的成员,TlsAlloc函数就返回TLS_OUT_OF_INDEXES(在WinBase.h文件中定义为-1),意味着失败。

例如,在第一次调用TlsAlloc的时候,系统发现位数组中第一个成员的值是FREE,它就将此成员的值改为INUSE,然后返回0。

当一个线程被创建时,Windows就会在进程地址空间中为该线程分配一个长度为TLS_MINIMUM_AVAILABLE的数组,数组成员的值都被初始化为0。在内部,系统将此数组与该线程关联起来,保证只能在该线程中访问此数组中的数据。如图3.7所示,每个线程都有它自己的数组,数组成员可以存储任何数据。

(2)每个线程调用TlsSetValue和TlsGetValue设置或读取线程数组中的值,函数原型为:

BOOL TlsSetValue(

DWORD dwTlsIndex,     // TLS 索引

LPVOID lpTlsValue                   // 要设置的值

);

LPVOID TlsGetValue(DWORD dwTlsIndex );       // TLS索引

TlsSetValue函数将参数lpTlsValue指定的值放入索引为dwTlsIndex的线程数组成员中。这样,lpTlsValue的值就与调用TlsSetValue函数的线程关联了起来。此函数调用成功,会返回TRUE。

调用TlsSetValue函数,一个线程只能改变自己线程数组中成员的值,而没有办法为另一个线程设置TLS值。到现在为止,将数据从一个线程传到另一个线程的惟一方法是在创建线程时使用线程函数的参数。

TlsGetValue函数的作用是取得线程数组中索引为dwTlsIndex的成员的值。

TlsSetValue和TlsGetValue分别用于设置和取得线程数组中的特定成员的值,而它们使用的索引就是TlsAlloc函数的返回值。这就充分说明了进程中惟一的位数组和各线程数组的关系。例如,TlsAlloc返回3,那就说明索引3被此进程中的每一个正在运行的和以后要被创建的线程保存起来,用以访问各自线程数组中对应的成员的值。

(3)主线程调用TlsFree释放局部存储索引。函数的惟一参数是TlsAlloc返回的索引。

利用TLS可以给特定的线程关联一个数据。比如下面的例子将每个线程的创建时间与该线程关联了起来,这样,在线程终止的时候就可以得到线程的生命周期。整个跟踪线程运行时间的例子的代码如下:

#include <stdio.h>                                   // 03UseTLS工程下

#include <windows.h>            

#include <process.h>

// 利用TLS跟踪线程的运行时间

DWORD g_tlsUsedTime;

void InitStartTime();

DWORD GetUsedTime();

UINT __stdcall ThreadFunc(LPVOID)

{       int i;

         // 初始化开始时间

         InitStartTime();

         // 模拟长时间工作

         i = 10000*10000;

         while(i--){}

         // 打印出本线程运行的时间

         printf(" This thread is coming to end. Thread ID: %-5d, Used Time: %d /n",

                                                                                                       ::GetCurrentThreadId(), GetUsedTime());

         return 0;

}

int main(int argc, char* argv[])

{       UINT uId;

         int i;

         HANDLE h[10];

         // 通过在进程位数组中申请一个索引,初始化线程运行时间记录系统

         g_tlsUsedTime = ::TlsAlloc();

         // 令十个线程同时运行,并等待它们各自的输出结果

         for(i=0; i<10; i++)

         {       h[i] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);         }

         for(i=0; i<10; i++)

         {       ::WaitForSingleObject(h[i], INFINITE);

                   ::CloseHandle(h[i]);      }

         // 通过释放线程局部存储索引,释放时间记录系统占用的资源

         ::TlsFree(g_tlsUsedTime);

         return 0;

}

// 初始化线程的开始时间

void InitStartTime()

{       // 获得当前时间,将线程的创建时间与线程对象相关联

         DWORD dwStart = ::GetTickCount();

         ::TlsSetValue(g_tlsUsedTime, (LPVOID)dwStart);

}

// 取得一个线程已经运行的时间

DWORD GetUsedTime()

{       // 获得当前时间,返回当前时间和线程创建时间的差值

         DWORD dwElapsed = ::GetTickCount();

         dwElapsed = dwElapsed - (DWORD)::TlsGetValue(g_tlsUsedTime);

         return dwElapsed;

}

GetTickCount函数可以取得Windows从启动开始经过的时间,其返回值是以毫秒为单位的已启动的时间。

一般情况下,为各线程分配TLS索引的工作要在主线程中完成,而分配的索引值应该保存在全局变量中,以方便各线程访问。上面的例子代码很清除地说明了这一点。主线程一开始就使用TlsAlloc为时间跟踪系统申请了一个索引,保存在全局变量g_tlsUsedTime中。之后,为了示例TLS机制的特点同时创建了10个线程。这10个线程最后都打印出了自己的生命周期,如图3.10所示。

3.10 各线程的生命周期

这个简单的线程运行时间记录系统仅提供InitStartTime和GetUsedTime两个函数供用户使用。应该在线程一开始就调用InitStartTime函数,此函数得到当前时间后,调用TlsSetValue将线程的创建时间保存在以g_tlsUsedTime为索引的线程数组中。当想查看线程的运行时间时,直接调用GetUsedTime函数就行了。这个函数使用TlsGetValue取得线程的创建时间,然后返回当前时间和创建时间的差值。

另外用于线程同步的内核对象还有互斥体和信号量,它们的用法也比较简单,这里就不介绍了。

抱歉!评论已关闭.