Windows中的多线程程序设计一直是所有编程人员感到困难的一个地方,主要是在Windows多线程程序设计中,往往要考虑很多的东西,线程启用的多少,线程之间的同步问题等等...下面通过模拟火车售票系统来进行讲解
#include <windows.h>
#include <iostream>
using namespace std ;
DWORD WINAPI ThreadProc1(LPVOID lpParameter) ;
DWORD WINAPI ThreadProc2(LPVOID lpParameter) ;
int index ;
int tickets = 100 ;
HANDLE hMutex ; // 声明一个全局的互斥对象句柄
int main()
{
HANDLE hThread1 ;
HANDLE hThread2 ;
hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL) ;
hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL) ;
CloseHandle(hThread1) ; // 关闭线程内核对象句柄, 并没有阻止新建线程的结束, 但是将线程内核对象的引用计数减1,
CloseHandle(hThread2) ; // 当新建线程执行完成后, 引用计数也会减1。 当引用计数为0时, 系统就会释放线程内核对象
// 如果此处不使用CloseHandle()函数关闭新建线程内核对象, 则在该线程执行完成后, 其引用计数也不会为0,
// 则该线程内核对象会一直存在在系统内核当中, 所以, 在我们创建了一个新线程之后, 如果后面的代码不会
// 用到此线程内核对象, 则应该调用CloseHandle()函数关闭该线程的线程内核对象
hMutex = CreateMutex(NULL, FALSE, NULL) ; // 创建一个不属于当前线程的匿名互斥对象
Sleep(4000) ;
return 0 ;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(hMutex, INFINITE) ;
if (tickets > 0)
{
Sleep(1) ;
cout << "thread1 sell ticket: " << tickets-- << endl ;
}
else
break ;
ReleaseMutex(hMutex) ; // 释放hMutex互斥对象的所有权
}
return 0 ;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(hMutex, INFINITE) ;
if (tickets > 0)
{
Sleep(1) ;
cout << "thread2 sell ticket: " << tickets-- << endl ;
}
else
break ;
ReleaseMutex(hMutex) ;
}
return 0 ;
}
运行之后,结果如下:
我们可以看到线程一和线程二之间交替的执行,直到将票卖完。 那么这样的结果,肯定是缺少不了线程之间的同步,那么Windows程序中,线程之间是如何维持线程直接的同步的呢?? 这也主要靠windows操作系统为我们提供的一个内核对象 ------> (互斥对象) 来实现的。下面我们将主要分析程序是如何执行的;
分析:
当我们创建互斥对象(hMutex)时(上述代码中的第26行), 第二个参数传递为FALSE,那么当前就没有线程拥有该互斥对象(hMutex)的所有权,那么操作系统就会将该互斥对象(hMutex)设置为已通知状态,也就是信号态;当程序执行到第一个线程(ThreadProc1)的WaitForSingleObject()函数时,因为此时的互斥对象(hMutex)处于信号态,那么该线程(ThreadProc1)就请求获得了该互斥对象(hMutex)的所有权,那么操作系统就会将该互斥对象(hMutex)的线程ID设置成线程一(ThreadProc1)的线程ID,此时,线程一(ThreadProc1)便拥有了该互斥对象(hMutex),同时,操作系统立即将该互斥对象(hMutex)设置为未通知状态(即非信号态),程序继续向下运行,执行到Sleep()函数时,线程一(ThreadProc1)休眠,这时,操作系统便会选择线程二(ThreadProc2)开始运行,同样执行到WaitForSingleObject()函数,因为此时互斥对象(hMutex)被线程一(ThreadProc1)所拥有,处于未通知状态(即非信号态),这样线程二(ThreadProc2)便不能获得该互斥对象(hMutex)的所有权,WaitForSingleObject()函数便处于等待状态,直到线程1(ThreadProc1)休眠结束,继续执行线程一(ThreadProc1)中的后续代码(即线程一(ThreadProc1)中Sleep()函数后的代码),线程一(ThreadProc1)的工作完成后,我们便要释放线程一(ThreadProc1)对该互斥对象(hMutex)的所有权(即ReleaseMutex()的操作),当我们调用ReleaseMutex()函数时, 操作系统便会将该互斥对象(hMutex)的线程ID置为0并且将该互斥对象(hMutex)设置为已通知状态(即信号态),而此时处于等待状态的线程二(ThreadProc2)便立即请求到该互斥对象(hMutex)的所有权,同时操作系统立即会将互斥对象(hMutex)的线程ID设置为线程二的线程ID并且将其(hMutex)状态转变为未通知状态(即非信号态),线程二(ThreadProc2)中的相关代码继续向下执行,当线程二(ThreadProc2)的工作完成后, 也会调用ReleaseMutex()函数释放线程二(ThreadProc2)对互斥对象(hMutex)的所有权 ----> 这样程序会一直在线程一(ThreadProc1)和线程二(ThreadProc2)之间交替的运行直到数据的处理完毕为止。而且,这样做还能保持线程之间的同步,对数据的处理会更加的安全。
现在应该对互斥对象以及windows是如何通过互斥对象来实现多个线程之间的同步的。
我们知道互斥对象使操作系统维护的一个数据结构,那么该数据结构都包含什么东西呢??
* 互斥对象
-- 互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。-- 互斥对象包含一个使用数量,一个线程ID和一个计数器。(如果该互斥对象属于那个线程,则该互斥对象的线程ID便设置为拥有者线程的线程ID)
-- ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。
可以看到互斥对象除了一个线程ID还有一个计数器,那么,我们再来看看计数器是怎么回事以及它是如何运作的,在此之前,我们先来看一段代码,如下:
MultiThread2.cpp
#include <windows.h>
#include <iostream>
using namespace std ;
DWORD WINAPI ThreadProc1(LPVOID lpParameter) ;
DWORD WINAPI ThreadProc2(LPVOID lpParameter) ;
int index ;
int tickets = 100 ;
HANDLE hMutex ; // 声明一个全局的互斥对象句柄
int main()
{
HANDLE hThread1 ;
HANDLE hThread2 ;
hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL) ;
hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL) ;
CloseHandle(hThread1) ; // 关闭线程内核对象句柄, 并没有阻止新建线程的结束, 但是将线程内核对象的引用计数减1,
CloseHandle(hThread2) ; // 当新建线程执行完成后, 引用计数也会减1。 当引用计数为0时, 系统就会释放线程内核对象
// 如果此处不使用CloseHandle()函数关闭新建线程内核对象, 则在该线程执行完成后, 其引用计数也不会为0,
// 则该线程内核对象会一直存在在系统内核当中, 所以, 在我们创建了一个新线程之后, 如果后面的代码不会
// 用到此线程内核对象, 则应该调用CloseHandle()函数关闭该线程的线程内核对象
// hMutex = CreateMutex(NULL, FALSE, NULL) ; // 创建一个不属于当前线程的匿名互斥对象
hMutex = CreateMutex(NULL, TRUE, NULL) ; // 创建一个属于当前线程的匿名互斥对象
ReleaseMutex(hMutex) ; // 释放当前线程对互斥对象的所有权
Sleep(4000) ;
return 0 ;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(hMutex, INFINITE) ;
if (tickets > 0)
{
Sleep(1) ;
cout << "thread1 sell ticket: " << tickets-- << endl ;
}
else
break ;
ReleaseMutex(hMutex) ; // 释放hMutex互斥对象的所有权
}
return 0 ;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(hMutex, INFINITE) ;
if (tickets > 0)
{
Sleep(1) ;
cout << "thread2 sell ticket: " << tickets-- << endl ;
}
else
break ;
ReleaseMutex(hMutex) ;
}
return 0 ;
}
和MultiThread1.cpp的代码差不多,只是略微的修改了一下,将MultiThread1.cpp代码中的第26行注释掉,在第28行重新创建一个互斥对象(注意这两行(26和28)代码的区别,并且在29行又多添加了一行代码,【关于这两行代码的意义,在程序的注释中写的很详细】我们运行程序,看看结果,如下:
程序也是能够正常的执行,分析可以结合MultiThread1.cpp程序的分析,自己研究。
我们继续向下,MultiThread2.cpp程序也是能够正常的执行,但是如果我们在MultiThread2.cpp程序的第28和第29行之间加上一行代码,完整代码如下:
MultiThread3.cpp
#include <windows.h>
#include <iostream>
using namespace std ;
DWORD WINAPI ThreadProc1(LPVOID lpParameter) ;
DWORD WINAPI ThreadProc2(LPVOID lpParameter) ;
int index ;
int tickets = 100 ;
HANDLE hMutex ; // 声明一个全局的互斥对象句柄
int main()
{
HANDLE hThread1 ;
HANDLE hThread2 ;
hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL) ;
hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL) ;
CloseHandle(hThread1) ; // 关闭线程内核对象句柄, 并没有阻止新建线程的结束, 但是将线程内核对象的引用计数减1,
CloseHandle(hThread2) ; // 当新建线程执行完成后, 引用计数也会减1。 当引用计数为0时, 系统就会释放线程内核对象
// 如果此处不使用CloseHandle()函数关闭新建线程内核对象, 则在该线程执行完成后, 其引用计数也不会为0,
// 则该线程内核对象会一直存在在系统内核当中, 所以, 在我们创建了一个新线程之后, 如果后面的代码不会
// 用到此线程内核对象, 则应该调用CloseHandle()函数关闭该线程的线程内核对象
// hMutex = CreateMutex(NULL, FALSE, NULL) ; // 创建一个不属于当前线程的匿名互斥对象
hMutex = CreateMutex(NULL, TRUE, NULL) ; // 创建一个属于当前线程的匿名互斥对象
WaitForSingleObject(hMutex, INFINITE) ;
ReleaseMutex(hMutex) ; // 释放当前线程对互斥对象的所有权
Sleep(4000) ;
return 0 ;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(hMutex, INFINITE) ;
if (tickets > 0)
{
Sleep(1) ;
cout << "thread1 sell ticket: " << tickets-- << endl ;
}
else
break ;
ReleaseMutex(hMutex) ; // 释放hMutex互斥对象的所有权
}
return 0 ;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(hMutex, INFINITE) ;
if (tickets > 0)
{
Sleep(1) ;
cout << "thread2 sell ticket: " << tickets-- << endl ;
}
else
break ;
ReleaseMutex(hMutex) ;
}
return 0 ;
}
声明:由于我的电脑是多核的,造成第一种结果。对于单核电脑,是只可能出现第二种结果的。
来分析一下程序的执行过程,如下:
代码中的第28行中创建了一个互斥对象(hMutex),并且被当前线程(主线程main)所拥有(因为CreaeMutex()函数的第二个参数为TRUE),之后,程序向下执行到WaitForSingleObject()函数时, 操作系统会去判断拥有的互斥对象(hMutex)的线程(主线程main)的线程ID是否等于请求线程(此处,请求的线程就是主线程main)的线程ID,如果相等,那么即使此时互斥对象(hMutex)处于未通知状态(即非信号态),当前的请求线程(主线程main)也能获得此互斥对象(hMutex)的所有权,那么WaitForSingleObject()函数就会返回。【 对于一个线程多次拥有一个互斥对象是通过互斥对象内部的计数器来记录的】当我们第一次创建互斥对象是(hMutex),主线程拥有了这个互斥对象(hMutex),操作系统除了将该互斥对象(hMutex)的线程ID设置为主线程的线程ID,同时将该互斥对象(hMutex)的计数器变为1,当用WaitForSingleObject()函数去请求该互斥对象(hMutex)时,而此时的互斥对象(hMutex)是处于未通知状态(即非信号态)的,但是此时请求互斥对象(hMutex)的线程ID等于该互斥对象(hMutex)中的线程ID,所以我们仍能够请求到该互斥对象,那么操作系统通过互斥对象中的计数器来记录我们请求了多少次互斥对象,于是,此时该互斥对象(hMutex)在加1变为2,当我们在调用ReleaseMutex()函数时,实际上是将互斥对象内部的计数器进行减1操作,当程序中的互斥对象(hMutex)中的计数器减1操作后,其计数器的值还剩1,这样主线程还是没有失去对该互斥对象(hMutex)的的所有权,那么,该互斥对象(hMutex)还是处于未通知状态,所以,线程一和线程二就不能请求到对该互斥对象(hMetex)的所有权, 故线程一和线程二中的代码就不会被执行。
如果想要线程一和线程二中的代码被执行,我们可以在main()函数中再次调用ReleaseMutex()函数,将互斥对象(hMetex)的的计数器递减为0。
我测试程序是可以正常的执行的,这也和我们根据分析得到的结果一致,是不是很高兴啊 !!! 具体的代码就不贴出来了,程序运行的结果和MultThread1.cpp运行的结果一样。有兴趣的话,大家可以自己试一试 !!!