关闭

多线程程序设计(四)

587人阅读 评论(1) 收藏 举报

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取得线程的创建时间,然后返回当前时间和创建时间的差值。

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

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:250501次
    • 积分:2275
    • 等级:
    • 排名:第16474名
    • 原创:24篇
    • 转载:27篇
    • 译文:0篇
    • 评论:57条
    最新评论