多线程8:经典线程同步总结 关键段 事件 互斥量 信号量

时间:2022-10-27 14:42:53

原文地址:http://blog.csdn.net/morewindows/article/details/7538247(在原文基础上有所修改)

下面对它们作个总结,帮助大家梳理各个知识点。

 

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

 

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

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

 

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

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

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

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

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

 

3.临界资源和临界区

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

 

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

 

互斥:关键段CS、互斥量Mutex

同步:事件Event、信号量Semaphore


关键段CS与互斥量Mutex

 

创建或初始化

销毁

进入互斥区域

离开互斥区域(释放关键段、互斥量)

关键段CS

CriticalSection

Delete-CriticalSection

Enter-CriticalSection

Leave-CriticalSection

互斥量Mutex

CreateMutex

CloseHandle

等待系列函数如WaitForSingleObject

ReleaseMutex

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

互斥量能很好的处理“遗弃”情况,因此在多进程之间可以放心的使用。

 

事件Event

  创建 使事件触发 等待事件 销毁 使事件未触发
事件Event CreateEvent ResetEvent 等待系列函数
WaitForSingleObject
CloseHandle ResetEvent


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

 

信号量Semaphore

 

创建

销毁

递减计数(等待信号量变为有信号状态)

递增计数(释放信号量,使计数减1

若为0,则变为无信号状态)

信号量

Semaphore

Create-Semaphore

CloseHandle

等待系列函数如WaitForSingleObject

Release-Semaphore

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

 

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

 

呵呵^_^,本系列一共使用了六篇文章来讲解了上面三个表格,如果读者能轻松写出这个表格并能解释下各函数的用法,那么对多线程的同步互斥问题也就有了良好的基础。


关于内核对象:

1 什么是内核对象
   内核对象是内核分配的一段空间,如文件对象和进程对象等。可以用Windows提供的函数来创建相应的内核对象。创建成功后返回一个对象句柄,并且对象句柄值是进程相关的。程序不能直接操作内核对象,只能通过Windows提供的函数来控制。


1.1内核对象的使用计数
     内核对象可以被多个进程同时使用,句柄值通常会不一样,但是引用的内容是同一块。内核对象的所有者是操作系统,而非进程,因此内核对象的存在时间通常会比进程的存在时间长。内核对象中有一个值用来保存当前使用该内核对象的进程数,这就是使用计数。这样可以确保在没有进程引用该对象时系统中不保留任何内核对象。内核对象由内核控制何时释放,而不是由调用它的进程,每一个内核对象的数据中都有一个进程引用计数,当某进行创建了一个内核对象时,该内核对象中的引用计数被置为1,之后要是有其它进程访问该内核对象时,引用计数加1,当所有访问内核对象的进程都释放,引用计数为0时,该内核对象由内核释放)。


1.2安全性
     内核对象提供了安全描述符来限制操作权限,如文件映射内核对象的函数
     HANDLE CreateFileMapping(
        HANDLE hFile,
        PSECURITY_ATTRIBUTES psa,
        DWORD flProtect,
        DWORD dwMaximumSizeHigh,
        DWORD dwMaximumSizeLow,
        PCTSTR pszName);
   参数psa就是来控制访问权限的。当访问一个现有的文件映射内核对象时需要设定要对对象执行什么操作,如: HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ, FALSE,"MyFileMapping"),其中FILE_MAP_READ说明是对内核对象执行读操作,"MyFileMapping"是内核对象的名字。
   注:我们可以通过对象的创建函数中是否有安全描述符来区分是内核对象还是一般的用户对象。
         
当调用一个用于创建内核对象的函数时,该函数会返回一个用于标识该对象的句柄


2 进程的内核对象句柄表

2.1创建内核对象
   当进程创建时,它会创建一个句柄表,用于存储内核对象的信息。当生成一个内核对象时,系统会将内核对象的地址以及其他信息存入句柄表,并返回一个句柄。这个句柄是和内存相关的,即不同的进程访问同一个内核对象时,返回的句柄值是不同的。当访问内核对象时,可以通过句柄到句柄表中查找对应的内核地址。
注:当查看创建内核对象的函数返回值时,必须格外小心。特别要注意的是,当调用CreateFile函数失败时,返回值为INVALID_HANDLE_VALUE(值为-1)。其他的内核函数返回值为NULL。


2.2关闭内核对象
关闭内核对象可以调用函数BOOL CloseHandle(HANDLE hobj)。系统会先判断句柄是否有效,如果有效,再判断计数是否为0,如果是的话,就将从内存中撤销该内核对象。不管成功与否,都将从句柄表中删除该对象信息,该进程将不能再访问。如果忘记调用CloseHandle函数,进程运行时将有可能引起资源泄漏。但是进程终止时,系统浏览句柄表,并释放所有资源。


内核对象跨进程使用:

有三种不同的机制允许进程共享内核对象:

      1、使用对象句柄继承

      2、为对象命名

      3、复制对象句柄

一、使用对象句柄继承

       进程之间的关系为父子进程关系的时候才能继承。实质为:父进程在创建子进程的时候,如果需要继承对象句柄,那么系统的扫描父进程的句柄表,将需要继承的对象句柄表中的信息,复制到子进程的句柄表中,然后通过某种方式将句柄值传给子进程,这样子进程就可以使用的这内核对象了。在这儿父进程、子进程对于内核对象而言是相同级别的,对两个进程而言,他们得到的句柄值是相同的,而且指向的是内核中的相同的一个对象

     所以我们可以看出,在这儿继承的不是对象,而仅仅是对象的句柄!!!那么我们如何去设置呢?

     首先我们要知道,实现继承必须要有三个开关

    1、创建内核对象时,有个参数开关                                 //针对于某个对象
    2、父进程句柄表的记录项中有个标志位的开关                       //针对于某个对象
    3、父进程创建子进程的时候,有个参数开关                         //针对于子进程(所有的可继承对象)

    那么我们分别来看:

1、创建内核对象时,有个参数开关           

    父进程必须分配并初始化一个SECURITY_ATTRIBUTES结构,然后将这个结构传给Create函数:

     SECURITY_ATTRIBUTES   sa ;

     sa.nLength = sizeof( sa );

     sa.lpSecurityDescriptor = NULL ;    //默认安全属性

     sa.bInhertHandle = TRUE ;  //make the returned handle  interitable  可继承的

     HANDLE   hMutex = CreateMutex ( &sa , FLASE , NULL ) ;

  2、父进程句柄表的记录项中有个标志位的开关       

      那就是句柄表中每一个记录项中保存的标志

      如果标志位0x00000000则表示此对象不可继承   如果标志位为0x00000001表示可以继承

     在这儿用函数SetHandleInformation在程序中可以改变句柄标志,

宏定义两个标志

#define   HANDLE_FLAG_INHERIT               0x00000001      //打开继承标志

#define   HANDLE_FLAG_RPOTECT_FROM_CLOSE    0x00000002      //标志此对象不允许用CloseHandle关闭句柄

     BOOL   SetHandleInformation (

           HANDLE   hObject ,    //标识某个句柄

           DWORD   dwMask ,   //表示我们想要修改哪个标志   有两个标志可以再这儿修改

           DWORD   dwFlags ) ;

          例如

       SetHandleInformation (hObj , HANDLE_FLAG_INHERIT , HANGDLE_FLAG_INHERIT ) ; 打开继承

      SetHandleInformation (hObj , HANDLE_FLAG_INHERIT , 0) ;                     关闭继承

      SetHandleInformation (hObj , HANDLE_FLAG_RPOTECT_FROM_CLOSE ,                                                                         HANDLE_FLAG_RPOTECT_FROM_CLOSE) ;  不允许关闭      

      CloseHandle(hObj);  //这样会引发异常  因为标志设为不允许关闭!!!

      注:还有一个函数GetHandleInformation可以在程序中查询当前标志位!!!

  3、父进程创建子进程的时候,有个参数开关         CreateProcess函数

     BOOL    CreateProcess(

            PCTSTR   pszApplicationName ,

            PTSTR   pszCommandLine ,          //命令行参数

            PSECURITY_ATTRIBUTES   psaProcess ,

            PSECURITY_ATTRIBUTES   psaThread ,

            BOOL   bInheritHandles ,      //设置子进程是否可以继承的属性  这又是一个开关!!!

            DWORD   dwCreationFlags ,

            PVOID   pvEnvironment ,

            PCTSTR   pszCurrentDirectory ,

            LPSTARTUPINFO   pStartupInfo ,

            PPROCESS_INFORMATION   pProcessInformation ) ;

    最后这儿有一个问题:子进程是继承了父进程的对象,复制工作操作系统也都已经完成,但是子进程还不知道所继承的对象的句柄呢...这个问题的解决方案是:在创建子进程的时候,将所继承的句柄通过命令行参数传进去,让子进程取得这个句柄!!!

 

二、为对象命名

    只是许多(而不是全部)内核对象可以进行命名,必须为对象指定一个名称才能用这种方法实现对象的共享。

如 CreateMutex   CreateEvent   CreateSemphore   CreateWaitableTimer   CreateFileMapping  CreateJobObject等,这些函数的最后一个参数是pszName。如果向这个参数传入NULL,那么这个对象就是匿名的,如果传入一个名称,那么这个对象就是有名称的。

   实质:例如A进程调用CreateMutex

     HANDLE hMutexPeocessA = CreateMutex ( NULL, FLASE, TEXT("AAA") ) ;

    当B进程也调用CreateMutex时      HANDLE hMutexPeocessB = CreateMutex ( NULL, FLASE, TEXT("AAA") ) ;

   系统首先会查看是否存在一个名为“AAA”的内核对象,如果不存在,那么创建,如果存在,内核会接着检查对象的类型,如果类型相符(A的是互斥,B也想创建互斥),那么这时候系统会进行安全检查,验证调用者是否拥有对该对象的完全访问权限。如果有权限,那么系统会再B进程的句柄表中找到一个空白位置,初始化这个记录项指向现有的那个对象。

特点:1、A进程和B进程得到的两个句柄值很有可能不相等,但是两者都指向的是同一个内核对象

      2、B进程可以不是A进程的子进程。

用这个方式为对象命名的时候,出现的情况是如果要创建的对象没有,则会创建,如果要创建的对象存在,则实现共享。

但是后者才是我们想实现的,即我们想仅仅实现共享。

    所以在这儿我们可以调用OpenMutex   OpenEvent   OpenSemphore   OpenWaitableTimer  OpenFileMapping   OpenJobObject这些函数的最后一个参数也是pszName,用这些函数可以实现,如果要打开的对象没有,则会返回失败,如果要打开的对象存在,则实现共享。

 

 

 

 三、复制对象句柄

 跨进程边界共享内核对象的最后一招是  使用  DuplicateHandle函数:

  这个参数的第一个和第三个是进程内核对象,必须是相对于调用这个函数的那个进程(需要理解)

BOOL  DuplicateHandle (

      HANDLE   hSourceProcessHandle //源进程的进程内核对象,是进程内核对象  传进其他内核对象会调用失败

      HANDLE    hSourceHandle //想要复制的内核对象的句柄,这个句柄和本进程无关,是源进程中的内核对象

      HANDLE    hTargetPeocessHandle ; //目标进程的进程内核对象   传进其他内核对象会调用失败

      PHANDLE   phTargetHandle ;  //得到新复制的内核对象在本进程中体现的句柄值

      DWORD   dwDesiredAccess ,  //设置新复制的对象的访问掩码

      BOOL   bInheritHandle ;        //设置新复制的对象的继承标志

      DWORD   dwOptions ) ;    //这个参数的设定和上面两个参数相关联,这个参数可以设置为0或            

            //DUPLICATE_SAME_ACCESS或DUPLICATE_CLOSE_SOURCE

           //如果这个参数设置为0,则上面两个参数就会起作用.

           //如果这个参数设置为DUPLICATE_SAME_ACCESS,那么目标句柄会获得和源进程的句柄一样的访问掩码

           //而且如果设置这个参数的话,程序将不会在看其他两个参数

          //如果这个参数设置为DUPLICATE_CLOSE_SOURCE,会关闭源进程中的句柄,如果是这样,内核对象的

          //计数就不会增加,刚好加1又减一,

          //注意:1、这儿是关闭的源进程的对应的对象的句柄

         //       2、DUPLICATE_SAME_ACCESS和DUPLICATE_CLOSE_SOURCE也可以联合使用

   

    简单的说,这个函数就是获得一个进程的句柄表中的一个记录项,然后在另一个进程的句柄表中创建一个副本。 在这儿两个进程得到的某对象的句柄值也不一定相等,复制的是句柄表中记录项,而不是复制的句柄值

    例子:进程S是源进程,它拥有一个对象的访问权。进程T是目标进程,它想要获得这个内核对象的访问权。进程C则是一个催化剂过程,它执行DuplicateHandle 这个函数。见《windows核心编程》P60