16.1.4 利用事件对象实现线程同步.txt
来自「网上第一本以TXT格式的VC++深入详解孙鑫的书.全文全以TXT格式,并每一章节」· 文本 代码 · 共 250 行
TXT
250 行
16.1.4 利用事件对象实现线程同步
本章仍以上一章火车票销售系统的例子来讲解线程间的同步,但这里将利用事件对象来实现。事件
对象与上一章介绍的互斥对象都属于内核对象。首先,新建一个空的 Win32 Console Application
类型的工程,工程名命名为 : Even t,井为该工程添加一个 C++源文件 : Event.cpp,然后就在此
文件中添加具体的实现代码,读者可以复制上一章示例程序中的 MultiThread.cpp文件中的内容,
并作相应的调整,结果代码如例 16-1所示。
例 16-1
#include <windows.h>
#include <iostream.h>
DWORD WINAPI Fun1Proc(
LPVOID lpParameter // thread data
DWORD WINAPI Fun2Proc( LPVOID lpParameter // thread data
int tickets=100;
HANDLE g_hEvent;
void main ()
HANDLE hThread1;
HANDLE hThread2;
//创建人工重置事件内核对象 ~
g_hEvent=CreateEvent(NULL , TRUE , FALSE , NULL);
//创建线程
hThread1=CreateThread(NULL, 0, Fun1Proc , NULL , 0, NULL);
hThread2=CreateThread(NULL, 0, Fun2Proc , NULL , 0 , NULL);
CloseHandle(hThreadl);
CloseHandle(hThread2);
//让主线程睡眠4秒、
Sleep(4000)i
//关闭事件对象句柄
CloseHandle(g_hEvent) ;
//线程1的入口函数
DWORD WINAPI Fun1Proc( LPVO工o lpParameter // thread data
while (TRUE)
//请求事件对象
WaitForSingleObject(g_hEvent,INFINITE) ;
if(tickets>0)
Sleep(1);
cout<<"thread1 sell ticket : "<<tickets--<<endl; }
else
break;
return 0;
//线程 2的入口函数
DWORD WINAPI Fun2Proc( LPVOID lpParameter // thread data
while(TRUE)
{
//请求事件对象
WaitForSingleObject (g_hEvent , INFINITE) ;
if(tickets>0)
Sleep(1);
cout<<"thread2 sell ticket : "<<tickets--<<endl;
else
break;
return 0;
.
上述例 16-1所示代码主要由以下几个部分组成 z
(1)包含必要的头文件
因为程序中需要访问 WindowsAPI函数,因此需要包含 windows.h文件,另外还用到了 C++的标准输
出函数,所以需要包含 C++的标准输入输出头文件: iostream.h.
(2)线程函数
在每个线程中都调用 WaitlForSingle C>bject函数请求事件对象,一旦得到事件对象之后,就可以
进入所保护的代码中,完成销售火车票的工作。
(3)全局变量的定义
定义了两个全局变量,其中一个是整型变量: tickets.表示当前销售的票号,初始值为 100;另一个
是 HANDLE类型的变量 :g_hEvent.用来保存即将创建的事件对象句柄。
(4) m甜1函数
当程序启动运行后,就会产生主线程, min函数就是主线程的入口函数。在这个主线程中可以创建
新的线程。在上述 mm函数中,首先调用 CreateEvent函数创建了一个事件内核对象,该函数的第一
个参数设置为 NULL.让该事件对象使用进程默认的安全性:第二个参数设置为 TRUE.即创建一个人
工重置的事件对象:第三个参数设置为 FALSE.即该事件对象初始处于无信号状态:最后一个参数设
置为 NULL.即创建一个匿名的事件对象。接下来调用 CreateThread函数创建了两个新的线程。接着,
让主线程睡眠 4秒。在 min函数结束之前,调用 CloseHandle函数关闭所创建的事件对象句柄。
Build井运行 Event程序,将会发现线程 1和线程 2并没有如我们所期望的那样完成销售火车票的工
作。读者应注意,在上述例 16-1所示代码的 man函数中创建事件对象时,将其初始状态设置为无信
号状态,这样,当线程 1和线程 2请求 g_hEvent这个事件对象时,
因为该事件对象始终都处于无信号状态, Wai tForSingleObject函数将导致线程暂停,这两个线程
都没有得到该事件对象,因此也就没有运行线程函数中 if语句块内的代码。如果想让线程 1或线程
2得到 g_hEvent这个事件对象的所有权,就必须将该事件对象设置为有信号状态。有两种方法可以
实现这一目的,一种方法是在创建事件对象时,将 CreateEvent函数的第三个参数设置为 TRUE,这
样所创建的事件对象初始就是处于有信号状态。员一种方法是在创建事件对象之后,通过调用
SetEvent函数把指定的事件对象设置为有信号状态。这里我们采用后一种方法,在如例 16-1所示代
码的 main函数中,在创建事件对象 (即食符号所示代码行〉之后,添加下面这条语句:
SetEvent(g_hEvent);
再次运行 Event程序,结果如图 16.1所示。
图 16.1利用事件对象实现线程同步示例程序运行结果失败
可以看到,这时线程 1和线程 2确实销售了火车票,但是发现最后打印出号码为的 O票号了,说明
程序存在问题。前面的内容曾提到,当人工重置的事件对象得到通知时,等待该事件对象的所有线
程均变为可调度线程。本例现在创建的就是一个人工重置的事件对象 (g_hEvent),当这个事件对象变
成有信号状态时,所有等待该对象的线程都变为可调度线程,也就是说,线程 1和线程 2可以同时
运行,正因为这两个线程可以同时运行,所以对其所保护的代码来说,这两个线程可以同时去执行
该代码,从而导致了程序打印出票号
0 (具体执行过程参见上一章中相应的解释内容)。既然两个线程可以同时运行,这就说明程序实现
的线程间的同步失败了。究其原因,主要是因为本例创建的是人工重置的事件对象。当一个线程等
待到一个人工重置的事件对象之后,这个事件对象仍然处于有信号状态,所以其他线程可以得到该
事件对象,从而进入所保护的代码并执行。那么如何解决这一问题呢?读者可能会想到这样的方法:
既然人工重置事件对象在被一个线程得到之后仍是有信号状态,那么线程在得到该事件对象之后,
立即调用 ResetEvent函数,将该事件对象设置为无信号状态,然后在对所保护的代码访问结束之后
再调用 SetEvent函数将该事件对象设置为有信号状态,这时才允许其他线程获得该事件对象的所有
权。也就是说,这时线程函数的代码如例 16-2所示。
例 16-2
DWORD W工 NAPI FunlProc (
LPVOID lpPararneter // t hread data
while (TRUE)
WaitForSingleObject(g_hEvent, INF工 NITE) ;
ResetEvent(g_hEvent);
if(tickets>0)
{
Sleep(1) ;
cout<<"threadl sell ticket : "<<tickets--<<endl ;
SetEvent(g_hEvent);
else
SetEvent(g_hEvent);
break;
return 0 ;
DWORD WINAPI Fun2Proc( LPVOID lpParameter /1 thread data
while(TRUE)
{
WaitForSingleObject(g_hEvent , INFINITE) ;
ResetEvent(g_hEvent);
if(tickets>0)
Sleep(1) ;
cout<<"thread2 sell ticket : "<<tickets--<<endl ;
SetEvent(g_hEvent);
else
SetEvent(g_hEvent) ;
break;
return 0 ;
这样是不是就可以解决问题了呢?我们来分析这时程序的执行过程。当线程 l获得事件对象后,调用
ResetEvent函数将事件对象:ιhEvent设置为无信号状态。当进入其 if
语句块之后,因为 Sleep函数的调用,该线程睡眠了,于是轮到线程 2开始执行。因为此
时 g_hEvent事件对象已经变成无信号状态,所以线程 2无法请求到该对象,只能一直等待。
当线程 1睡醒之后,销售一张火车票,再调用 SetEvent函数将 ιhEvent事件对象设置为有信号状态,
如果这时又轮到线程 2开始执行,那么它就可以得到该事件对象。同样地,在线程 2对保护的代码
执行完成之后,也调用 SetEvent函数将该事件设置为有信号状态。线程 1又可以获得该对象,然后
重复上述执行过程。
读者可以运行这时的 Event程序,但是将会发现程序结果仍然有数值为 O的票号,说明程序仍然没
有实现线程间的同步。实际上,上述做法存在两个问题,一个问题是,在单 CPU平台下,同一时刻
只能有一个线程在运行,假设线程 l先执行,它得到事件对象: g_hEvent,但是正好这时,它的时
间片终止了,于是轮到线程 2执行,但因为现在线程 l中, ResetEvent函数还没有被执行,所以该
事件对象仍然处于有信号状态,因此线程 2就可以得到该事件对象,也就是说,此时两个线程都可
以进入所保护的代码,于是结果就不可预料了。另一个问题是,当把 Event程序移植到多 CPU平台
上时,线程 l和线程 2就可以同时运行,这时再调用 SetEvent函数将其设置为有信号状态这一操作
己经不起作用了,因为这两个线程都己经进入了所保护的代码,它们同时使用同一资源,当然结果
也是未知的。所以为了实现线程间的同步,不应该使用人工重置的事件对象,而应该使用自动重置
的事件对象。也就是说,应该修改上述例 16-1所示代码中对 CreateEvent函数的调用 (即 '例 16-1
所示代码中食符号所在那行代码),将其第二个参数设置为 FALSE,修改结果如下所示:
g_hEvent=CreateEvent(NULL ,FALSE , FALSE ,NULL);
这时 Event程序的主线程将创建一个自动重置事件对象:ιhEvent,且它的初始状态为无信号状态。
然后读者还应该将先前在线程中添加的 ResentEvent函数和 SetEvent函数调用(即在上述例 16-2
所示代码中加灰显示的那些代码)注释起来。
再次运行 Eve nt程序,但是将会发现线程 l打印出票号 100之后,线程就没有再继续运行了,程序
结果如图 16 .2所示。
图 16.2只有线程 1打印出票号 100
我们来分析原因,前面己经提到,当一个自动重置的事件得到通知时,等待该事件的线程中只有一
个线程变为可调度线程。上述结果说明线程 1得到了事件对象,之后,因为它是一个自动事件,所
以操作系统会将该事件对象设置为无信号状态。程序继续向下执行,当线程 1睡眠时,轮到线程 2
执行,该线程调用 WaitForSingleObject函数请求事件对象,然而因为这时该事件对象已经处于无
信号状态,所以线程 2只能等待,于是执行权返回给线程1,线程 l继续运行,输出: 100。接着,
线程 1会再次请求事件对象,然而这时该事件对象的状态是无信号状态,所以 WaitForSingleObject
函数也无法得到该事件对象,于是
线程 1也只能等待。这样,线程 l和线程 2都在等待,所以就看到如图 16. 2所示的结果。
因为在线程请求到事件对象后,操作系统就会将该事件对象设置为无信号状态,所以为了让本程序
能够正常运行,在线程对保护的代码访问完成之后应该立即调用 SetEv ent函数,将该事件对象设
置为有信号状态,允许其他等待该对象的线程变成可调度状态。也就是说,这时线程 1函数的代码
如例 16-3所示,加灰显示的代码就是新添加的代码(按照同样的方法为线程 2函数添加 SetEvent
函数调用)。
例 16-3
1. DWORD WINAPI Fun1Proc(
2. LPVOID lpParameter / / thread data
3. )
4. {
5. while(TRUE)
6. {
7. WaitForSingleObject(g_hEvent , INFINITE) ;
8. / / ResetEvent(g_hEvent) ;
9. if (tickets>0)
10.
11 . Sleep(1);
12 . coutcc"thread1 sell ticket : "cctickets--c<endl ;
13. SetEvent(g_hEvent) ;
14 .
15 . else
16.
17. SetEvent(g_hEvent) ;
18 . break;
19 .
20 .
21. return 0 ;
22. }
完成上述代码修改之后,再次运行 Event程序,这时将会看到程序执行的结果正常了。
通过上面的例子可以知道,在使用事件对象实现线程间同步时,一定要注意区分人工重置事件对象
和自动重置事件对象。当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调
度线程:当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度
线程,同时操作系统会将该事件对象设置无信号状态,这样,当对所保护的代码执行完成后,需要
调用 SetEvent函数将该事件对象设置为有信号状态。而人工重置的事件对象,在一个线程得到该事
件对象之后,操作系统并不会将该事件对象设置为无信号状态,除非显式地调用 ResetEvent函数将
其设置为无信号状态,否则该对象会一直是有信号状态。
⌨️ 快捷键说明
复制代码Ctrl + C
搜索代码Ctrl + F
全屏模式F11
增大字号Ctrl + =
减小字号Ctrl + -
显示快捷键?