线程同步总结--临界区 事件 互斥量 信号量

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/i_chaoren/article/details/81301916

在WIN32中,同步机制主要有以下几种:

  • 临界区(Critical section)

  • 事件(Event);

  • 互斥量(mutex);

  • 信号量(semaphore);

临界区(Critical section)

临界区(Critical Section)指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源有无法同时被多个线程访问的特性。

在有一个线程进入临界区后,其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

临界区可以用于线程间的互斥,但不可以用于同步。

除临界区外,事件、互斥量和信号量都是内核对象。

一旦线程进入一个临界区,则它就可以一再的重复进入该临界区,当然每个进入操作都必须对应离开操作。

扫描二维码关注公众号,回复: 4715054 查看本文章

初始化

销毁

进入临界区

离开临界区

InitializeCriticalSection

DeleteCriticalSection

EnterCriticalSection

LeaveCriticalSection

也就是EnterCriticalSection( ),可以嵌套。

但是千万不要在临界区中调用 sleep(),或任何 Wait..() 函数。

临界区的缺点是:没有办法知道进入临界区中的那个线程是生是死。如果那个线程在进入临界区后当掉了,而且没有退出来,那么系统就没有办法消除掉此临界区。

 

事件(Event)

 用来通知线程有一些事件已发生,从而启动后继任务的开始。

       在创建事件对象时可以设置为“自动重置事件”或“手动重置事件”。当一个手动重置事件被触发的时候,正在等待该事件的所有线程都将变成可调度状态;而当一个自动重置事件对象被触发的时候,只有一个正在等待该事件的线程会变成可调度状态,随后该事件对象自动变为未触发态,但是如果被触发时没有等待该事件对象的线程,那么该事件对象会保持触发状态,直至遇到第一个等待该事件对象的线程。

创建事件

销毁事件

获得事件句柄

触发事件

使事件未触发

CreateEvent

CloseHandle

OpenEvent

SetEvent

ResetEvent

举例:

//使用事件机制同步线程的例子
//设置三个线程,一个主线程,一个读线程和一个写线程,
//读线程必须在写线程写之后才能读,主线程必须在读线程读之后才能结束
/*
实现:定义两个事件, evRead, evFinish;读线程等待evRead, 主线程等待evFinish.
*/
#include <iostream>
#include <Windows.h>
#include <process.h>
using namespace std;
HANDLE evRead, evFinish;
void ReadThread(void* param){
    //等待读事件
    WaitForSingleObject(evRead, INFINITE);
    //读操作
    cout << "reading" << endl;
    //激发事件evFinish
    SetEvent(evFinish);
}
void WriteThread(void* param){
    //写操作
    cout << "writing" << endl;
    //唤醒读事件
    SetEvent(evRead);
}
int main()
{
    //创建两个事件,并初始化为未激发状态
    evRead = CreateEvent(NULL, false, false, NULL);
    evFinish = CreateEvent(NULL, false, false, NULL);
    
    //创建读线程和写线程
    _beginthread(ReadThread, 0, NULL);
    _beginthread(WriteThread, 0, NULL);
    //等待事件evFinish
    WaitForSingleObject(evFinish, INFINITE);
    cout << "the program is end." << endl;
    return 0;
}

互斥量(mutex)

互斥量(mutex)是指用来防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。

互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。 

创建互斥量

打开互斥量

触发互斥量

清理互斥量

CreateMutex

OpenMutex

ReleaseMutex

CloseHandle

Mutexes 用途和 Critical Section 非常类似,线程拥有 mutex 就好象线程进入 critical section 一样,但是它牺牲速度以增加弹性

一旦没有任何线程拥有那个 mutex,这个 mutex 便处于激发状态。

它与临界区的区别是:

1. Mutexes 操作要比 Critical Section 费时的多。

2. Mutexes 可以跨进程使用,Critical Section 则只能在同一进程中使用。

3. 等待一个 Mutex 时,你可以指定"结束等待"的时间长度,而 Critical Section 则不行。

说明:

1. Mutex 的拥有权:

Mutex 的拥有权并非属于那个产生它的线程,而是那个最后对些 Mutex 进行 WaitXXX() 操作并且尚未进行 ReleaseMutex() 操作的线程。

2. Mutex 被舍弃:

如果线程在结束前没有调用 ReleaseMutex(),比如线程调用了 EXitThread() 或者因为当掉而结束。这时的 mutex 不会被摧毁,而是被视为"未被拥有"以及"未被激发"的状态,在下一个 WaitXXX() 中线程会被以WAIT_ABANDONED_0 (WAIT_ABANDONED_0_n + 1 )来通知。

3. 最初拥有者:

CreateMutex(),第二个参数 bInitialOwner,允许你指定现行线程是否立刻拥有产生出来的 mutex。

示例:

第一个程序:创建互斥量并等待用户输入后就触发互斥量。

#include <stdio.h>
#include <conio.h>
#include <windows.h>
const char MUTEX_NAME[] = "Mutex_MoreWindows";
int main()
{
    HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEX_NAME); //创建互斥量
    printf("互斥量已经创建,现在按任意键触发互斥量\n");
    getch();
    //exit(0);
    ReleaseMutex(hMutex);
    printf("互斥量已经触发\n");
    CloseHandle(hMutex);
    return 0;
}

第二个程序:先打开互斥量,成功后就等待并根据等待结果作相应的输出。

#include <stdio.h>
#include <windows.h>
const char MUTEX_NAME[] = "Mutex_MoreWindows";
int main()
{
    HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, TRUE, MUTEX_NAME); //打开互斥量
    if (hMutex == NULL)
    {
        printf("打开互斥量失败\n");
        return 0;
    }
    printf("等待中....\n");
    DWORD dwResult = WaitForSingleObject(hMutex, 20 * 1000); //等待互斥量被触发
    switch (dwResult)
    {
    case WAIT_ABANDONED:
        printf("拥有互斥量的进程意外终止\n");
        break;
    case WAIT_OBJECT_0:
        printf("已经收到信号\n");
        break;
    case WAIT_TIMEOUT:
        printf("信号未在规定的时间内送到\n");
        break;
    }
    CloseHandle(hMutex);
    return 0;
}

信号量Semaphore

信号量Semaphore用于保持在0至指定最大值之间的一个计数值。

信号量对象对线程的同步方式与前面几种方法不同,信号量允许多个线程同时使用共享资源 ,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目, 不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。 

创建信号量

销毁

打开信号量

递增计数

递减计数

CreateSemaphore

CloseHandl

OpenSemaphore

ReleaseSemaphore

WaitForSingleObject

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

#include <stdio.h>
#include <process.h>
#include <windows.h>
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//信号量与关键段
HANDLE            g_hThreadParameter;
CRITICAL_SECTION  g_csThreadCode;
int main()
{
    printf("     经典线程同步 信号量Semaphore\n");
    printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
    //初始化信号量和关键段
    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)_beginthreadex(NULL, 0, Fun, &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]);
    return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
    int nThreadNum = *(int *)pPM;
    ReleaseSemaphore(g_hThreadParameter, 1, NULL);//信号量++
    Sleep(50);//some work should to do
    EnterCriticalSection(&g_csThreadCode);
    ++g_nNum;
    Sleep(0);//some work should to do
    printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);
    LeaveCriticalSection(&g_csThreadCode);
    return 0;
}

 总结
1. 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量 。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。 
2. 互斥量(Mutex),信号量(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和 线程退出。 
3. 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器

参考文章:

https://blog.csdn.net/morewindows/article/details/7538247

https://blog.csdn.net/sunshinewave/article/details/50851061

猜你喜欢

转载自blog.csdn.net/i_chaoren/article/details/81301916