实验2 Windows线程同步和互斥
实验目的
1、了解Windows内核对线程同步的支持。
2、了解C的线程函数库及Windows 基本的线程API 函数的使用。 3、进一步理解线程的同步控制原理。
预备知识
一、Windows线程同步机制(注:互斥是同步的一种特例)
? 事件(Event)
? 临界区(Critical Section) ? 互斥量(Mutex) ? 信号量(Semaphore) 1、是否能跨进程使用?
互斥量、信号量、事件都可以跨进程来实现同步数据操作。
临界区只能用在同一进程的线程间互斥,因为临界区无名(无句柄)。如果只为了在进程内部用的话,使用临界区会带来速度上的优势并能够减少资源占用量。 2、其它区别
临界区:访问临界资源的代码段。课堂上讲过。(存钱、取钱的例子还记得吗?) 互斥量:资源独占使用 信号量:资源计数器
事件对象:可以通过“通知”的方式来保持线程的同步。事件是WIN32中最灵活的线程间同步机制。事件存在两种状态:激发状态(Signaled or True)未激发状态(Unsignaled or False)。 3、详细解释:
(见下面实验内容每个程序前)
二、VC++(略)
实验内容
1、用事件(Event)对象来进行线程同步
? 事件可分为两类:
? 手动设置: 这种对象只可能用程序手动设置,在需要该事件或者事件发生
时,采用SetEvent及ResetEvent来进行设置。 ? 自动恢复: 一旦事件发生并被处理后,自动恢复到没有事件状态,不需要再
次设置。
? _beginthread函数:创建一个线程。所在库文件:#include
void( *start_address )( void * ), unsigned stack_size, void *arglist );
返回值:
假如成功,函数将返回一个处理信息对这个新创建的线程。如果失败_beginthread将返回-1。 start_address
新线程的起始地址 ,指向新线程调用的函数的起始地址stack_size stack_size 新线程的堆栈大小,可以为0arglist arglist 传递给线程的参数列表,无参数是为NULL
? CreateEvent函数:创建事件对象 windows.h HANDLE CreateEvent( // SECURITY_ATTRIBUTES结构指针,可为NULL
LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, // 手动/自动 // TRUE:在WaitForSingleObject后必须手动调用ResetEvent清除信号 // FALSE:在WaitForSingleObject后,系统自动清除事件信号 BOOL bInitialState, //初始状态 LPCTSTR lpName //事件的名称 );
? 使用“事件”机制应注意以下事项:
? 如果跨进程访问事件,必须对事件命名,在对事件命名的时候,要注意不要
与系统命名空间中的其它全局命名对象冲突; ? 事件是否要自动恢复; ? 事件的初始状态设置。
? 由于event对象属于内核对象,故进程B可以调用OpenEvent函数通过对象的名字
获得进程A中event对象的句柄,然后将这个句柄用于ResetEvent、SetEvent和WaitForMultipleObjects等函数中。此法可以实现一个进程的线程控制另一进程中线程的运行,例如:
HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,\ ResetEvent(hEvent);
验证程序:3个线程。主线程创建2个线程。一读,一写。写线程(并不真写,只是输出writing等字符串)完成后,读线程才能读,读线程完成后,主线程才能结束。 新建一个Win32控制台应用程序项目(win32 console application) #include \#include
HANDLE evRead,evFinish;//全局变量,事件对象的句柄 void ReadThread(LPVOID param) {
WaitForSingleObject(evRead, INFINITE);//等待evRead被激活 cout<<\读完成,唤醒主线程\ SetEvent(evFinish);//激活evFinish事件 }
void WriteThread(LPVOID param) {
cout<<\写完成,唤醒读线程\ SetEvent(evRead);//激活evRead事件 }
int main(int argc, char* argv[]) { evRead= CreateEvent(NULL,FALSE,FALSE,NULL); evFinish= CreateEvent(NULL,FALSE,FALSE,NULL); _beginthread(ReadThread,0,NULL); _beginthread(WriteThread,0,NULL); WaitForSingleObject(evFinish, INFINITE);//等待evFinish被激活 cout<<\ return 0; }
如果引入了
error C2065: '_beginthread' : undeclared identifier Error executing cl.exe. 解决:
工程?设置?c/c++标签?分类(Category)下拉列表里选择代码生成(Code Generation) ?选用运行时库(Use Run-Time Library)下拉列表里选择多线程Multithreaded。然后重新编译。若还不行,再选Multithreaded DLL。
验证:用//将两条WaitForSingleObject语句屏蔽。重新编译运行,多运行几次,看结果有何不同。思考原因。
2、用临界区(Critical Section)来进行线程互斥
? 临界区是保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许
一个线程对共享资源进行访问。
? 如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问
此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。 ? 临界区包含两个操作原语:
? EnterCriticalSection() 进入临界区 ? LeaveCriticalSection() 离开临界区
? EnterCriticalSection()语句执行后代码将进入临界区以后无论发生什么,必
须确保与之匹配的LeaveCriticalSection()都能够被执行到。否则,临界区保护的共享资源将永远不会被释放。
? 虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多
个进程中的线程。
? 创建临界区
为了创建临界区,首先必须在进程中分配一个全局CRITICAL_SECTION数据结构:CRITICAL_SECTION gCriticalSection; ? 使用临界区
使用临界区之前,必须调用InitializeCriticalSection函数初始化:
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
? 进入临界区
调用EnterCriticalSection函数进入临界区:
VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection); ? 离开临界区
调用LeaveCriticalSection函数退出了临界区:
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection); ? 删除临界区 调用DeleteCriticalSection函数删除临界区:
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection); ? 临界区一般用法:
EnterCriticalSection(& gCriticalSection ); //do something
LeaveCriticalSection(& gCriticalSection); ? 关于临界区的使用,有下列注意点:
? 每个共享资源使用一个CRITICAL_SECTION变量;
? 不要长时间运行关键代码段,当一个关键代码段长时间运行时,其他线程就
会进入等待状态,这会降低应用程序的运行性能;
? 如果需要同时访问多个资源,则可能连续调用EnterCriticalSection; ? Critical Section不是OS核心对象,如果进入临界区的线程\挂\了,将无法释
放临界资源。这个缺点在Mutex中得到了弥补。
验证程序:一个银行系统中两个线程对同一账户执行取款操作,余额1000元。一个使用ATM机取900元,另一个使用存折在柜台取700元。如果不加于控制,会使得账户余额为负数。 #include \#include
CRITICAL_SECTION cs;
void WithDrawThread1(LPVOID param) {//取900元 EnterCriticalSection(&cs); if ((total-900) >= 0) { total-=900;
cout<<\你取了900元\ } else { cout<<\钱不够了,禁止取钱,马上退卡!\ }
LeaveCriticalSection(&cs); SetEvent(evFin[0]); }
void WithDrawThread2(LPVOID param) {//取700元 EnterCriticalSection(&cs); if ((total-700) >= 0) { total-=700;
cout<<\你取了700元\ } else { cout<<\钱不够了,禁止取钱!\ }
LeaveCriticalSection(&cs); SetEvent(evFin[1]); }
int main(int argc, char* argv[]) { evFin[0] = CreateEvent(NULL,FALSE,FALSE,NULL); evFin[1] = CreateEvent(NULL,FALSE,FALSE,NULL); InitializeCriticalSection(&cs); _beginthread(WithDrawThread1,0,NULL); _beginthread(WithDrawThread2,0,NULL); WaitForMultipleObjects(2, evFin, TRUE, INFINITE); DeleteCriticalSection(&cs); cout<<\余额是\ return 0; }
多运行几次,观察结果并分析。
3、用互斥量(Mutex)来进行线程互斥
? 互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限。
? 由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线
程所访问。
? 当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获
得后得以访问资源。
? 互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资
源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。 用CreateMutex函数创建互斥量: HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,// 安全属性结构指针,可为NULL BOOL bInitialOwner, //是否占有该互斥量,TRUE:占有,FALSE:不占有 LPCTSTR lpName //信号量的名称 );
涉及到的其它API如下: