onesimple

windows平台多线程同步之Event的应用

  • 前言
    线程组成
  1. 线程的内核对象,操作系统用来管理该线程的数据结构。
  2. 线程堆栈,它用于维护线程在执行代码时需要的所有参数和局部变量。

  操作系统为每一个运行线程安排一定的CPU时间 —— 时间片。系统通过一种循环的方式为线程提供时间片,线程在自己的时间内运行,多个线程不断地切换运行,因时间片相当短,因此,给用户的感觉,就好像线程是同时运行的一样。
  单cpu计算机一个时间只能运行一个线程,如果计算机拥有多个CPU,线程就能真正意义上同时运行了。
  windows平台下,创建线程可以使用windows api 函数CreateThread来实现,函数声明是:

1
2
3
4
5
6
7
8
9
10
11
WINBASEAPI
HANDLE
WINAPI
CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);

  参数说明:

参数 说明
lpThreadAttributes 线程安全性,使用缺省安全性,一般缺省null
dwStackSize 堆栈大小,0为缺省大小
lpStartAddress 线程要执行的函数指针,即入口函数
lpParameter 线程参数
dwCreationFlags 线程标记,如为0,则创建后立即运行
lpThreadId LPDWORD为返回值类型,一般传递地址去接收线程的标识符,一般设为null

  因为要使用windows api函数,所以包含:

1
#include <windows.h>

  另外需要标准输入输出函数,所以包含:

1
#include <iostream.h>

  • 问题引出

  以多个售票窗口卖同一张火车票为例,定义一个全局的票数tickets,用两个线程来执行卖票,两个线程访问同一个变量tickets,先看一个不正确的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//问题程序
#include <windows.h>
#include <iostream.h>
DWORD WINAPI Fun1Proc(
LPVOID lpParameter
);
DWORD WINAPI Fun2Proc(
LPVOID lpParameter
);
int tickets=100;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
system("pause");
}
DWORD WINAPI Fun1Proc(
LPVOID lpParameter
)
{
while(TRUE)
{
if(tickets>0)
{
Sleep(1);//假定为卖票需要花费的时间
cout<<"thread1 sell ticket : "<<tickets--<<endl;
}
else
break;
}
return 0;
}
DWORD WINAPI Fun2Proc(
LPVOID lpParameter // thread data
)
{
while(TRUE)
{
if(tickets>0)
{
Sleep(1);
cout<<"thread2 sell ticket : "<<tickets--<<endl;
}
else
break;
}
return 0;
}

  线程中sleep(1);表名该线程放弃执行的权利,操作系统会选择另外的线程进行执行。所以执行结果是:

![Alt test]

执行结果




  可以看见程序不是按照预期的效果执行的,tickets的改变是混乱的。所以两个线程访问同一块资源时,需要考虑线程同步问题,即其中一个线程操作改资源时,其他线程不能访问该资源,只能等待,该线程执行结束之后,其他线程才能对该资源进行访问。
  一般采用互斥对象事件对象关键代码段等来实现线程的同步。可参考我的另一篇博文 windows平台多线程同步之Mutex的应用.本节介绍的是事件对象的使用。

  • 事件对象
    特征
      事件对象也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值。
      有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件。
      当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。
      创建事件对象,可以使用api函数CreateEvent,具体声明如下:
1
2
3
4
5
6
7
8
9
WINBASEAPI
HANDLE
WINAPI
CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCWSTR lpName
);

  参数说明:


















lpEventAttributes 安全性,采用null默认安全性。
bManualReset (TRUE)人工重置或(FALSE)自动重置事件对象为非信号状态,若设为人工重置,则当事件为有信号状态时,所有等待的线程都变为可调度线程。
bInitialState 指定事件对象的初始化状态,TRUE:初始为有信号状态。
lpName 事件对象的名字,一般null匿名即可。

  新建一个win32控制台工程。创建一个全局的事件句柄对象,保存创建的事件的句柄。先看这样一个有问题的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <windows.h>
#include <iostream.h>
DWORD WINAPI Fun1Proc(LPVOID lpParameter );
DWORD WINAPI Fun2Proc(LPVOID lpParameter );
int tickets=100;
HANDLE g_hEvent;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
g_hEvent=CreateEvent(NULL,TRUE,FALSE,"tickets");
system("pause");
CloseHandle(g_hEvent);
}
DWORD WINAPI Fun1Proc(LPVOID lpParameter )
{
while(TRUE)
{
WaitForSingleObject(g_hEvent,INFINITE);
ResetEvent(g_hEvent);
if(tickets>0)
{
Sleep(1);
cout<<"thread1 sell ticket : "<<tickets--<<endl;
}
else
break;
SetEvent(g_hEvent);
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(TRUE)
{
WaitForSingleObject(g_hEvent,INFINITE);
ResetEvent(g_hEvent);
if(tickets>0)
{
Sleep(1);
cout<<"thread2 sell ticket : "<<tickets--<<endl;
}
else
break;
SetEvent(g_hEvent);
}
return 0;
}

  初始化中将时间设为手动重置:

1
g_hEvent=CreateEvent(NULL,TRUE,FALSE,"tickets");

  具体思路是:其中一个线程等待到信号之后,重置事件,进行数据处理,处理完成之后再次触发事件。另一个线程收到新的信号,同样处理并重置事件。先来看看执行结果:


执行结果




  通过查看结果可知没能实现线程的同步,因为线程1 WaitForSingleObject等待到事件有信号时,该线程被分配的时间片可能已经用完,此时操作系统执行线程2,正好事件还是有信号状态,所以线程2直接往下执行,导致了读写的混乱。
  如果移植到多cpu平台,线程1和线程2可以同时运行,此时处理的结果就更加不可预料,所以这种同步手段不可用。
  故采用初始化时设置事件对象为自动重置的对象,并触发为有信号,等待该事件的线程只会有一个是有信号状态,该线程执行结束,设置该事件对象为有信号状态,则另外的一个线程可以变为有信号状态。实现代码变为如下:
初始化中将事件设为手动触发且无信号状态,之后触发信号:
1
2
g_hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);
SetEvent(g_hEvent);

  线程中请求到该事件信号,自动将事件置为无信号状态,线程按cpu分配的时间片去执行,等到执行完毕,将事件信号触发为有信号状态,另一个线程此时请求到该信号,进行同样的执行。线程while循环为:

1
2
3
4
5
6
7
8
9
10
11
12
while(TRUE)
{
WaitForSingleObject(g_hEvent,INFINITE);//收到信号,自动重置
if(tickets>0)
{
Sleep(1);
cout<<"thread1 sell ticket : "<<tickets--<<endl;
}
else
break;
SetEvent(g_hEvent);
}

  与mutex对象.不同的是,设为人工重置的事件对象,如果重复SetEvent()设为有信号状态,若想重置只需要执行一次ResetEvent()即可。

  • 创建命名事件对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    g_hEvent=CreateEvent(NULL,TRUE,FALSE,"tickets");
    if(g_hEvent)
    {
    if(ERROR_ALREADY_EXISTS==GetLastError())
    {
    cout<<"only instance can run!"<<endl;
    return;
    }
    }

  命名事件对象的一种应用是:通过命名事件对象,可以保证当前只有一个应用程序实例在运行。
  以上是关于windows平台下多线程同步相关的事件对象的使用问题,之后将对线程同步的关键代码段进行介绍和解析,敬请关注。文中如有谬误,还望不吝赐教。

🐶 五百年雨打的石桥,有你走过 🐶