您的位置:首页 > 其它

Windows多线程程序设计之线程同步分析(结合事件对象)(上)

2013-06-25 19:04 555 查看
声明: 关于这几篇《Windows多线程程序设计之线程同步分析(结合xx对象)》的博文中的程序, 大家最好能在一台单核CPU中进行测试, 本人使用的双核CPU在测试这些程序的过程中出现了各种各样的不同的问题, 但并不影响我们对多线程程序实现原理的理解, 出现的问题, 由于本人能力有限, 并没能给出相关的解释, 所以也就没有将问题写出来, 希望日后我能够找出这些问题的原因, 此系列的文章仅供参考, 文中如有不对的地方请各位读者给以明示, Thank you!!!

先来看代码吧, 如下:

ThreadEvent1.cpp

#include <windows.h>
#include <iostream>

using namespace std ;

DWORD WINAPI ThreadProc1(LPVOID lpParameter) ;
DWORD WINAPI ThreadProc2(LPVOID lpParameter) ;

int tickets = 100 ;
HANDLE g_hEvent ;

int main()
{
HANDLE hThread1 ;
HANDLE hThread2 ;

hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL) ;
hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL) ;

CloseHandle(hThread1) ;
CloseHandle(hThread2) ;

g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL) ;	// 创建一个匿名的手动重置的事件对象, 并且将初始化状态设置为非信号态

Sleep(4000) ;

return 0 ;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE) ;
if (tickets > 0)
{
Sleep(1) ;
cout << "thread sell ticket: " << tickets-- << endl ;
}
else
{
break ;
}
}

return 0 ;
}

DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE) ;
if (tickets > 0)
{
Sleep(1) ;
cout << "thread two sell ticket: " << tickets-- << endl ;
}
else
{
break ;
}
}

return 0 ;
}
先来看看程序的执行结果, 如下:



分析:

本程序中我们创建了一个事件对象g_hEvent(代码中的第23行)以及两个线程(线程一ThreadProc1和线程二ThreadProc2), 然后在两个线程中分别调用WaitForSingleObject()函数去请求该事件对象(g_hEvent), 一旦某个线程请求到了该事件对象(g_hEvent), 则就可进入到该线程中受保护的代码段中, 但是我们看到程序并没有正常的执行, 这是为什么呢?? 我们再来分析代码, 注意到代码中创建事件对象的那一行代码(第23行代码),
我们将CreateEvent()函数的第二个参数设置为TRUE,而第三个参数设置为FALSE, 第二个参数为TRUE表示新创建的事件对象(g_hEvent)是人工重置, 第三个参数传递为FALSE, 表示此时创建的事件对象的状态是非信号态,这样, 在线程一和线程二中尽管我们调用WaitForSingleObject()函数去请求事件对象, 但是, 因为事件对象一直处于非信号态, 导致程序阻塞在请求事件对象的代码处, 线程一和线程二一直在请求着事件对象, 所以程序也就不能正常的执行下去。解决上面的问题其实很简单,
就是因为初始创建的事件对象的状态为非信号态, 所以, 线程一和线程二不能够请求到事件对象, 因此 我们可以重置事件对象的状态为信号态, 以让线程一和线程而能够正常的请求到事件对象。而刚才我们说到CreateEvent()函数的第二个参数为TRUE, 表示新创建的事件对象(g_hEvent)是人工重置, 而对于人工重置的事件对象, 我们要想改变其状态, 就必须通过调用SetEvent()函数, 将事件对象(g_hEvent)设置为信号态, 修改后的代码如下:

ThreadEvent2.cpp

#include <windows.h>
#include <iostream>

using namespace std ;

DWORD WINAPI ThreadProc1(LPVOID lpParameter) ;
DWORD WINAPI ThreadProc2(LPVOID lpParameter) ;

int tickets = 100 ;
HANDLE g_hEvent ;

int main()
{
HANDLE hThread1 ;
HANDLE hThread2 ;

hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL) ;
hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL) ;

CloseHandle(hThread1) ;
CloseHandle(hThread2) ;

g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL) ;	// 创建一个匿名的手动重置的事件对象, 并且将初始化状态设置为非信号态

SetEvent(g_hEvent) ;		// 设置事件对象的状态为信号态

Sleep(4000) ;

return 0 ;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE) ;
if (tickets > 0)
{
Sleep(1) ;
cout << "thread sell ticket: " << tickets-- << endl ;
}
else
{
break ;
}
}

return 0 ;
}

DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE) ;
if (tickets > 0)
{
Sleep(1) ;
cout << "thread two sell ticket: " << tickets-- << endl ;
}
else
{
break ;
}
}

return 0 ;
}
注意到代码中的第25行, 我们调用SetEvent()函数将刚刚创建的匿名事件对象(g_hEvent)设置为了信号态度。现在再来看看程序运行的结果, 如下所示:



可以看到程序打印出来的很乱, 并没能达到线程间同步的效果, 这是为什么呢?

分析:

还是来看代码中的第23行, 在对ThreadEvent1.cpp中代码进行分析时, 我们知道, 我们新创建的匿名事件对象(g_hEvent)为人工重置的事件对象, 在本代码中(ThreadEvent2.cpp), 我们通过调用函数SetEvent()将该事件对象(g_hEvent)设置为信号态, 而当人工重置的事件对象得到通知时(即变为信号态), 等待该事件对象的所有线程均变为可调度线程。即在本程序(ThreadEvent2.cpp)中线程一、线程二均变为可调度的线程,
两个线程同时运行, 那么, 对于线程一和线程二中受保护的代码就可以同时的被执行, 也就导致了上面commend命令行中输出的结果。线程一和线程二能够同时的运行起来, 表示了我们进行线程间同步失败。此时, 要想解决这个问题, 我们还是来分析代码以找到解决的方法, 通过分析代码, 有人可能会想, 既然通过手动将事件对象设置为信号态(即通过调用SetEvent()函数实现), 那么线程一和线程二都会去请求该事件对象(g_hEvent), 而对于单核CPU来说, 肯定只有一个线程请求到了该事件对象(g_hEvent),
那么当我们让请求事件对象的线程请求得到事件对象之后, 调用函数ResetEvent()将事件对象设置为非信号态, 这样如果第二个线程去请求事件对象, 便不能请求到, 达到了某个时间段内只有一个线程运行的效果, 在运行线程的受保护代码段执行完毕之后, 我们在调用函数SetEvent()将事件对象设置为信号态, 而处于等待状态的另一个线程便会立即请求得到事件对象, 同样的, 请求到之后, 调用ResetEvent()函数将事件对象设置为非信号态, 在受保护代码执行完成后, 调用SetEvent()函数将事件对象的状态改为信号态,
这样就达到了一个时间段内只有一个在运行的线程, 根据这种想法, 我们得到更改后的代码, 如下:

ThreadEvent3.cpp

#include <windows.h>
#include <iostream>

using namespace std ;

DWORD WINAPI ThreadProc1(LPVOID lpParameter) ;
DWORD WINAPI ThreadProc2(LPVOID lpParameter) ;

int tickets = 100 ;
HANDLE g_hEvent ;

int main()
{
HANDLE hThread1 ;
HANDLE hThread2 ;

hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL) ;
hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL) ;

CloseHandle(hThread1) ;
CloseHandle(hThread2) ;

g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL) ;	// 创建一个匿名的手动重置的事件对象, 并且将初始化状态设置为非信号态

SetEvent(g_hEvent) ;				// 设置事件对象的状态为信号态

Sleep(4000) ;

return 0 ;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE) ;

ResetEvent(g_hEvent) ;			// 设置事件对象的状态为非信号态

if (tickets > 0)
{
Sleep(1) ;
cout << "thread sell ticket: " << tickets-- << endl ;
}
else
{
break ;
}

SetEvent(g_hEvent) ;			// 设置事件对象的状态为信号态
}

return 0 ;
}

DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE) ;

ResetEvent(g_hEvent) ;			// 设置事件对象的状态为非信号态

if (tickets > 0)
{
Sleep(1) ;
cout << "thread two sell ticket: " << tickets-- << endl ;
}
else
{
break ;
}

SetEvent(g_hEvent) ;			// 设置事件对象的状态为信号态
}

return 0 ;
}
注意到我们在线程一和线程二中加入了相关的代码(线程一中的第38和50行, 线程二中的62和74行), 现在我们在来看看程序的执行效果, 如下:



效果并不是预期的那样, 这是为什么呢?来看看我们下面的分析;

分析:

我们都知道多线程程序的执行是按照CPU的时间片来划分的, 如果一个线程分的的时间片完了, 操作系统就会自动切换到另一个线程中执行, 同理, 我们来分析上面的代码(ThreadEvent3.cpp), 当程序执行到线程一的WaitForSingleObject()函数并且执行完了该行代码(代码中的36行), 即线程一已经请求到了事件对象(g_hEvent),此时, 它(线程一)的CPU时间片到了, 操作系统就切换到线程二中去执行, 而此时线程一中用于设置事件对象(g_hEvent)到非信号态的ResetEvent()函数并没有执行,
事件对象(g_hEvent)还是处于信号态, 所以, 线程二就会顺顺利利的请求到事件对象(g_hEvent), 此时的程序中, 线程一和线程二都请求到了事件对象(g_hEvent), 我们原本的一个时间段只能有一个线程拥有事件对象便失败了, 这也就造成了程序数据如图中所示的结果。

所以对于线程同步我们不能通过创建手工重置的事件对象来实现, 我们必须的创建自动重置的线程来实现;

更改后的代码如下:

ThreadEvent4.cpp

#include <windows.h>
#include <iostream>

using namespace std ;

DWORD WINAPI ThreadProc1(LPVOID lpParameter) ;
DWORD WINAPI ThreadProc2(LPVOID lpParameter) ;

int tickets = 100 ;
HANDLE g_hEvent ;

int main()
{
HANDLE hThread1 ;
HANDLE hThread2 ;

hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL) ;
hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL) ;

CloseHandle(hThread1) ;
CloseHandle(hThread2) ;

g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL) ;	// 创建一个匿名的自动重置的事件对象, 并且将初始化状态设置为非信号态

SetEvent(g_hEvent) ;				// 设置事件对象的状态为信号态

Sleep(4000) ;

return 0 ;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE) ;

if (tickets > 0)
{
Sleep(1) ;
cout << "thread sell ticket: " << tickets-- << endl ;
}
else
{
break ;
}
}

return 0 ;
}

DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE) ;

if (tickets > 0)
{
Sleep(1) ;
cout << "thread two sell ticket: " << tickets-- << endl ;
}
else
{
break ;
}
}

return 0 ;
}
运行程序后, 程序的执行结果如下:



看到这种结果, 我们肯定会说why?我也会这么问的,来我们看看分析,如下:

分析:

当一个自动重置的事件对象得到通知时(变为信号态), 等待该事件对象的线程中只有一个线程变为可调度线程。(我的计算机是双核的, 可能输出的结果和单核的不太一样, 在单核CPU中, 此处只能有一个线程被执行, 至于双核的CPU执行的结果为什么是这样的, 大家多想一想也应该能想明白, 但是对于最后的线程同步, 我们肯定能保证, 一个时刻只有一个线程在运行) 在本程序(ThreadEvent4.cpp)中, 程序执行后肯定会有其中的一个线程被调度, 不管那个被调度, 在执行线程时, 都会调用WaitForSingleObject()函数去请求事件对象(g_hEvent),
当请求到事件对象(g_hEvent)后, 操作系统就会自动将事件对象设置为非信号态(因为是自动重置嘛), 当执行着的线程执行到Sleep()函数时, 运行线程休眠, 此时, 操作系统会切换到另一线程开始执行, 另一个线程在执行过程中也会请求事件对象(g_hEvent), 但是, 事件对象(g_hEvent)一直是处于非信号态的, 所以, 请求不到事件对象, 只能暂停下来等待, 而休眠中的线程在休眠结束后继续执行其线程代码, 打印出相关信息后继续开始下一轮的循环, 在下一轮的循环中, 该线程(处于运行中的线程)还会调用WaitForSingleObject()函数去请求事件对象(g_hEvent),
因为事件对象(g_hEvent)处于非信号态, 导致该线程最终请求不到事件对象(g_hEvent), 这样, 结果, 两个线程都处于等待状态, 主线程(main())继续执行, 程序结束。

这样还是没有达到线程同步的要求啊! 那么怎么实现线程同步呢?更改后的代码如下所示:

ThreadEvent5.cpp

#include <windows.h>
#include <iostream>

using namespace std ;

DWORD WINAPI ThreadProc1(LPVOID lpParameter) ;
DWORD WINAPI ThreadProc2(LPVOID lpParameter) ;

int tickets = 100 ;
HANDLE g_hEvent ;

int main()
{
HANDLE hThread1 ;
HANDLE hThread2 ;

hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL) ;
hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL) ;

CloseHandle(hThread1) ;
CloseHandle(hThread2) ;

g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL) ;	// 创建一个匿名的自动重置的事件对象, 并且将初始化状态设置为非信号态

SetEvent(g_hEvent) ;				// 设置事件对象的状态为信号态

Sleep(4000) ;

CloseHandle(g_hEvent) ;

return 0 ;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE) ;

if (tickets > 0)
{
Sleep(1) ;
cout << "thread one sell ticket: " << tickets-- << endl ;
}
else
{
break ;
}

SetEvent(g_hEvent) ;			// 设置事件对象的状态为信号态
}

return 0 ;
}

DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE) ;

if (tickets > 0)
{
Sleep(1) ;
cout << "thread two sell ticket: " << tickets-- << endl ;
}
else
{
break ;
}

SetEvent(g_hEvent) ;			// 设置事件对象的状态为信号态
}

return 0 ;
}
运行结果如下:



这个就不给分析了, 自己结合上一个程序和分析, 应该是可以想明白的!!!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐