上一节中说到多线程的用处,但没有举到例子。这次是一个模拟售票系统的多线程程序,主线程负责创建2个子线程,而子线程负责将票售完。全局变量tickets是共享资源,每一个线程均可访问它。
代码1:
- #include <windows.h>
- #include <iostream.h>
- int tickets=100;
- //线程入口函数的声明
- DWORD WINAPI Func1Proc(LPVOID lpParameter);
- DWORD WINAPI Func2Proc(LPVOID lpParameter);
- //主线程
- int main()
- {
- HANDLE hThread1;
- HANDLE hThread2;
- hThread1=CreateThread(NULL,0,Func1Proc,NULL,0,NULL);
- hThread2=CreateThread(NULL,0,Func2Proc,NULL,0,NULL);
- CloseHandle(hThread1);
- CloseHandle(hThread2);
- Sleep(4000);
- return 0;
- }
- //线程1的入口函数
- DWORD WINAPI Func1Proc(LPVOID lpParameter)
- {
- while(true)
- {
- if(tickets>0)
- {
- Sleep(1);
- cout<<"thread1 sell tickets: "<<tickets--<<endl; //tickets先输出,再减1
- }
- else
- {
- break;
- }
- }
- return 0;
- }
- //线程2的入口函数
- DWORD WINAPI Func2Proc(LPVOID lpParameter)
- {
- while(true)
- {
- if(tickets>0)
- {
- Sleep(1);
- cout<<"thread2 sell tickets: "<<tickets--<<endl; //tickets先输出,再减1
- }
- else
- {
- break;
- }
- }
- return 0;
- }
但程序的运行结果是,杂乱无章,为什么呢?因为对线程而言,时间片转到它,它就运行,比如线程1刚好执行完if那一行代码(假设此时tickets=1),时间片却轮到了线程2,在售出为1的票号之后(再假如这时tickets尚没有减1,时间片又到了,此时tickets仍然等于1),接着再次轮到线程1,继续执行刚才的代码,也售出为1的票号;或者(假如这时tickets已经减1,时间片又到了,此时tickets等于0),接着再次轮到线程1,执行之后,售出的是为0的票号。这两种情况都是很糟糕的,尤其是发生在现实生活中的时候,既不能售出票号一样的票,更不能售出不存在的票号。在上面的程序中,还加了两条Sleep函数语句,但无论加不加,结果同样都坏。
证据:
(可以看到,不仅如此,如果有这种情况出现,事实可能更为糟糕!)
--------------------------------------------------------------------------------------------------直到这个时候,多线程之互斥对象的Mutex才浮出水面。互斥对象真正要实现的作用是,使共享资源tickets得到协调使用(即所谓的线程同步,如果这样命名的话,其实并不好理解)。它的实现原理是,通过调用函数WaitForSingleObject来获得互斥对象的拥有权,一旦该线程获得互斥对象,那么别的线程必须无限期等待(既然处于等待状态,它的代码也没有办法执行),直到拥有互斥对象的线程调用函数ReleaseMutex释放互斥对象的拥有权。说到这里,互斥对象的机制应该就比较清楚了。
代码2:
- #include <windows.h>
- #include <iostream.h>
- int tickets=100; //共享资源
- HANDLE hMutex; //共享对象(因为是全局变量嘛~)
- //线程入口函数的声明
- DWORD WINAPI Func1Proc(LPVOID lpParameter);
- DWORD WINAPI Func2Proc(LPVOID lpParameter);
- //主线程
- int main()
- {
- HANDLE hThread1;
- HANDLE hThread2;
- /*安全属性(SecurityAttributes),
- *是否拥有互斥对象(InitialOwner),
- *是否是匿名的互斥对象(MutexName)
- */
- hMutex=CreateMutex(NULL,FALSE,NULL);
- hThread1=CreateThread(NULL,0,Func1Proc,NULL,0,NULL);
- hThread2=CreateThread(NULL,0,Func2Proc,NULL,0,NULL);
- CloseHandle(hThread1);
- CloseHandle(hThread2);
- Sleep(4000);
- return 0;
- }
- //线程1的入口函数
- DWORD WINAPI Func1Proc(LPVOID lpParameter)
- {
- while(true)
- {
- WaitForSingleObject(hMutex,INFINITE);
- if(tickets>0)
- {
- cout<<"thread1 sell tickets: "<<tickets--<<endl; //tickets先输出,再减1
- }
- else
- {
- break;
- }
- ReleaseMutex(hMutex);
- }
- return 0;
- }
- //线程2的入口函数
- DWORD WINAPI Func2Proc(LPVOID lpParameter)
- {
- while(true)
- {
- WaitForSingleObject(hMutex,INFINITE);
- if(tickets>0)
- {
- cout<<"thread2 sell tickets: "<<tickets--<<endl; //tickets先输出,再减1
- }
- else
- {
- break;
- }
- ReleaseMutex(hMutex);
- }
- return 0;
- }
这个程序需要注意的是,函数CreateMutex的3个参数和函数WaitForSingleObject的返回值,解释如下:
WINBASEAPI
HANDLE
WINAPI
CreateMutexA(
LPSECURITY_ATTRIBUTES lpMutexAttributes, //一般为NULL,表示默认的安全属性
BOOL bInitialOwner, //当为FALSE时,表示创建互斥对象的主线程没 有互斥对象的拥有权
LPCSTR lpName //当为NULL时,表示匿名的互斥对象
);
WaitForSingleObject函数的返回值,①WAIT_OBJECT_0,所请求的对象是有信号状态;②WAIT_TIMEOUT,指定的时间间隔已过,并且所请求的对象是无信号状态;③WAIT_ABANDONED,所请求的对象是一个互斥对象,并且先前拥有该对象的线程在终止前没有释 放该对象,该对象的拥有权将授予当前调用线程,并且将该互斥对象设为无信号状态。
(显然,这时候不存在任何的问题。)
继续做实验,加深理解Mutex(实验结果略):
①如果去掉线程1循环里的ReleaseMutex那一行代码,那么线程2将无法得到互斥对象,永远等待下去,运行结果只有线程1在售票;
②如果主线程里的函数CreateMutex的第二个参数为TRUE(表示主线程此时有互斥对象的拥有权),那么线程1和线程2都没有办法得到互斥对象,只有当主线程里成对的出现函数ReleaseMutex时,也就是主线程释放互斥对象的拥有权,线程1和线程2才能够再次拥有互斥对象。
③如果主线程,在函数CreateMutex的第二个参数为TRUE的基础上(此时,互斥对象为无信号状态),再添加函数WaitForSingleObject,由于申请互斥对象的主线程(它的线程ID)和互斥对象的拥有者(即主线程的ID)相同,所以即使互斥对象为无信号也可以再次申请到互斥对象。现在,互斥对象的内部记录是,计数器(哪个线程拥有它的次数)为2,如果要释放互斥对象的话,需要调用函数WaitForSingleObject两次。