现代操作系统都支持多线程操作了,多线程操作带来的一个麻烦就是多个线程对共享数据的访问。假设我们有线程A
和线程B,它们需要访问同一内存区域,线程A写,线程B读。一般情况下我们是希望线程A写操作完成后再进行读操
作或者线程B读操作完成后我们再进行写操作。但是在多线程中,可能由于线程A分配的时间片用完了或者其他原因导
致线程A的写操作还没完成就调用线程B来对这块共享内存进行读操作,也有可能在线程B的读操作还没完成就调用线
程A来对这块共享内存进行写操作,这些情况都有可能导致严重的逻辑错误。为了解决这一现象,我们就需要一种机
制使得各个线程能够协同工作,这就是我们所讲的线程同步机制了。
Windows系统中用于线程同步的常用机制有:
互斥对象(Mutex)
事件对象(Event)
信号量(Semaphore)
临界区(critical section)
可等待计时器(Waitable Timer)
在学习线程同步之前,我们需要先来了解下同步过程中最重要的两个概念:同步对象和等待函数。
同步对象主要有(Mutex、Event、Semaphore、critical section)。同步对象一般具有两种状态:标志的和未标志的。线
程根据是否已经完成操作将同步对象设置为标志的或未标志的。
而等待函数的功能是专门用于等待同步对象状态改变。一个线程调用等待函数后执行会暂停,直到同步对象的状态变
为标志的之后,等待函数才会返回,线程才能继续执行下去。
关于上面所讲的几种主要同步对象的概念,这篇临界区,互斥量,信号量,事件的区别讲的很详细,不懂的朋友可以
去肯看。
线程同步的过程:
1、在需要进行线程同步的进程中定义某种同步对象,同步对象必需是全局的,以保证需要同步的所有线程都可以访
问到同步对象。
2、开始时,所有的线程相互独立地运行
3、当某一线程(为了方便描述设为线程A)需要访问共享资源时,若同步对象为“未标志的”,继续等待;反之,线程A将
同步对象设为“未标志的”,并对共享资源进行访问,访问结束后再将同步对象设为“标志的”使得其它线程可以访问
共享资源。
为了便于理解线程同步的过程,我们可以把我们需要访问的共享资源当成是一件放在房间里的东西,而同步对象当成
是门上的锁,而需要访问资源的线程就可以当做是取东西的人了,“标志的”状态表示门是开的,“未标志的”状态表示
门是锁着的,而此时钥匙在进去的那个人手里。当某人进入房间后,就将门锁上,其他人就无法进入了,只有等这个
人出来之后才能进入。
下面是一个我自己写的利用事件对象来同步访问共享内存实例:
#include <windows.h> #include <stdio.h> #include <string.h> TCHAR szSharedBuffer[100] = {0}; //共享内存 HANDLE hEvent; //事件对象句柄 DWORD WINAPI ThreadForWrite (LPVOID lpParam); DWORD WINAPI ThreadForRead (LPVOID lpParam); int main() { HANDLE hWrite; HANDLE hRead; hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); hWrite = CreateThread(NULL, 0, ThreadForWrite, 0, 0, NULL); hRead = CreateThread(NULL, 0, ThreadForRead, 0, 0, NULL); SetEvent(hEvent); while(1); return 0; } DWORD WINAPI ThreadForWrite(LPVOID lpParam) { while (1) { WaitForSingleObject(hEvent, INFINITE); printf("Please input the shared chars: "); scanf("%s", szSharedBuffer); SetEvent(hEvent); } return 0; } DWORD WINAPI ThreadForRead(LPVOID lpParam) { while (1) { WaitForSingleObject(hEvent, INFINITE); if (!strlen(szSharedBuffer)) printf("The shared chars is null now!\n"); else printf("The shared chars is %s\n", szSharedBuffer); SetEvent(hEvent); } return 0; }
慢慢补充。
补充:
在上面这个例子中,创建事件对象时,第二个参数我设置的是FALSE,也就是说将事件自动重置。后来我自己改用
设置为TRUE,结果出了问题。后来想起来设置为TRUE的话,需要我们手动设置。于是在WaitForSingleObject函数
后面加了ResetEvent函数。本以为这样就可以解决问题了,但还是有问题。后来在网上问了下别人也找了点书看才知
道了原因。
这种做法存在两个问题,一个问题是,在单CPU平台下,同一时刻只能有一个线程在运行,假设线程ThreadForWrite
先执行,它得到事件对象:hEvent,但是如果正好这时它的时间片终止了,于是轮到线程ThreadForRead执行,但因
为现在在线程ThreadForWrite中,ResetEvent函数还没有被执行,所以该事件对象仍然处于“标志的”状态,因此线程
ThreadForRead就可以得到该事件对象,也就是说,此时两个线程都可以访问共享资源,于是结果就无法预料了。
第二个问题,当把这段程序移植到多CPU平台上时,两个线程就可以同时运行,这时再主函数里调用SetEvent函数将
其设置为”标志的“状态已经没多大意义了,因为这两个线程都已经可以访问共享资源了,而且是同时使用。
看来以后实现线程同步时还是老实点使用自动重置的事件对象。