65.9K
CodeProject 正在变化。 阅读更多。
Home

通过真实生活示例理解线程同步对象

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2013年6月23日

CPOL

25分钟阅读

viewsIcon

35123

downloadIcon

568

本文将通过一个真实的例子帮助您理解线程的同步对象。

引言

很多人觉得编写多线程应用程序很困难。当我第一次学习线程时,我也处于类似的情况。从概念上理解线程及其同步非常重要。一旦概念清晰,编码就会变得非常容易。

我将尝试通过一个例子来解释这些概念。本文假设您对Windows线程API有基本了解(至少知道如何创建线程、向线程函数传递数据以及等待一个或多个线程完成)。如果您没有基本知识,可以参考文档包中的Thread.docx文档。此文档来自网络。

Using the Code

在您进一步阅读之前,请下载该软件包。软件包将包含两个文件夹,即FilesDocuments。以test结尾的CPP文件包含各种场景的测试。默认情况下,将运行CriticalSectionTest。要运行其他测试,您需要注释掉CriticalSectionTest并取消注释您想要运行的Test

Document文件夹中,您会找到Thread.docx,其中解释了线程的基本术语及其用法。我建议您先阅读此文档。它将让您了解什么是线程以及线程的一些基本术语。

files文件夹中,您会找到本文所需的所有文件。创建一个Visual Studio 9项目并添加所有文件。构建它。在开始构建之前,请更改Visual Studio中的以下设置。请参考下图。

转到“项目”->“属性”->“常规”->“字符集”。

从“使用Unicode字符集”更改为“使用多字节字符集”。请参考下图。

Click to enlarge image

在文件中,您会看到以Test结尾的cpp文件(例如MutexTest.cpp)。这些文件包含测试。所有文件的内容都被注释掉了。所以如果您想运行其中一个测试,您必须取消注释该文件的内容。您可以一次运行一个测试。除了您想运行测试的文件之外,所有文件的内容都应该被注释掉。

假设您的项目已准备就绪,让我们开始学习。

让我们以一个浴室为例。人们在浴室外面排队,希望按顺序一个接一个地使用它。必须有一个管理员来监督他们,以便他们一次只能一个人使用浴室。为此,管理员可以定义自己的规则来创建秩序。

我们将通过从上述例子中引申出来的类比来理解线程和同步过程,并且我们还将使用各种同步技术对其进行编码。

什么是资源?

顾名思义,资源可以理解为可以被使用的东西。在我们的例子中,它是浴室。换句话说,它是门内的一切。

什么是线程?

线程可以理解为一系列指令。就像在我们的例子中,排队等候的人就是线程。

同步对象

假设没有管理员可以制定规则,规定人们应该如何使用浴室。想象一下这种情况下会造成的混乱。为了维护系统的秩序,我们需要某种规则,以便资源(在我们的例子中是浴室)被线程(在我们的例子中是Persons)以系统和有序的方式使用。为了维护秩序,管理员定义了某种机制(在我们的例子中,比如说钥匙和灯泡)。通过这种机制可以有序地完成事情,这可以理解为同步对象。

首先让我们编写myThread类。

查看下载中的myThread.hmyThread.cpp文件。

  • 构造函数设置类成员,例如回调函数、线程名称、传递给线程函数的参数。
  • Execute()启动线程并将线程句柄存储在类变量m_thread中。
  • waitForThreadToFinish()等待线程完成。
  • waitForThreadsToFinish(int numOfThreads,HANDLE threadHandle[])等待我们以句柄数组形式传递到此方法中的所有线程完成。这被设计为static方法,因为它正在等待许多线程完成,所以我们应该能够以通用方式调用它。
  • 还有用于恢复、挂起线程的方法。还有其他功能也是不言自明的。

不同的管理员

临界区

假设有一个管理员,他的名字叫临界区(Critical Section)。他定义了自己的规则来维护秩序。让我们看看他是如何维护秩序的。

  1. 他站在排队的人群前面,一次只允许一个人进入浴室。
  2. 他只在看到浴室空着的时候才允许人进去。

让我们编写myCriticalSection类。查看下载包中的myCriticalSection.hmyCriticalSection.cpp文件。

  • 构造函数初始化临界区。初始化象征着管理员的创建。
  • 析构函数删除临界区。删除象征着管理员的删除。当所有排队的人都使用完浴室后,就不需要管理员了,所以需要删除。
  • LockCriticalSection()方法用于锁定资源,因为有人正在使用它们。这表示管理员已经锁定了浴室,因为有人正在使用它,他正站在排队的人群前面,不允许任何人进入。
  • UnLockCriticalSection()方法用于解锁资源,因为访问它的线程已经完成使用。这表示使用浴室的人已经使用完毕,所以管理员解锁浴室,并允许队列中的第一个人进入。
  • CloseCriticalSection()专门删除管理员。

到目前为止,我们已经准备好了临界区管理员和线程。让我们通过编写程序来模拟浴室场景。

请查看CriticalSectionTest.cpp文件。它包含主函数。

  • 我们将创建管理员。
     myCriticalSection csBathroom("Bathroom");
  • 我们将创建4个线程,分别为(RamShyamRajHari)并运行它们。这表示我们创建了4个人,他们都想去洗手间。这里有一点非常重要,就是将所有线程放入操作系统队列的机制。我们将线程的句柄存储在句柄数组中。
    myThread ShyamThread("Shyam", UseBathroomMonitoredByCS, (void*)"Shyam");
    ShyamThread.execute();
    handles[1] =  ShyamThread.getThreadHandle();
  • 每个线程/人尝试运行的函数是UseBathroom()。在这个函数内部,管理员锁定资源,然后人/线程使用资源,一旦人/线程完成使用资源,管理员解锁资源。资源使用由这4行代码定义。
    x = count;
    x++;
    count = x;
    Sleep(5000);

    前3行只是递增计数。虽然方式有点奇怪。目前,只需假设它就是这样工作的。本教程的最后会有解释。我们设置了5秒的睡眠时间,以便模拟人们使用浴室。

  • 最后,系统等待所有线程完成。它象征着所有人都使用完了浴室。
    WaitForMultipleObjects(numThreads,handles,TRUE,INFINITE);
  • 最后,我们删除管理员,因为他的工作已经完成。
    csBathroom.CloseCriticalSection();
  • 现在,让我们运行并查看。
    Initializing the critical section
    Entering the critical section
    Ram is in the bathroom
    Ram is using the bathroom
    Ram will leave the bathroom
    Leaving the critical section Entering the critical section
    
    Shyam is in the bathroom
    Shyam is using the bathroom
    Shyam will leave the bathroom
    Leaving the critical section Entering the critical section
    
    Raj is in the bathroom
    Raj is using the bathroom
    Raj will leave the bathroom
    Leaving the critical section Entering the critical section
    
    Hari is in the bathroom
    Hari is using the bathroom
    Hari will leave the bathroom
    Leaving the critical section
    Deleting the critical section
    The no. of people used the bathroom are:4
    Deleting the critical section

正如我们所看到的,系统是有序的。下一次运行时,第一个使用浴室的人可能是Shyam。线程的排队由操作系统完成,因此可能是随机的。我们无法控制这一点。

互斥体

假设有一位管理员,名叫互斥体(Mutex)。Mutex先生定义了他自己的规则来维持秩序。让我们看看他是如何维持秩序的。

  1. 他有一把浴室的钥匙。他把钥匙交给排在第一位的人。一旦那个人/线程拿到钥匙,他基本上就拥有了互斥体或钥匙。
  2. 那个人/线程使用浴室/资源,然后把钥匙还给/释放互斥体给管理员。
  3. 管理员再次把这把钥匙交给队列中的第一个人。
  4. 互斥体不是很聪明,记忆力很差。所以他随身带着一个登记簿,总是记录钥匙是否在他手上。如果他决定把钥匙给某人,他会在登记簿上标记为假,然后把它给队列中第一个等待的人。标记为假可以看作是给那个人钥匙可以拿的信号。当钥匙在他手上时,他会在登记簿上标记为真。

让我们编写myMutex类。查看下载包中的myMutex.hmyMutex.cpp文件。

  • 构造函数初始化互斥体。初始化象征着管理员的创建。
    • 这由系统方法CreateMutex()完成。此方法接受3个参数。
      1. 安全属性
      2. 如果此值为TRUE且调用者创建了互斥体,则调用线程获取互斥体对象的初始所有权。否则,调用线程不获取互斥体的所有权。如果您希望最初拥有互斥体,请将其设置为TRUE。如果设置为FALSE,则第一个请求权限的线程将获得所有权。
      3. 互斥体的名称,如果无名则为0。

o 返回值

    1. 如果函数成功,则返回值为新创建的互斥体对象的句柄。
    2. 如果函数失败,则返回值为NULL。要获取扩展错误信息,请调用GetLastError
    3. 如果互斥体是有名互斥体且此函数调用前对象已存在,则返回值为现有对象的句柄,GetLastError返回ERROR_ALREADY_EXISTSbInitialOwner被忽略,并且调用线程未获得所有权。但是,如果调用者访问权限有限,则函数将失败并返回ERROR_ACCESS_DENIED,并且调用者应使用OpenMutex函数。

o 如果第二个参数为false,则表示互斥体处于信号状态,排在第一个的人可以获取它。

  • 析构函数删除互斥体。删除象征着管理员的删除。当所有排队的人都使用完浴室后,就不需要管理员了,所以需要删除。
  • LockMutex()方法用于锁定资源,因为有人正在使用它们。这表示管理员已经锁定了浴室,因为有人正在使用它,他正站在排队的人群前面,不允许任何人进入。这是通过WaitForSingleObject()无限等待时间完成的。WaitForSingleObject()等待句柄(在本例中为互斥体句柄)并获取它。
  • UnlockMutex()方法用于解锁资源,因为访问它的线程已经完成。这表示使用浴室的人已经使用完毕,所以管理员解锁浴室并允许队列中的第一个人进入。这是通过ReleaseMutex()方法完成的。ReleaseMutex()释放互斥体。
  • CloseMutex()专门删除管理员。
  • OpenMutex函数使多个进程能够打开同一个互斥体对象的句柄。该函数仅在某个进程已使用CreateMutex函数创建了互斥体的情况下才能成功。调用进程可以在任何需要互斥体对象句柄的函数(例如等待函数)中使用返回的句柄,但受限于函数第一个参数中指定的访问权限。

o 它接受3个参数。

  1. 对互斥体对象的访问。使用互斥体只需要SYNCHRONIZE访问权限;要更改互斥体的安全性,请指定MUTEX_ALL_ACCESS。如果指定对象的安全描述符不允许调用进程请求的访问,则函数将失败。有关访问权限列表,请参阅同步对象安全性和访问权限。
  2. 如果此值为TRUE,则此进程创建的进程将继承此句柄。否则,进程不继承此句柄。
  3. 要打开的互斥体的名称。名称比较区分大小写。

o 返回值

    1. 如果函数成功,则返回值为互斥体对象的句柄。
    2. 如果函数失败,则返回值为NULL。要获取扩展错误信息,请调用GetLastError
    3. 如果命名的互斥体不存在,则函数失败,GetLastError返回ERROR_FILE_NOT_FOUND

我们已经准备好互斥体管理员。让我们尝试通过编写程序来模拟浴室场景。

请查看MutexTest.cpp文件。它包含主函数。由于我们已经对临界区进行了说明,因此代码是不言自明的。预期会得到相同的输出。

信号量

现在是时候介绍一位名叫信号量(Semaphore)的新管理员了。他定义了以下规则:

  1. 他有一把浴室的钥匙。他把钥匙交给排在第一位的人。一旦那个人/线程拿到钥匙,他基本上就拥有了信号量或钥匙。
  2. 那个人/线程使用浴室/资源,然后把钥匙还给/释放信号量给管理员。
  3. 管理员再次把这把钥匙交给队列中的第一个人。
  4. 信号量比互斥体更聪明。他会记录信号量所拥有的钥匙数量。所以当他拥有钥匙时,他会将计数器增加1。他桌子上有一个计数器,指示着数量。当他把钥匙交给那个人时,他会将计数器减少1。当计数器显示大于0时,意味着信号量可供获取,队列中的第一个人可以获取它。
  5. 让我们考虑一个场景,有不止一个浴室(比如3个),我们需要为多个浴室的使用创建秩序。在这种情况下,我们的信号量管理员可以通过遵循其规则来管理它:
    1. a. 计数器中的初始计数将显示3。
    2. b. 他把一把钥匙给队列中的第一个人,并将计数器减1。所以当3个人都在浴室时,计数器为0,因此其他人必须等待。
    3. c. 只要有人出来并交回钥匙,计数器就会增加,第一个人就可以拿到钥匙并使用浴室。
  6. 到目前为止,您会发现如果信号量只能处理1把钥匙,它的行为就像互斥体。
  7. 信号量可以分为两类:
    1. 互斥信号量/二进制信号量。
      1. 如果计数器的值只能是0和1,则称为二进制信号量。
      2. 如果我们将计数器替换为布尔变量,则它变为互斥信号量。
      3. 两者可以互换使用。
    2. 计数信号量
      1. 如前所述,计数器的最大值可以大于1。

让我们编写mySemaphore类。查看下载包中的mySemaphore.hmySemaphore.cpp文件。

我们将更详细地编写这个类。例如,我们将初始化安全属性。我们将编写可以创建和打开信号量的构造函数。内存分配/删除通过HeapAlloc()HeapFree()方法完成。这将真实地展现实际类应该是什么样子。

• 构造函数初始化信号量,如果它已经存在,则尝试打开它。初始化象征着管理员的创建。如果信号量已经存在,它可以打开它。

  • 这是通过CreateSemaphore()方法完成的。它创建或打开信号量。
  • 它接受4个参数。
    1. 安全属性。
    2. 信号量计数的初始值。这里给出的初始值为1——这意味着信号量已发出信号,并且队列中的第一个线程。
    3. 信号量计数的最大值。
    4. 信号量的名称,如果信号量无名则为0。
    • 返回值:返回信号量的句柄,或在出错时返回NULL。如果指定名称的信号量已经存在,则GetLastError()将返回ERROR_ALREADY_EXISTS,但句柄将有效。
    • 打开现有信号量通过OpenSemaphore()完成。详细信息可在网上查阅。
  • 析构函数删除信号量。删除象征着管理员的删除。当所有排队的人都使用完浴室后,就不需要管理员了,所以需要删除。
  • LockSemaphore (BOOL wait=TRUE)方法用于锁定资源,因为有人正在使用它们。这表示管理员已经锁定了浴室,因为有人正在使用它,他正站在排队的人群前面,不允许任何人进入。它接受一个布尔参数。如果此值为true,它会无限期等待;如果为false,则不等待任何时间。这是通过WaitForSingleObject()无限等待时间完成的。WaitForSingleObject()等待句柄(在本例中为信号量句柄)并获取它。
  • UnlockSemaphore ()方法用于解锁资源,因为访问它的线程已经完成。这表示使用浴室的人已经使用完毕,所以管理员解锁浴室并允许队列中的第一个人进入。这是通过ReleaseSemaphore ()方法完成的。ReleaseSemaphore ()释放信号量。
  • CloseSemaphore ()专门删除管理员。
  • 其他方法都相当容易理解。

我们已经准备好信号量管理员。让我们尝试通过编写程序来模拟浴室场景。

请查看SemaphoreTest.cpp文件。它包含主函数。由于我们已经对临界区进行了说明,因此代码是不言自明的。预期会得到相同的输出。如果您已经理解了信号量的概念,请尝试为3个浴室和10个人创建程序。

事件

让我们了解一个新的管理员,名叫Event。他定义了以下规则:

  1. 他在浴室门上方安装了一个灯泡。它将作为排队等候的人的信号。
  2. 一旦有人进入浴室,灯泡就会变红,告诉其他人等待。
  3. 当那个人走出浴室时,信号变为绿色,指示排队的人资源可用。
  4. 事件管理员很聪明,也有点懒惰,所以他要求线程自己发出信号(当他们使用完浴室后将灯泡从红色变为绿色),他则愉快地坐着观察他们。
  5. 如果他以这种方式将灯泡从绿色变为红色(仅此)的任务委托给线程,则称为手动信号。否则,如果由管理员执行,则称为自动信号。
    1. 手动重置事件
      1. 正如我们所知,天下没有免费的午餐,因此当线程自行处理信号(仅从绿色到红色)时,一旦灯泡变为绿色,队列中的所有线程都会尝试抢占浴室。
      2. 线程有责任将信号从红色变为绿色,再从绿色变为红色。从绿色到红色通过resetEvent()完成,从红色到绿色通过setEvent()完成。
    2. 自动重置事件
      1. 一旦灯泡从红色变为绿色,队列中只有1个线程(第一个)会尝试获取资源。
      2. 线程有责任将信号从红色变为绿色。从绿色到红色通过resetEvent()完成,从红色到绿色通过setEvent()完成。
  6. 当一个线程需要与另一个线程协调时,自动重置事件很有用。当一个线程需要与任意数量的线程协调时,手动重置事件很有用。
  7. 让我们编写myEvent类。查看下载包中的myEvent.hmyEvent.cpp文件。
  • 构造函数初始化事件。它接受三个参数。第一个是事件名称,第二个是布尔值,指示事件是手动重置还是自动重置。第三个是初始信号状态。
    • 这是通过CreateEvent()完成的。它创建一个事件,如果指定的名称已存在,则打开它。
    • 它接受4个参数。
      1. 安全属性
      2. 手动重置为TRUE,自动重置为FALSE
      3. 事件的初始状态:如果已发出信号,则为TRUE
      4. 事件的名称,如果无名则为0
    • 返回值:返回事件的句柄,或在出错时返回0。如果指定名称的事件已经存在,则GetLastError将返回ERROR_ALREADY_EXISTS,但句柄将有效。
  • 析构函数删除事件。删除象征着管理员的删除。当所有排队的人都使用完浴室后,就不需要管理员了,所以需要删除。这是通过closehandle()消息完成的。
  • setEvent()方法用于将信号从红色变为绿色。
  • resetEvent()方法用于将信号从绿色变为红色。
  • pulseEvent()方法的详细信息可在网上查阅。
  • closeEvent()专门删除管理员。
  • 其他方法都相当容易理解。

我们已经准备好事件管理员。让我们尝试通过编写程序来模拟浴室场景。

  1. 我们将尝试使用自动重置事件来模拟浴室场景。请查看AutoEventTest.cpp。输出符合预期,每个人轮流使用浴室。
    1. 让我们看看UseBathroomMonitoredByAutoEvent方法的作用。
    2. 首先,我们使用WaitForEvent()等待信号;
    3. 一旦信号变为绿色,队列中的第一个线程将尝试获取资源(前面已解释)。
    4. 使用资源。
      Use Resources
      x = count;
      x++;
      count = x;
      Sleep(5000);
    5. 将信号设置为绿色,以便其他线程可以使用。
    6. 我们将在此处看到浴室的同步使用。
    7. 由于初始状态是红色或未发出信号,我们在主函数中将其更改为绿色或已发出信号:manualEventBathroom.setEvent();
    8. 其他内容与前面的例子保持相同。
    9. 当一个线程需要向另一个线程发出信号时,这种类型的自动重置事件非常有用。这就是它帮助我们同步的原因。

    我们将尝试使用手动重置事件来模拟浴室场景。请查看ManualEventTest.cpp。我们得到的输出类似这样:

    Initializing the Event
    Giving the signal
    The signal has been given, Acquire the resource The signal has been given, 
    Acquire the resource The signal has been given, 
    Acquire the resource The signal has been given, Acquire the resource
    Resetting the Event  Resetting the Event  Resetting the Event  Resetting the Event
    RamShyamRajHari is in the bathroom is in the bathroom is in the bathroom is in the bathroom
    RamShyamRajHari is using the bathroom is using the 
    bathroom is using the bathroom is using the bathroom
    RamShyamRajHari will leave the bathroom will leave the 
    bathroom will leave the bathroom will leave the bathroom
    Giving the signal Giving the signal Giving the signal Giving the signal
    Deleting the Event
    The no. of people used the bathroom are:4
    Deleting the Event
  2. 让我们看看UseBathroomMonitoredByManualEvent方法的作用。
    1. 首先,我们使用WaitForEvent()等待信号;
    2. 一旦信号变为绿色,队列中的所有线程都将尝试获取资源(前面已解释)。
    3. 一旦我们收到信号,我们就会将信号变为红色。
      manualEventBathroom.resetEvent();
    4. 使用资源。
      Use Resources
      x = count;
      x++;
      count = x;
      Sleep(5000);
    5. 将信号设置为绿色,以便其他线程可以使用。
    6. 我们没有看到同步使用。它非常随机。
  3. 现在问题出现了,在什么情况下我们应该使用手动重置事件。当一个线程想要向许多其他线程发出信号时,可以使用它。在这种情况下,所有线程/人员都捕获信号并试图抢占浴室。
  4. 由于初始状态是红色或未发出信号,我们在主函数中将其更改为绿色或已发出信号。
    manualEventBathroom.setEvent();
    
  5. 其他内容与前面的例子保持相同。

到目前为止,您应该已经发现事件与锁(临界区、互斥体和信号量)不同。事件用于信号传递。然而,我们使用正确的机制来同步线程。正如我们所见,自动重置事件可以用于同步线程。但是,我们仍然需要弄清楚如何使用手动重置事件。

现在让我们看看如何使用手动重置事件。我们将通过解决一个问题来理解这个场景。问题如下:

假设正在进行一场比赛。

  • 有三名参与者(A、B、C)。
  • A在6秒内完成比赛,B在7秒内完成,C在8秒内完成。
  • A在比赛开始后1秒发出信号,B在2秒后发出,C在3秒后发出。
  • 我们必须以一种方式模拟这个场景,即我们能够捕获所有信号,并且能够告知所有参与者何时完成了比赛。
    请查看ManualEventTest1.cpp文件。
  • 参与比赛的三个人由三个线程表示(RunParticipantARunParticipantBRunParticipantC)。
  • 我们分别为参与者A、B和C创建了三个手动事件(Event1Event2Event3)。所有事件的初始信号状态都是未发出信号。
  • 让我们看看参与者A是如何进行比赛的。他开始比赛。1秒后发出信号。
    • 1秒的等待通过sleep函数模拟。
    • 然后事件被重置(不需要,但是这样做是因为初始状态没有发出信号)。
    • 然后事件发出信号。这是通过setEvent()完成的。
    • 线程再等待5秒以完成。
  • 在主函数中,我们收集所有事件的句柄并等待它们全部发生。
    • 这是通过WaitForMultipleObjects( 3, Handles_Events, TRUE, 6000);完成的。
  • 在主函数中,我们还收集线程的句柄,并等待它们全部完成,或者所有人都结束比赛。
  • 输出将是这样的:
    Initializing the Event
    Initializing the Event
    Initializing the Event
    RACE STARTED
    Participant A started the race
    Participant C started the race
    Participant B started the race
    Waiting for all the events to get signalled
    Resetting the Event
    Participant A will give the signal...
    Giving the signal
    Deleting the Event
    Resetting the Event
    Participant B will give the signal...
    Giving the signal
    Deleting the Event
    Resetting the Event
    Participant C will give the signal...
    Giving the signal All events are signalled
    Deleting the Event
    Participant A has finished the race
    Participant B has finished the race
    Participant C has finished the race
    RACE FINISHED
    Deleting the Event
    Deleting the Event
    Deleting the Event

通过查看输出,我们模拟了上面问题描述中提到的场景。

让我们尝试编写一个比我们之前编写的event类更智能的event类。我们称之为mySmartEvent类。请查看mySmartEvent.hmySmartEvent.cpp文件。

这个事件类有什么聪明之处?

每当创建此类的对象时,都会创建一个事件,我们启动一个线程来监视该事件的状态。当事件发出信号时,此线程会将回调排队等待执行。让我们尝试查看类函数及其实现。

  • 构造函数接受三个参数。第一个是事件名称;第二个是回调函数名称;最后一个是布尔值,用于控制是否应该将回调排队。
    • 创建了一个手动重置事件,其初始状态未发出信号。
    • 回调函数被设置为类成员变量,之后将使用它。
    • 启动一个线程,该线程将调用ThreadProc()方法。值得注意的是,我们在此处将this指针作为线程参数传递。这样做是因为在Thread Proc中我们将需要事件对象。
  • ThreadProc等待事件对象处于信号状态。
    • 这是通过WaitForSingleObjectEx()完成的。
  • 回调函数通过QueueUserAPC()排队。
  • 我们让线程无限睡眠以确保回调被调用。
  • 其他功能与之前的myEvent类相同。

现在让我们来测试这个类。请查看SmartEventTest.cpp

  • 我们定义了一个回调,它递增一个static整数并打印它。
  • 创建一个智能事件对象。
    • 一旦我们创建此对象,一个线程就会开始等待该事件发出信号。
  • main()内部,我们发出事件信号。
    • 线程看到事件已发出信号并排队回调以执行。
  • 为了再次确认,我们使用eventSmart1.WaitForEvent(0)检查事件是否已发出信号。我们使用0等待时间是因为我们知道事件已经发生。我们这里不是等待,而是仅仅检查。
  • 程序的输出将是:
    Initializing the Event
    Signaling the event
    Giving the signal The state of the specified event object is signaled.
    Waiting for event to happen schedule the callback...
    The signal has been given, Acquire the resource Hello I am callback and value if ii is
    1The event has happened
    Deleting the Event

WaitForSingleObjectEx 和 WaitForSingleObject 有什么区别?

WaitForSingleObjectEx函数与WaitForSingleObject函数类似,不同之处在于它多了一个布尔参数,该参数的值决定了调用线程是否处于可警报等待状态。

如果此布尔参数设置为TRUE,则每当完成例程或异步过程调用(APC)排队时,该函数将返回。当调用QueueUserAPC API时,APC排队;当ReadFileExWriteFileEx API完成时,完成例程排队。当调用ReadFileExWriteFileEx API时,将完成函数名称作为参数传递。当实际读写完成时,等待函数返回,并且系统将调用完成函数,前提是线程处于可警报等待状态。

让我们考虑一个例子来更好地理解可警报等待状态的含义。假设您正在使用命名管道或邮槽进行进程间通信(IPC)。进一步假设客户端应用程序必须等待线程完成。同时,每当消息到达管道时,必须执行一些操作。这可以通过使用WaitForSingleObjectEx API,并将线程句柄作为第一个参数,并将可警报参数设置为TRUE来实现。

摘要

到此,本文将结束。回顾一下,我们已经了解了基本的同步对象(临界区、互斥体和信号量)。我们还学习了事件。我们已经看到了事件与锁的不同之处,以及它们如何用于同步线程。我们还看到了如何创建一个智能事件类,该类在指定时将调用一个回调函数。

请原谅我的语法错误。您的建议和建设性意见将不胜感激。

关注点

线程并没有那么难处理。线程的概念在所有操作系统中或多或少都是相同的。学习基本概念总能节省大量时间。

© . All rights reserved.