Windows线程同步(下)

时间:2022-01-29 09:41:14
  • 线程同步三:事件

CreateEvent:Creates or opens a named or unnamed event object.

HANDLE WINAPI CreateEvent(
  _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
  _In_     BOOL                  bManualReset,
  _In_     BOOL                  bInitialState,
  _In_opt_ LPCTSTR               lpName
);

函数说明:

第一个参数表示安全控制,一般直接传入NULL。

第二个参数确定事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。如果为自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态

第三个参数表示事件的初始状态,传入TRUR表示已触发。

第四个参数表示事件的名称,传入NULL表示匿名事件。

OpenEvent:Opens an existing named event object.

HANDLE WINAPI OpenEvent(
  _In_ DWORD   dwDesiredAccess,
  _In_ BOOL    bInheritHandle,
  _In_ LPCTSTR lpName
);

函数说明:

第一个参数表示访问权限,对事件一般传入EVENT_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示事件句柄继承性,一般传入TRUE即可。

第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个事件。

SetEvent:Sets the specified event object to the signaled state.

BOOL WINAPI SetEvent(
  _In_ HANDLE hEvent
);

函数功能:触发事件

函数说明:每次触发后,必有一个或多个处于等待状态下的线程变成可调度状态

ResetEvent:Sets the specified event object to the nonsignaled state.

BOOL WINAPI ResetEvent(
  _In_ HANDLE hEvent
);

函数功能:将事件设为末触发

CloseHandle:事件销毁

举例:与Mutex中的例子相比,这里只修改了两行代码

#include <iostream>  
#include <windows.h>  

using namespace std;

long g_nNum;
#define MAX_THREADS 20
HANDLE  g_hThreadEvent;
CRITICAL_SECTION g_csThreadCode;

DWORD WINAPI MyThreadFunction(LPVOID lpParam)
{
    int nThreadNum = *(int *)lpParam;
    SetEvent(g_hThreadEvent);//释放互斥量以便供其他线程获取 

    Sleep(500);
    EnterCriticalSection(&g_csThreadCode);
    g_nNum++;
    Sleep(500);
    cout << "参数 " << nThreadNum << " : " << "资源:" << g_nNum << endl;
    LeaveCriticalSection(&g_csThreadCode);
    return 0;
}

int main()
{
    g_hThreadEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
    InitializeCriticalSection(&g_csThreadCode);
    HANDLE  hThreadArray[MAX_THREADS];
    DWORD dwThreadIdArray[MAX_THREADS] = { 0 };

    g_nNum = 0;
    int i = 0;
    while (i < MAX_THREADS)
    {
        WaitForSingleObject(g_hThreadEvent, INFINITE);//等待获取互斥量
        hThreadArray[i] = (HANDLE)CreateThread(NULL, 0, MyThreadFunction, &i, 0, &dwThreadIdArray[i]);
        i++;
    }
    WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE);

    //销毁互斥量和关键段  
    CloseHandle(g_hThreadEvent);
    DeleteCriticalSection(&g_csThreadCode);
    for (i = 0; i < MAX_THREADS; i++)
        CloseHandle(hThreadArray[i]);

    system("pause");
    return 0;
}

模仿CRITICAL_SECTION例子中的方式,在WaitForSingleObject和SetEvent上分别下断点:

Windows线程同步(下)

然后这次再运行,与CRITICAL_SECTION中例子不同的现象发生了,这次会轮流在WaitForSingleObject和SetEvent处断下,而运行结果也显示:

Windows线程同步(下)

参数不再有重复的了,说明利用Event实现了主线程和各个子线程间的同步

但是为什么Event可以?

Mutex有一个“所有者”的概念,只有拥有互斥对象的线程才具有访问资源 的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。事件对象也可以通过通知操作的方式来保持线程的同步。可以这么理解,对于Mutex,A线程占有资源S,A可以重复多次访问资源S,除非A自身决定释放,否则其它线程无法去访问S。但是对于Event就不同了。A线程Wait将事件变成未触发状态进行它的操作,操作后,B线程可以触发这个事件,继续B的后续的操作,若无线程释放,则A也无法重复的去获取这个事件以重复进行操作。

此外,Event还有一个API:PulseEvent

BOOL WINAPI PulseEvent(
  _In_ HANDLE hEvent
);

函数功能:将事件触发后立即将事件设置为未触发,相当于触发一个事件脉冲。

函数说明:这是一个不常用的事件函数,此函数相当于SetEvent()后立即调用ResetEvent();此时情况可以分为两种:

1.对于手动置位事件,所有正处于等待状态下线程都变成可调度状态。

2.对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。

此后事件是末触发的。该函数不稳定,因为无法预知在调用PulseEvent ()时哪些线程正处于等待状态。

举例:

设为自动置位:

#include <iostream>  
#include <windows.h>
using namespace std;
HANDLE  g_hThreadEvent;
//快线程  
DWORD WINAPI FastThreadFun(LPVOID lpParam)
{
    Sleep(10); //用这个来保证各线程调用等待函数的次序有一定的随机性  
    //cout << "启动线程" << lpParam << endl;
    printf("%s 启动线程\n", lpParam);
    WaitForSingleObject(g_hThreadEvent, INFINITE);//线程在等待时间
    //自动就有个ResetEvent
    printf("%s 等到事件被触发 顺利结束\n", lpParam);
    return 0;
}
//慢线程  
DWORD WINAPI SlowThreadFun(LPVOID lpParam)
{
    Sleep(10);
    printf("%s 启动线程\n", lpParam);
    WaitForSingleObject(g_hThreadEvent, INFINITE);
    //自动就有个ResetEvent 置为未触发状态
    printf("%s 等到事件被触发 顺利结束\n", lpParam);
    return 0;
}
int main()
{
    BOOL bManualReset = FALSE;//自动置位FALSE 即自动ResetEvent(未触发状态)
    //第三个参数是FALSE,标明事件被初始化时,其状态是未触发的
    g_hThreadEvent = CreateEvent(NULL, bManualReset, FALSE, NULL);

    //"数组"的数组,其实"数组"就是地址。szFastThreadName[5][30]表示5个"数组",即5个地址.szFastThreadName[i]就是一个地址
    //观察内存也可发现,这个数组szFastThreadName[5],保存了5个地址,每个地址处开辟了30字节的空间
    //int a[5] = { 1, 2, 3, 4, 5 };//这里每四个字节存一个数
    char szFastThreadName[5][30] = { "Fast1000", "Fast1001", "Fast1002", "Fast1003", "Fast1004" };//这里每30个字节存一个字符数组
//但a[i]是第一个数字,而szFastThreadName[i]不是第i个数组,而是第i个数组的地址。开辟了5*30字节空间,但是用地址代表每个数组
    //我们可以想象,a是数组a[5]的首地址;那么,szSlowThreadName[1],就是数组szSlowThreadName[1][30]的首地址
    char szSlowThreadName[2][30] = { "Slow196", "Slow197" };
    DWORD dwFastThreadID[5];
    DWORD dwSlowThreadID[2];
    int i;
    for (i = 0; i < 5; i++)
        CreateThread(NULL, 0, FastThreadFun, szFastThreadName[i], 0, &dwFastThreadID[i]);
    for (i = 0; i < 2; i++)
        CreateThread(NULL, 0, SlowThreadFun, szSlowThreadName[i], 0, &dwSlowThreadID[i]);

    Sleep(50); //保证快线程已经全部启动  
    cout << "现在主线程触发一个事件脉冲 - PulseEvent()" << endl;
    PulseEvent(g_hThreadEvent);
    //自动的话,已经有了一个ResetEvent
//     SetEvent(g_hThreadEvent);  //触发
//     ResetEvent(g_hThreadEvent);  

    //Sleep(3000);
    cout << "时间到,主线程结束运行" << endl;
    CloseHandle(g_hThreadEvent);
    system("pause");
    return 0;
}

Windows线程同步(下)

自动置位的思路是这样的,由于CreateEvent的第三个参数是FALSE,所以,这个事件一上来是未激活的。主线程先起来7个子线程,当主线程执行一个PulseEvent时,会在某一瞬间触发一个事件,所以会有一个正在Wait的子线程被激活而顺利执行,但由于是自动置位的,所以这个被激活的子线程在WaitForSingleObject后,就立刻自动ResetEvent了,于是其它的线程无法被激活,由于主线程没有WaitForSingleObject,所以,只有主线程继续顺利执行下去。

 而如果设为手动置位结果如下:

Windows线程同步(下)

因为,主线程PulseEvent之后,某个线程Wait到这个事件,而它在WaitForSingleObject并未ResetEvent,所以事件还是触发状态,其它的正在Wait的子线程仍可以Wait到,所以激活了全部子线程。

参考:http://blog.csdn.net/morewindows/article/details/7445233

  • 线程同步四:信号量(Semaphore)

CreateSemaphore

HANDLE WINAPI CreateSemaphore(
  _In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
  _In_     LONG                  lInitialCount,
  _In_     LONG                  lMaximumCount,
  _In_opt_ LPCTSTR               lpName
);

函数功能:创建信号量

函数说明:

第一个参数表示安全控制,一般直接传入NULL

第二个参数表示初始资源数量

第三个参数表示最大并发数量

第四个参数表示信号量的名称,传入NULL表示匿名信号量。

OpenSemaphore

HANDLE WINAPI OpenSemaphore(
  _In_ DWORD   dwDesiredAccess,
  _In_ BOOL    bInheritHandle,
  _In_ LPCTSTR lpName
);

函数功能:打开信号量

函数说明:

第一个参数表示访问权限,对一般传入SEMAPHORE_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示信号量句柄继承性,一般传入TRUE即可。

第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个信号量。

ReleaseSemaphore

BOOL WINAPI ReleaseSemaphore(
  _In_      HANDLE hSemaphore,
  _In_      LONG   lReleaseCount,
  _Out_opt_ LPLONG lpPreviousCount
);

函数功能:递增信号量的当前资源计数

函数说明:

第一个参数是信号量的句柄。

第二个参数表示增加个数,必须大于0且不超过最大资源数量。

第三个参数可以用来传出先前的资源计数,设为NULL表示不需要传出。

注意:当前资源数量大于0,表示信号量处于触发,等于0表示资源已经耗尽故信号量处于末触发。在对信号量调用等待函数时,等待函数(WaitForSingleObject)会检查信号量的当前资源计数,如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量

CloseHandle

 

最后一个 信号量的清理与销毁.由于信号量是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。

 

在经典多线程问题中设置一个信号量和一个关键段。用信号量处理主线程与子线程的同步,用关键段来处理各子线程间的互斥。举例:

 

#include <stdio.h>   
#include <windows.h>  
long g_nNum;
const int THREAD_NUM = 10;
//信号量与关键段  
HANDLE            g_hThreadParameter;
CRITICAL_SECTION  g_csThreadCode;

DWORD WINAPI ChildThread(LPVOID lpParam)
{
    int nThreadNum = *(int *)lpParam;
    ReleaseSemaphore(g_hThreadParameter, 1, NULL);//信号量++  变为1,主线程中的WaitForSingleObject
    //才能Wait到信号量,从而继续的执行

    Sleep(50); 

    EnterCriticalSection(&g_csThreadCode);
    ++g_nNum;
    Sleep(0); 
    printf("线程:%d  资源:%d\n", nThreadNum, g_nNum);
    LeaveCriticalSection(&g_csThreadCode);
    return 0;
}


int main()
{
    //初始化信号量和关键段  
    g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL);//初始0个资源,最大允许1个同时访问  
    InitializeCriticalSection(&g_csThreadCode);

    HANDLE  handle[THREAD_NUM];
    g_nNum = 0;
    int i = 0;
    while (i < THREAD_NUM)
    {
        handle[i] = (HANDLE)CreateThread(NULL, 0, ChildThread, &i, 0, NULL);
        WaitForSingleObject(g_hThreadParameter, INFINITE);//等待信号量>0  
        ++i;
    }
    WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);

    DeleteCriticalSection(&g_csThreadCode);
    CloseHandle(g_hThreadParameter);
    for (i = 0; i < THREAD_NUM; i++)
        CloseHandle(handle[i]);

    system("pause");
    return 0;
}

 

 

Windows线程同步(下)

 

CreateSemaphore的第二个参数位0,第三个参数为1,表示信号量初始为0,最多允许1个线程同时访问。主线程中CreateThread之后,信号量个数为0,所以主线程只能一直Wait,直到子线程中有一个ReleaseSemaphore是的信号量个数加1大于0,
这时主线程才能Wait到信号量,且自动减1。而后边主线程如果再想执行,也必须要等到其它子线程先ReleaseSemaphore才行。

 

可以看出来,信号量也可以解决线程之间的同步问题。

参考:http://blog.csdn.net/morewindows/article/details/7481609