三种方式实现多线程同步问题

来源:互联网 发布:行业数据网站 编辑:程序博客网 时间:2024/06/07 18:08

1使用互斥量和事件对象实现线程同步的代码

#include <windows.h>#include <iostream.h>DWORD WINAPI Fun1Proc(  LPVOID lpParameter   // thread data);DWORD WINAPI Fun2Proc(  LPVOID lpParameter   // thread data);int tickets = 100;//HANDLE hMutex;HANDLE hEvent;void main(){HANDLE handle1,handle2;handle1 = CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);handle2 = CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);CloseHandle(handle1);CloseHandle(handle2);//hMutex = CreateMutex(NULL,FALSE,NULL);//hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);hEvent = CreateEvent(NULL,FALSE,FALSE,"tickets");if(hEvent){   if(ERROR_ALREADY_EXISTS == GetLastError())   {    cout<<"only instance can run!"<<endl;   return ;   }       }SetEvent(hEvent);Sleep(4000);CloseHandle(hEvent);}DWORD WINAPI Fun1Proc(LPVOID lpParameter){while(TRUE){//WaitForSingleObject(hMutex,INFINITE);WaitForSingleObject(hEvent,INFINITE);   if(tickets > 0)   {   Sleep(1);   cout<<"thread1 is selling tickets:"<<tickets--<<endl;   }   else    break; //  ReleaseMutex(hMutex);SetEvent(hEvent);}return 0;}DWORD WINAPI Fun2Proc(LPVOID lpParameter){while(TRUE){//WaitForSingleObject(hMutex,INFINITE);WaitForSingleObject(hEvent,INFINITE);        if(tickets > 0){Sleep(1);        cout<<"thread2 is selling tickets:"<<tickets--<<endl;}else break;  // ReleaseMutex(hMutex);SetEvent(hEvent);}return 0;}
2使用临界区实现线程同步的代码

#include <Windows.h>#include <iostream>using namespace std;DWORD WINAPI Fun1Proc(LPVOID lParameter);DWORD WINAPI Fun2Proc(LPVOID lParameter);CRITICAL_SECTION g_cs;int tickets = 100;int main(){HANDLE hThread1;HANDLE hThread2;hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);CloseHandle(hThread1);CloseHandle(hThread2);//初始化关键代码段InitializeCriticalSection(&g_cs);Sleep(4000);//使用完毕删除关键代码段DeleteCriticalSection(&g_cs);return 0;}DWORD WINAPI Fun1Proc(LPVOID lParameter){while (TRUE){//进入关键代码段,如果被人占用则一直等待EnterCriticalSection(&g_cs);Sleep(1);if (tickets > 0){cout << "thread1 sell ticket : " << tickets-- << endl;//离开关键代码段LeaveCriticalSection(&g_cs);}else{LeaveCriticalSection(&g_cs);break;}}return 0;}DWORD WINAPI Fun2Proc(LPVOID lParameter){while (TRUE){EnterCriticalSection(&g_cs);Sleep(1);if (tickets > 0){cout << "thread2 sell ticket : " << tickets-- << endl;LeaveCriticalSection(&g_cs);}else{LeaveCriticalSection(&g_cs);break;}}return 0;}

以上过程中所涉及的函数有:

(1)

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
SIZE_T dwStackSize,//initialstacksize
LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction
LPVOID lpParameter,//threadargument
DWORD dwCreationFlags,//creationoption
LPDWORD lpThreadId//threadidentifier
)
lpThreadAttributes:指向SECURITY_ATTRIBUTES型态的结构的指针。在Windows 98中忽略该参数。在Windows NT中,NULL使用默认安全性,不可以被子线程继承,否则需要定义一个结构体将它的bInheritHandle成员初始化为TRUE
dwStackSize,设置初始栈的大小,以字节为单位,如果为0,那么默认将使用与调用该函数的线程相同的栈空间大小。任何情况下,Windows根据需要动态延长堆栈的大小。
lpStartAddress,指向线程函数的指针,形式:@函数名,函数名称没有限制,但是必须以下列形式声明:
DWORD WINAPI 函数名 (LPVOID lpParam) ,格式不正确将无法调用成功。
lpParameter:向线程函数传递的参数,是一个指向结构的指针,不需传递参数时,为NULL。
dwCreationFlags :线程标志,可取值如下
(1)CREATE_SUSPENDED(0x00000004):创建一个挂起的线程,
(2)0:表示创建后立即激活。
(3)STACK_SIZE_PARAM_IS_A_RESERVATION(0x00010000):dwStackSize参数指定初始的保留堆栈 的大小,否则,dwStackSize指定提交的大小。该标记值在Windows 2000/NT and Windows Me/98/95上不支持。
lpThreadId:保存新线程的id。
返回值:函数成功,返回线程句柄;函数失败返回false。若不想返回线程ID,设置值为NULL。
(2)
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes, // 指向安全属性的指针
BOOLbInitialOwner, // 初始化互斥对象的所有者
LPCTSTRlpName // 指向互斥对象名的指针
);
lpMutexAttributes SECURITY_ATTRIBUTES,指定一个SECURITY_ATTRIBUTES结构,或传递零值(将参数声明为ByVal As Long,并传递零值),表示使用不允许继承的默认描述符
bInitialOwner Long,如创建进程希望立即拥有互斥体,则设为TRUE。一个互斥体同时只能由一个线程拥有
lpName String,指定互斥体对象的名字。用vbNullString创建一个未命名的互斥体对象。如已经存在拥有这个名字的一个事件,则打开现有的已命名互斥体。这个名字可能不与现有的事件、信号机、可等待计时器或文件映射相符
(3)
HANDLECreateEvent(
LPSECURITY_ATTRIBUTESlpEventAttributes,// 安全属性
BOOLbManualReset,// 复位方式
BOOLbInitialState,// 初始状态
LPCTSTRlpName // 对象名称
);[1] 

lpEventAttributes[输入]
一个指向SECURITY_ATTRIBUTES结构的指针,确定返回的句柄是否可被子进程继承。如果lpEventAttributes是NULL,此句柄不能被继承。
Windows NT/2000:lpEventAttributes的结构中的成员为新的事件指定了一个安全符。如果lpEventAttributes是NULL,事件将获得一个默认的安全符。
bManualReset[输入]
指定将事件对象创建成手动复原还是自动复原。如果是TRUE,那么必须用ResetEvent函数来手工将事件的状态复原到无信号状态。如果设置为FALSE,当一个等待线程被释放以后,系统将会自动将事件状态复原为无信号状态。
bInitialState[输入]
指定事件对象的初始状态。如果为TRUE,初始状态为有信号状态;否则为无信号状态。
lpName[输入]
指定事件的对象的名称,是一个以0结束的字符串指针。名称的字符格式限定在MAX_PATH之内。名字是对大小写敏感的。
如果lpName指定的名字,与一个存在的命名的事件对象的名称相同,函数将请求EVENT_ALL_ACCESS来访问存在的对象。这时候,由于bManualReset和bInitialState参数已经在创建事件的进程中设置,这两个参数将被忽略。如果lpEventAttributes是参数不是NULL,它将确定此句柄是否可以被继承,但是其安全描述符成员将被忽略。
如果lpName为NULL,将创建一个无名的事件对象。
(4)
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
hHandle[in]对象句柄。可以指定一系列的对象,如Event、Job、Memory resource notification、Mutex、Process、Semaphore、Thread、Waitable timer等。
dwMilliseconds[in]定时时间间隔,单位为milliseconds(毫秒).如果指定一个非零值,函数处于等待状态直到hHandle标记的对象被触发,或者时间到了。如果dwMilliseconds为0,对象没有被触发信号,函数不会进入一个等待状态,它总是立即返回。如果dwMilliseconds为INFINITE,对象被触发信号后,函数才会返回。

总结:

互斥对象、事件对象、关键代码段的应用和比较

一.     互斥对象

        互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。

互斥对象包含一个使用数量,一个线程ID和一个计数器。其中线程ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。

       创建互斥对象:调用函数CreateMutex。调用成功,该函数返回所创建的互斥对象的句柄。

       请求互斥对象的所有权:调用函数WaitForSingleObject函数。线程必须主动请求共享对象的所有权才能获得该所有权。

       释放指定互斥对象的所有权:调用ReleaseMutex函数。线程访问共享资源结束后,线程要主动释放对互斥对象的所有权,使该对象处于已通知状态。

 

二.     事件对象

        事件对象也属于内核对象,它包含以下三个成员:

        ●    使用计数;

        ●    用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值;

        ●    用于指明该事件处于已通知状态还是未通知状态的布尔值。

        事件对象有两种类型:人工重置的事件对象和自动重置的事件对象。这两种事件对象的区别在于当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;而当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。

1.      创建事件对象

       调用CreateEvent函数创建或打开一个命名的或匿名的事件对象。

2.      设置事件对象状态

       调用SetEvent函数把指定的事件对象设置为有信号状态。

3.      重置事件对象状态

       调用ResetEvent函数把指定的事件对象设置为无信号状态。

4.      请求事件对象

线程通过调用WaitForSingleObject函数请求事件对象。

三.     关键代码段

        关键代码段,也称为临界区,工作在用户方式下。它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。通常把多线程中访问同一种资源的那部分代码当做关键代码段。

1.      初始化关键代码段

       调用InitializeCriticalSection函数初始化一个关键代码段。

       该函数只有一个指向CRITICAL_SECTION结构体的指针。在调用InitializeCriticalSection函数之前,首先需要构造一个CRITICAL_SCTION结构体类型的对象,然后将该对象的地址传递给InitializeCriticalSection函数。

2.      进入关键代码段

       调用EnterCriticalSection函数,以获得指定的临界区对象的所有权,该函数等待指定的临界区对象的所有权,如果该所有权赋予了调用线程,则该函数就返回;否则该函数会一直等待,从而导致线程等待。

 

3.      退出关键代码段

        线程使用完临界区所保护的资源之后,需要调用LeaveCriticalSection函数,释放指定的临界区对象的所有权。之后,其他想要获得该临界区对象所有权的线程就可以获得该所有权,从而进入关键代码段,访问保护的资源。

4.      删除临界区

          当临界区不再需要时,可以调用DeleteCriticalSection函数释放该对象,该函数将释放一个没有被任何线程所拥有的临界区对象的所有资源。

四.     三者的比较

        对于上面介绍的三种线程同步的方式,它们之间的区别如下所述:

        ●    互斥对象和事件都属于内核对象,利用内核对象进行线程同步时,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个线程中的各个线程间进行同步。

        ●    关键代码段工作在用户方式下,同步速度较快,但在使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值。

       通常,在编写多线程程序并需要实现线程同步时,首选关键代码段,由于它的使用比较简单,如果是在MFC程序中使用的话,可以在类的构造函数中调用InitializeCriticalSection函数,在该类的析构函数中调用DeleteCriticalSection函数,在所需保护的代码前面调用EnterCriticalSection函数,在访问完所需保护的资源后,调用LeaveCriticalSection函数。可见,关键代码段在使用上是非常方便的,但有几点需要注意:一是在程序中调用了EnterCriticalSection后,一定要相应的调用LeaveCriticalSection函数,否则其他等待该临界区对象所有权的线程将无法执行。二是如果访问关键代码段时,使用了多个临界区对象,就要注意防止线程死锁的发生。另外,如果需要在多个线程间的各个线程间实现同步的话,可以使用互斥对象和事件对象。

  前提:假设有个经理,下面有5个项目组.经理同时最多能接5个项目,每个项目组一次只能做一个项目。

信标Semaphore:信标相当于,5个项目组都争先恐后的争夺项目。如果经理有5个项目,那么5个项目组都可以做。如果经理有3个项目,那就有2个项目没事情做,在等待。如果经理没有项目,那么5个组,都在闲着。

事件Event:相当于,外面有个项目,经理把项目接回来了。站在门口大喊:有项目了。喊完了,经理就不管了这个项目有没有人处理。

互斥Mutex:相当于,经理手上有1个项目。下面的组,主动地争夺项目,谁抢到谁就有事情做,没抢到的就得待着。


0 0
原创粉丝点击