VC++ 线程同步 总结

注:所谓同步,并不是多个线程一起同时执行,而是他们协同步调,按预定的先后次序执行。

与线程相关的基本函数包括:
CreateThread:创建线程
CloseHandle:关闭线程句柄。注意,这只会使指定的线程句柄无效(减少该句柄的引用计数),启动句柄的检查操作,如果一个对象所关联的最后一个句柄被关闭了,那么这个对象会从系统中被删除。关闭句柄不会终止相关的线程。

线程是如何运行的呢?这又与你的CPU有关系了,如果你是一个单核CPU,那么系统会采用时间片轮询的方式运行每个线程;如果你是多核CPU,那么线程之间就有可能并发运行了。这样就会出现很多问题,比如两个线程同时访问一个全局变量之类的。它们需要线程的同步来解决。所谓同步,并不是多个线程一起同时执行,而是他们协同步调,按预定的先后次序执行。
Windows下线程同步的基本方法有3种:互斥对象、事件对象、关键代码段(临界区),下面一一介绍:

互斥对象属于内核对象,包含3个成员:
1.使用数量:记录了有多少个线程在调用该对象
2.一个线程ID:记录互斥对象维护的线程的ID
3.一个计数器:前线程调用该对象的次数
与之相关的函数包括:
创建互斥对象:CreateMutex
判断能否获得互斥对象:WaitForSingleObject
对于WaitForSingleObject,如果互斥对象为有信号状态,则获取成功,函数将互斥对象设置为无信号状态,程序将继续往下执行;如果互斥对象为无信号状态,则获取失败,线程会停留在这里等待。等待的时间可以由参数控制。
释放互斥对象:ReleaseMutex
当要保护的代码执行完毕后,通过它来释放互斥对象,使得互斥对象变为有信号状态,以便于其他线程可以获取这个互斥对象。注意,只有当某个线程拥有互斥对象时,才能够释放互斥对象,在其他线程调用这个函数不得达到释放的效果,这可以通过互斥对象的线程ID来判断。

 1 #include <Windows.h>
 2 #include <stdio.h>
 3
 4 //线程函数声明
 5 DWORD WINAPI Thread1Proc(  LPVOID lpParameter);
 6 DWORD WINAPI Thread2Proc(  LPVOID lpParameter);
 7
 8 //全局变量
 9 int tickets = 100;
10 HANDLE hMutex;
11
12 int main()
13 {
14     HANDLE hThread1;
15     HANDLE hThread2;
16     //创建互斥对象
17     hMutex = CreateMutex( NULL,            //默认安全级别
18                           FALSE,        //创建它的线程不拥有互斥对象
19                           NULL);        //没有名字
20     //创建线程1
21     hThread1 = CreateThread(NULL,        //默认安全级别
22                             0,            //默认栈大小
23                             Thread1Proc,//线程函数
24                             NULL,        //函数没有参数
25                             0,            //创建后直接运行
26                             NULL);        //线程标识,不需要
27
28     //创建线程2
29     hThread2 = CreateThread(NULL,        //默认安全级别
30                             0,            //默认栈大小
31                             Thread2Proc,//线程函数
32                             NULL,        //函数没有参数
33                             0,            //创建后直接运行
34                             NULL);        //线程标识,不需要
35
36     //主线程休眠4秒
37     Sleep(4000);
38     //主线程休眠4秒
39     Sleep(4000);
40     //关闭线程句柄
41     CloseHandle(hThread1);
42     CloseHandle(hThread2);
43
44     //释放互斥对象
45     ReleaseMutex(hMutex);
46     return 0;
47 }
48
49 //线程1入口函数
50 DWORD WINAPI Thread1Proc(  LPVOID lpParameter)
51 {
52     while(TRUE)
53     {
54         WaitForSingleObject(hMutex,INFINITE);
55         if(tickets > 0)
56         {
57             Sleep(10);
58             printf("thread1 sell ticket : %d\n",tickets--);
59             ReleaseMutex(hMutex);
60         }
61         else
62         {
63             ReleaseMutex(hMutex);
64             break;
65         }
66     }
67
68     return 0;
69 }
70
71 //线程2入口函数
72 DWORD WINAPI Thread2Proc(  LPVOID lpParameter)
73 {
74     while(TRUE)
75     {
76         WaitForSingleObject(hMutex,INFINITE);
77         if(tickets > 0)
78         {
79             Sleep(10);
80             printf("thread2 sell ticket : %d\n",tickets--);
81             ReleaseMutex(hMutex);
82         }
83         else
84         {
85             ReleaseMutex(hMutex);
86             break;
87         }
88     }
89
90     return 0;
91 }
1 使用互斥对象时需要小心:
2 调用假如一个线程本身已经拥有该互斥对象,则如果它继续调用WaitForSingleObject,则会增加互斥对象的引用计数,此时,你必须多次调用ReleaseMutex来释放互斥对象,以便让其他线程可以获取:
1     //创建互斥对象
2     hMutex = CreateMutex( NULL,            //默认安全级别
3                           TRUE,            //创建它的线程拥有互斥对象
4                           NULL);        //没有名字
5     WaitForSingleObject(hMutex,INFINITE);
6     //释放互斥对象
7     ReleaseMutex(hMutex);
8     //释放互斥对象
9     ReleaseMutex(hMutex);

下面看事件对象,它也属于内核对象,包含3各成员:
1.使用计数
2.用于指明该事件是自动重置事件还是人工重置事件的布尔值
3.用于指明该事件处于已通知状态还是未通知状态。
自动重置和人工重置的事件对象有一个重要的区别:当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;而当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。
与事件对象相关的函数包括:
创建事件对象:CreateEvent
HANDLE CreateEvent(  LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState,LPCTSTR lpName);
设置事件对象:SetEvent:将一个这件对象设为有信号状态
BOOL SetEvent(  HANDLE hEvent  );
重置事件对象状态:ResetEvent:将指定的事件对象设为无信号状态
BOOL ResetEvent(  HANDLE hEvent );

下面仍然使用买火车票的例子:

 1 #include <Windows.h>
 2 #include <stdio.h>
 3
 4 //线程函数声明
 5 DWORD WINAPI Thread1Proc(  LPVOID lpParameter);
 6 DWORD WINAPI Thread2Proc(  LPVOID lpParameter);
 7
 8 //全局变量
 9 int tickets = 100;
10 HANDLE g_hEvent;
11
12 int main()
13 {
14     HANDLE hThread1;
15     HANDLE hThread2;
16     //创建事件对象
17     g_hEvent = CreateEvent( NULL,    //默认安全级别
18                             TRUE,    //人工重置
19                             FALSE,    //初始为无信号
20                             NULL );    //没有名字
21     //创建线程1
22     hThread1 = CreateThread(NULL,        //默认安全级别
23                             0,            //默认栈大小
24                             Thread1Proc,//线程函数
25                             NULL,        //函数没有参数
26                             0,            //创建后直接运行
27                             NULL);        //线程标识,不需要
28
29     //创建线程2
30     hThread2 = CreateThread(NULL,        //默认安全级别
31                             0,            //默认栈大小
32                             Thread2Proc,//线程函数
33                             NULL,        //函数没有参数
34                             0,            //创建后直接运行
35                             NULL);        //线程标识,不需要
36
37
38     //主线程休眠4秒
39     Sleep(4000);
40     //关闭线程句柄
41     //当不再引用这个句柄时,立即将其关闭,减少其引用计数
42     CloseHandle(hThread1);
43     CloseHandle(hThread2);
44     //关闭事件对象句柄
45     CloseHandle(g_hEvent);
46     return 0;
47 }
48
49 //线程1入口函数
50 DWORD WINAPI Thread1Proc(  LPVOID lpParameter)
51 {
52     while(TRUE)
53     {
54         WaitForSingleObject(g_hEvent,INFINITE);
55         if(tickets > 0)
56         {
57             Sleep(1);
58             printf("thread1 sell ticket : %d\n",tickets--);
59         }
60         else
61             break;
62     }
63
64     return 0;
65 }
66
67 //线程2入口函数
68 DWORD WINAPI Thread2Proc(  LPVOID lpParameter)
69 {
70     while(TRUE)
71     {
72         WaitForSingleObject(g_hEvent,INFINITE);
73         if(tickets > 0)
74         {
75             Sleep(1);
76             printf("thread2 sell ticket : %d\n",tickets--);
77         }
78         else
79             break;
80     }
81
82     return 0;
83 }

程序运行后并没有出现两个线程买票的情况,而是等待了4秒之后直接退出了,这是什么原因呢?问题出在了我们创建的事件对象一开始就是无信号状态的,因此2个线程线程运行到WaitForSingleObject时就会一直等待,直到自己的时间片结束。所以什么也不会输出。
如果想让线程能够执行,可以在创建线程时将第3个参数设为TRUE,或者在创建完成后调用

1     SetEvent(g_hEvent);

程序的确可以实现买票了,但是有些时候,会打印出某个线程卖出第0张票的情况,这是因为当人工重置的事件对象得到通知时,等待该对象的所有线程均可变为可调度线程,两个线程同时运行,线程的同步失败了。

也许有人会想到,在线程得到CPU之后,能否使用ResetEvent是得线程将事件对象设为无信号状态,然后当所保护的代码运行完成后,再将事件对象设为有信号状态?我们可以试试:

 1 //线程1入口函数
 2 DWORD WINAPI Thread1Proc(  LPVOID lpParameter)
 3 {
 4     while(TRUE)
 5     {
 6         WaitForSingleObject(g_hEvent,INFINITE);
 7         ResetEvent(g_hEvent);
 8         if(tickets > 0)
 9         {
10             Sleep(10);
11             printf("thread1 sell ticket : %d\n",tickets--);
12             SetEvent(g_hEvent);
13         }
14         else
15         {
16             SetEvent(g_hEvent);
17             break;
18         }
19     }
20
21     return 0;
22 }

线程2的类似,这里就省略了。运行程序,发现依然会出现卖出第0张票的情况。这是为什么呢?我们仔细思考一下:单核CPU下,可能线程1执行完WaitForSingleObject,还没来得及执行ResetEvent时,就切换到线程2了,此时,由于线程1并没有执行ResetEvent,所以线程2也可以得到事件对象了。而在多CPU平台下,假如两个线程同时执行,则有可能都执行到本应被保护的代码区域。
所以,为了实现线程间的同步,不应该使用人工重置的事件对象,而应该使用自动重置的事件对象:

1     hThread2 = CreateThread(NULL,0,Thread2Proc,NULL0,NULL);

并将原来写的ResetEvent和SetEvent全都注释起来。我们发现程序只打印了一次买票过程。我们分析一下原因:
当一个自动重置的事件得到通知后,等待该该事件的线程中只有一个变为可调度线程。在这里,线程1变为可调度线程后,操作系统将事件设为了无信号状态,当线程1休眠时,所以线程2只能等待,时间片结束以后,又轮到线程1运行,输出thread1 sell ticket :100。然后循环,又去WaitForSingleObject,而此时事件对象处于无信号状态,所以线程不能继续往下执行,只能一直等待,等到自己时间片结束,直到主线程睡醒了,结束整个程序。
正确的使用方法是:当访问完对保护的代码段后,立即调用SetEvent将其设为有信号状态。允许其他等待该对象的线程变为可调度状态:

 1 DWORD WINAPI Thread1Proc(  LPVOID lpParameter)
 2 {
 3     while(TRUE)
 4     {
 5         WaitForSingleObject(g_hEvent,INFINITE);
 6         if(tickets > 0)
 7         {
 8             Sleep(10);
 9             printf("thread1 sell ticket : %d\n",tickets--);
10             SetEvent(g_hEvent);
11         }
12         else
13         {
14             SetEvent(g_hEvent);
15             break;
16         }
17     }
18
19     return 0;
20 }

总结一下:事件对象要区分人工重置事件还是自动重置事件。如果是人工重置的事件对象得到通知,则等待该事件对象的所有线程均变为可调度线程;当一个自动重置的事件对象得到通知时,只有一个等待该事件对象的线程变为可调度线程,且操作系统会将该事件对象设为无信号状态。因此,当执行完受保护的代码后,需要调用SetEvent将事件对象设为有信号状态。

下面介绍另一种线程同步的方法:关键代码段。
关键代码段又称为临界区,工作在用户方式下。它是一小段代码,但是在代码执行之前,必须独占某些资源的访问权限。
我们先介绍与之先关的API函数:
使用InitializeCriticalSection初始化关键代码段
使用EnterCriticalSection进入关键代码段:
使用LeaveCriticalSection离开关键代码段:
使用DeleteCriticalSection删除关键代码段,释放资源
我们看一个例子:

 1 #include <Windows.h>
 2 #include <stdio.h>
 3
 4 //线程函数声明
 5 DWORD WINAPI Thread1Proc(  LPVOID lpParameter);
 6 DWORD WINAPI Thread2Proc(  LPVOID lpParameter);
 7
 8 //全局变量
 9 int tickets = 100;
10 CRITICAL_SECTION g_cs;
11
12 int main()
13 {
14     HANDLE hThread1;
15     HANDLE hThread2;
16     //初始化关键代码段
17     InitializeCriticalSection(&g_cs);
18     //创建线程1
19     hThread1 = CreateThread(NULL,        //默认安全级别
20                             0,            //默认栈大小
21                             Thread1Proc,//线程函数
22                             NULL,        //函数没有参数
23                             0,            //创建后直接运行
24                             NULL);        //线程标识,不需要
25
26     //创建线程2
27     hThread2 = CreateThread(NULL,        //默认安全级别
28                             0,            //默认栈大小
29                             Thread2Proc,//线程函数
30                             NULL,        //函数没有参数
31                             0,            //创建后直接运行
32                             NULL);        //线程标识,不需要
33
34
35     //主线程休眠4秒
36     Sleep(4000);
37     //关闭线程句柄
38     CloseHandle(hThread1);
39     CloseHandle(hThread2);
40     //关闭事件对象句柄
41     DeleteCriticalSection(&g_cs);
42     return 0;
43 }
44
45 //线程1入口函数
46 DWORD WINAPI Thread1Proc(  LPVOID lpParameter)
47 {
48     while(TRUE)
49     {
50         //进入关键代码段前调用该函数判断否能得到临界区的使用权
51         EnterCriticalSection(&g_cs);
52         Sleep(1);
53         if(tickets > 0)
54         {
55             Sleep(1);
56             printf("thread1 sell ticket : %d\n",tickets--);
57             //访问结束后释放临界区对象的使用权
58             LeaveCriticalSection(&g_cs);
59             Sleep(1);
60         }
61         else
62         {
63             LeaveCriticalSection(&g_cs);
64             break;
65         }
66     }
67
68     return 0;
69 }
70
71 //线程2入口函数
72 DWORD WINAPI Thread2Proc(  LPVOID lpParameter)
73 {
74     while(TRUE)
75     {
76         //进入关键代码段前调用该函数判断否能得到临界区的使用权
77         EnterCriticalSection(&g_cs);
78         Sleep(1);
79         if(tickets > 0)
80         {
81             Sleep(1);
82             printf("thread2 sell ticket : %d\n",tickets--);
83             //访问结束后释放临界区对象的使用权
84             LeaveCriticalSection(&g_cs);
85             Sleep(1);
86         }
87         else
88         {
89             LeaveCriticalSection(&g_cs);
90             break;
91         }
92     }
93
94     return 0;
95 }

在这个例子中,通过在放弃临界区资源后,立即睡眠引起另一个线程被调用,导致两个线程交替售票。
下面看一个多线程程序中常犯的一个错误-线程死锁。死锁产生的原因,举个例子:线程1拥有临界区资源A,正在等待临界区资源B;而线程2拥有临界区资源B,正在等待临界区资源A。它俩各不相让,结果谁也执行不了。我们看看程序:

  1 #include <Windows.h>
  2 #include <stdio.h>
  3
  4 //线程函数声明
  5 DWORD WINAPI Thread1Proc(  LPVOID lpParameter);
  6 DWORD WINAPI Thread2Proc(  LPVOID lpParameter);
  7
  8 //全局变量
  9 int tickets = 100;
 10 CRITICAL_SECTION g_csA;
 11 CRITICAL_SECTION g_csB;
 12 int main()
 13 {
 14     HANDLE hThread1;
 15     HANDLE hThread2;
 16     //初始化关键代码段
 17     InitializeCriticalSection(&g_csA);
 18     InitializeCriticalSection(&g_csB);
 19     //创建线程1
 20     hThread1 = CreateThread(NULL,        //默认安全级别
 21                             0,            //默认栈大小
 22                             Thread1Proc,//线程函数
 23                             NULL,        //函数没有参数
 24                             0,            //创建后直接运行
 25                             NULL);        //线程标识,不需要
 26
 27     //创建线程2
 28     hThread2 = CreateThread(NULL,        //默认安全级别
 29                             0,            //默认栈大小
 30                             Thread2Proc,//线程函数
 31                             NULL,        //函数没有参数
 32                             0,            //创建后直接运行
 33                             NULL);        //线程标识,不需要
 34     //关闭线程句柄
 35     //当不再引用这个句柄时,立即将其关闭,减少其引用计数
 36     CloseHandle(hThread1);
 37     CloseHandle(hThread2);
 38
 39     //主线程休眠4秒
 40     Sleep(4000);
 41
 42     //关闭事件对象句柄
 43     DeleteCriticalSection(&g_csA);
 44     DeleteCriticalSection(&g_csB);
 45     return 0;
 46 }
 47
 48 //线程1入口函数
 49 DWORD WINAPI Thread1Proc(  LPVOID lpParameter)
 50 {
 51     while(TRUE)
 52     {
 53         EnterCriticalSection(&g_csA);
 54         Sleep(1);
 55         EnterCriticalSection(&g_csB);
 56         if(tickets > 0)
 57         {
 58             Sleep(1);
 59             printf("thread1 sell ticket : %d\n",tickets--);
 60             LeaveCriticalSection(&g_csB);
 61             LeaveCriticalSection(&g_csA);
 62             Sleep(1);
 63         }
 64         else
 65         {
 66             LeaveCriticalSection(&g_csB);
 67             LeaveCriticalSection(&g_csA);
 68             break;
 69         }
 70     }
 71
 72     return 0;
 73 }
 74
 75 //线程2入口函数
 76 DWORD WINAPI Thread2Proc(  LPVOID lpParameter)
 77 {
 78     while(TRUE)
 79     {
 80         EnterCriticalSection(&g_csB);
 81         Sleep(1);
 82         EnterCriticalSection(&g_csA);
 83         if(tickets > 0)
 84         {
 85             Sleep(1);
 86             printf("thread2 sell ticket : %d\n",tickets--);
 87             LeaveCriticalSection(&g_csA);
 88             LeaveCriticalSection(&g_csB);
 89             Sleep(1);
 90         }
 91         else
 92         {
 93             LeaveCriticalSection(&g_csA);
 94             LeaveCriticalSection(&g_csB);
 95             break;
 96         }
 97     }
 98
 99     return 0;
100 }

在程序中,创建了两个临界区对象g_csA和g_csB。线程1中先尝试获取g_csA,获取成功后休眠,线程2尝试获取g_csB,成功后休眠,切换回线程1,然后线程1试图获取g_csB,因为g_csB已经被线程2获取,所以它线程1的获取不会成功,一直等待,直到自己的时间片结束后,转到线程2,线程2获取g_csB后,试图获取g_csA,当然也不会成功,转回线程1……这样交替等待,直到主线程睡醒,执行完毕,程序结束。

时间: 2024-10-11 14:32:05

VC++ 线程同步 总结的相关文章

VC++线程同步(三) 临界区使用例子

临界区(Crtical Section)同步对象 用户模式下的同步对象 Win32中,最容易使用的一个同步机制就是(关键段)Critical Section, 某些共享资源具有互斥性,也就是它要求被互斥地使用,他也是用于资源的互斥, 在大部分情况下,使用临界区替换Mutex(Mutex是内核模式下的同步对象). 局限性:他只能用于同步单个进程中的线程. 在任何同步机制当中,无论是哪个操作系统下,都不要 长时间的锁住资源,如果一直锁定资源,就会一致阻止其他线程的执行, 使整个程序,处于完全停止的状

VC++线程同步(二) Mutex互斥量的例子

同步对象使用实例 Win32窗口的建立: 我们将要学习的使用,分别是:互斥量,临界区,事件,信号量.所以我们需要一个窗口,呈现四种四种同步对象状态. 首先创建一个Win32项目,不要选空项目; 我们需要四个小窗口,先找到注册主窗口的代码. ATOM MyRegisterClass(HINSTANCE hInstance) { WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style= CS_HREDRAW | CS_VREDRAW

VC++线程同步(四) 事件使用例子

事件(Event)同步对象 (内核级别)事件内核对象包含: 1 一个使用计数器 2 一个表示事件是否是自动重置还是手动重置的布尔值 3 一个表示事件有没有被触发的布尔值 4 当触发为true时,等待该事件的线程变为可调度状态 5 事件的触发表示一个操作已经完成 作用: 通知其他线程,我已经完成读写操作了,轮到你们来做了. 他分为两种类型: 1是手动重置事件,也就是要进行手动的触发和非触发状态的切换. 2是自动重置事件,这种情况下只需要设置触发事件,不用管什么时候切换触发状态. 尽量使用手动重置方

VC++线程同步(五) 信号量使用例子

信号量(Semaphore) 信号量是内核对象,使用几个形象的例子,进行描述. 1 假设有5个位置,而外面有很多人要进来,那么当5个位置被人占用了 后,其他人就必须排队等待,每个人使用时间不同,5个占用的位置,其中有两个完成了,那么,排队的人中,最前面的两个人进行可以使用,但是最多就是5个人同时能够使用,这就是信号量. 2 例如我们在服务器中创建了一个线程池,它由5个线程组成,也就意味着,最多同时处理5个请求,一旦超过5个,那么请求就放入缓冲中,当一个或多个请求(最多5个)完成后,那么从缓冲中拿

VC++多线程编程-线程间的通信和线程同步

引用:http://blog.csdn.net/zjc0888/article/details/7372258 线程间通讯 一般而言,应用程序中的一个次要线程总是为主线程执行特定的任务,这样,主线程和次要线程间必定有一个信息传递的渠道,也就是主线程和次要线程间要进行通信.这种线程间的通信不但是难以避免的,而且在多线程编程中也是复杂和频繁的,下面将进行说明. 使用全局变量进行通信 由于属于同一个进程的各个线程共享操作系统分配该进程的资源,故解决线程间通信最简单的一种方法是使用全局变量.对于标准类型

VC++深入详解——16章:线程同步,事件对象

这章介绍另外:事件对象和关键代码段. 进程相关函数: CreateEvent函数: 第一个参数:安全属性,默认的安全属性为NULL 第二个参数:复位方式, 人工设置为TRUE,自动设置为FALSE, 当为人工设置时,等待事件的线程时,需要resetevent函数来设置其为无型号状态. 第三个参数:初始状态:TRUE为有信号状态,FALSE为无信号状态. 第四个参数:对象名称,NULL为匿名名称. 创建或打开一个命名或匿名的事件对象(也属于内核对象) 返回:返回的是事件对象的句柄. SetEven

[.net]基元线程同步构造

1 /* 基元线程同步构造 2 用户模式构造: 3 易变构造(Volatile Construct) 4 互锁构造(Interlocked Construct):自旋锁(Spinlock) 乐观锁(Optimistic Concurrency Control,乐观并发控制) 5 内核模式构造: 6 事件构造(Event) 7 信号量构造(Semaphore) 8 互斥体构造(Mutex) 9 */ 10 11 //易变构造,Volatile.Write()之前的所有字段写入操作,必须再该方法调用

iOS多线程编程:线程同步总结 NSCondtion

1:原子操作 - OSAtomic系列函数 iOS平台下的原子操作函数都以OSAtomic开头,使用时需要包含头文件<libkern/OSBase.h>.不同线程如果通过原子操作函数对同一变量进行操作,可以保证一个线程的操作不会影响到其他线程内对此变量的操作,因为这些操作都是原子式的.因为原子操作只能对内置类型进行操作,所以原子操作能够同步的线程只能位于同一个进程的地址空间内. 2:锁 - NSLock系列对象 iOS平台下的锁对象为NSLock对象,进入锁通过调用lock函数,解锁调用unl

线程同步之EVENT

事件可传信给其他线程,表示某些条件现在已具备,比如有可用的消息. 事件可分为手动复位和自动复位,前者可传信给许多同时等待事件的线程而且可以被复位. 自动复位的事件传信给单个等待时间的线程,该事件会自动复位. Applications can use event objects in a number of situations to notify a waiting thread of the occurrence of an event. For example, overlapped I/O