windows 线程同步的四种方式总结

本文详细介绍了Windows系统中三种内核态下的同步技术:互斥变量(Mutex)、事件对象(Event)和信号量(Semaphore),并通过火车售票示例展示了它们在并发控制中的应用。
摘要由CSDN通过智能技术生成

一:内核态下的三种同步方式:

        一、互斥变量Mutex)

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

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

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

        创建互斥对象函数 

HANDLE
WINAPI
CreateMutexW(
    _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,   //指向安全属性
    _In_ BOOL bInitialOwner,   //初始化互斥对象的所有者  TRUE 立即拥有互斥体
    _In_opt_ LPCWSTR lpName    //指向互斥对象名的指针  L“Bingo”
);

  • 第一个参数表示安全属性,这是每一个创建内核对象都会有的参数,NULL表示默认安全属性
  • 第二个参数表示互斥对象所有者,TRUE立即拥有互斥体
  • 第三个参数表示指向互斥对象的指针 
  • 例子1:
  • 下面这段程序声明了一个全局整型变量,并初始化为0。一个线程函数对这个变量进行+1操作,执行50000次;另一个线程函数对这个变量-1操作,执行10000次。两个线程函数各创建15个。因为我们使用了互斥变量,30个线程会按照一定顺序对这变量操作,因此最后结果为0。 

  • #include <stdio.h>
    #include <windows.h>
    #include <process.h>

    #define NUM_THREAD    30
    unsigned WINAPI threadInc(void* arg);
    unsigned WINAPI threadDes(void* arg);
    long long num = 0;
    HANDLE hMutex;

    int main() {
        //内核对象数组
        HANDLE tHandles[NUM_THREAD];
        int i;
        //创建互斥信号量
        hMutex = CreateMutex(0, FALSE, NULL);
        for (i = 0; i < NUM_THREAD; i++) {
            if (i % 2)
                tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
            else
                tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
        }

        WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
        //关闭互斥对象
        CloseHandle(hMutex);
        printf("result: %lld \n", num);
        return 0;
    }

    unsigned WINAPI threadInc(void* arg) {
        int i;
        //请求使用
        WaitForSingleObject(hMutex, INFINITE);
        for (i = 0; i < 100000; i++)
            num += 1;
        //释放
        ReleaseMutex(hMutex);
        return 0;
    }
    unsigned WINAPI threadDes(void* arg) {
        int i;
        //请求
        WaitForSingleObject(hMutex, INFINITE);
        for (i = 0; i < 100000; i++)
            num -= 1;
        //释放
        ReleaseMutex(hMutex);
        return 0;
    }

2) 实例2:

#define _AFXDLL
#include "afxmt.h"
#include "iostream"
using namespace std;
int array1[10];
CMutex Section;

UINT WrtThrd(LPVOID param)
{
Section.Lock();
for (int x = 0; x < 10; x++)
{
  array1[x] = x + 6;
  printf("first %d\n", array1[x]);

}
    Sleep(80);
Section.Unlock();
return 0;
}
int main(int argc, char* argv[])
{
    DWORD ThrdID;
    CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WrtThrd, NULL, 0, &ThrdID);
    Sleep(15);
    Section.Lock(); //获取互斥对象
    for (int x = 0; x < 10; x++)
        printf("second %d\n", array1[x]);
    Section.Unlock(); //释放互斥对象
    return 0;
}

输出: 

        二:事件对象 (Event)    

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

  • 使用计数;
  • 用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值;
  • 用于指明该事件处于已通知状态还是未通知状态的布尔值。

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

1)创建事件对象 

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

HANDLE CreateEvent(   
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性   
BOOL bManualReset,   // 复位方式  TRUE 必须用ResetEvent手动复原  FALSE 自动还原为无信号状态
BOOL bInitialState,   // 初始状态   TRUE 初始状态为有信号状态  FALSE 无信号状态
LPCTSTR lpName     //对象名称  NULL  无名的事件对象 
);

2. 设置事件对象状态

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

3. 重置事件对象状态

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

4. 请求事件对象

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

代码示例 

下面这段程序是一段火车售票:线程A和B会不停的购票直到票数小于0,执行完毕。在判断票数前会先申请事件对象,购票结束或者票数小于0时则会释放事件对象(事件对象置位有信号)。因为我们使用了事件对象。两个线程会按某一顺序购票,直到票数小于0。     

#include<iostream>
#include<Windows.h>
#include<process.h>
using namespace std;

//火车站卖票
int iTickets = 100;//总票数
HANDLE g_hEvent;


unsigned WINAPI SellTicketA(void* lpParam) {

    while (true) {
        WaitForSingleObject(g_hEvent, INFINITE);
        if (iTickets > 0) {
            Sleep(1);
            printf("A买了一张票,剩余%d\n", iTickets--);
        }
        else {
            SetEvent(g_hEvent);
            break;
        }
        SetEvent(g_hEvent);
    }
    return 0;
}

unsigned WINAPI SellTicketB(void* lpParam) {
    while (true) {
        WaitForSingleObject(g_hEvent, INFINITE);
        if (iTickets > 0) {
            Sleep(1);
            printf("B买了一张票,剩余%d\n", iTickets--);
        }
        else {
            SetEvent(g_hEvent);
            break;
        }
        SetEvent(g_hEvent);
    }
    return 0;
}

int main() {

    HANDLE hThreadA, hThreadB;
    hThreadA = (HANDLE)_beginthreadex(NULL, 0, SellTicketA, NULL, 0, NULL);
    hThreadB = (HANDLE)_beginthreadex(NULL, 0, SellTicketB, NULL, 0, NULL);

    CloseHandle(hThreadA); //只是关闭了一个线程句柄对象,表示我不再使用该句柄,即不对这个句柄对应的线程做任何干预了。并没有结束线程。
    CloseHandle(hThreadB);


    printf("Before CreateEvent\n");
    g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    SetEvent(g_hEvent);
    Sleep(4000);
    CloseHandle(g_hEvent);
    system("pause");
    return 0;
}

 输出结果如下:

三、资源信号量

         信号量(semaphore)是操作系统用来解决并发中的互斥和同步问题的一种方法。与互斥量不同的地方是,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。

        创建信号量函数:

        HANDLE    WINAPI    
CreateSemaphoreW(
    _In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,  // Null 安全属性
    _In_ LONG lInitialCount,  //初始化时,共有多少个资源是可以用的。 0:未触发状//态(无信号状态),表示没有可用资源
    _In_ LONG lMaximumCount,  //能够处理的最大的资源数量   
    _In_opt_ LPCWSTR lpName   //NULL 信号量的名称
);

  • 第一个参数表示安全属性,这是创建内核对象函数都会有的参数,NULL表示默认安全属性
  • 第二个参数表示初始时有多少个资源可用,0表示无任何资源(未触发状态)
  • 第三个参数表示最大资源数
  • 第四个参数表示信号量的名称,NULL表示无名称的信号量对象

        //  等待信号量

        信号灯有一个初始值,表示有多少进程/线程可以进入,当信号灯的值大于 0 时为有信号状态,小于等于 0 时为无信号状态,所以可以利用 WaitForSingleObject 进行等待,当 WaitForSingleObject 等待成功后信号灯的值会被减少 1,直到释放时信号灯会被增加 1 。

        DWORD WaitForMultipleObjects(
          DWORD nCount,                     // 等待的对象数量
          CONST HANDLE *lpHandles,  // 对象句柄数组指针
          BOOL fWaitAll,                        // 等待方式,
          //为TRUE表示等待全部对象都变为有信号状态才返回,为FALSE表示任何一个对象变为有信号状态则返回
          DWORD dwMilliseconds         // 超时设置,以ms为单位,如果为INFINITE表示无限期的等待
); 

        增加/释放信号量

        ReleaseSemaphore(
    _In_ HANDLE hSemaphore,   //信号量的句柄
    _In_ LONG lReleaseCount,   //将lReleaseCount值加到信号量的当前资源计数上面 0-> 1
    _Out_opt_ LPLONG lpPreviousCount  //当前资源计数的原始值
);

  • 第一个参数表示信号量句柄,也就是调用创建信号量函数时返回的句柄
  • 第二个参数表示释放的信号量个数,该值必须大于0,但不能大于信号量的最大计数
  • 第三个参数表示指向要接收信号量的上一个计数的变量的指针。如果不需要上一个计数, 则此参数可以为NULL 。

        关闭句柄 :

        CloseHandle(
            _In_ _Post_ptr_invalid_ HANDLE hObject );

        代码示例:

        下面这段程序创建了两个信号资源,其最大资源都为1;一个初始资源为0,另一个初始资源为1。线程中的for循环每执行一次会将另一个要申请的信号资源的可用资源数+1。因此程序的执行结果为两个线程中的for循环交替执行。 

#include<iostream>
#include<Windows.h>
#include<process.h>
using namespace std;

static HANDLE semOne;
static HANDLE semTwo;
static int num;

/*
* 信号资源semOne初始为0,最大1个资源可用
* 信号资源semTwo初始为1,最大1个资源可用
*/

unsigned WINAPI Read(void* arg) {
    int i;
    for (i = 0; i < 5; i++) {
        fputs("Input num:\n", stdout);
        printf("begin read\n");
        WaitForSingleObject(semTwo, INFINITE);
        printf("beginning read\n");
        scanf_s("%d", &num);
        ReleaseSemaphore(semOne, 1, NULL);
    }
    return 0;
}

unsigned WINAPI Accu(void* arg) {
    int sum = 0, i;
    for (i = 0; i < 5; ++i) {
        printf("begin Accu\n");
        WaitForSingleObject(semOne, INFINITE);
        printf("beginning Accu\n");
        sum += num;
        printf("sum=%d\n", sum);
        ReleaseSemaphore(semTwo, 1, NULL);
    }
    return 0;
}

int main() {
    HANDLE hThread1, hThread2;
    semOne = CreateSemaphore(NULL, 0, 1, NULL);//初始值没有可用资源
    semTwo = CreateSemaphore(NULL, 1, 1, NULL);//初始值有一个可用资源


    hThread1 = (HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);
    hThread2 = (HANDLE)_beginthreadex(NULL, 0, Accu, NULL, 0, NULL);
    WaitForSingleObject(hThread1, INFINITE);
    WaitForSingleObject(hThread2, INFINITE);

    CloseHandle(semOne);
    CloseHandle(semTwo);
    system("pause");
    return 0;
}

 

二:用户态下的同步方式:

        一 、关键代码

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

        1、初始化关键代码段

        调用Initialize CriticalSection函数初始化一个关键代码段:

       Initialzie CriticalSection(  _Out_ LPRRITICAL_SECTION lpCriticalSection );

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

        2、进入关键代码

        VOID WINAPI EnterCriticalSection( _Inout_ LPCRITICAL_SECTION lpCriticalSection);
        3、退出关键代码段

        VOID WINAPI LeaveCriticalSection( _Inout_ LPCRITICAL_SECTION lpCriticalSection);
         4、删除临界区

WINBASEAPI VOID WINAPI DeleteCriticalSection(  _Inout_ LPCRITICAL_SECTION lpCriticalSection);

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

程序实例:

        下面这段程序同样也是火车售票,其工作逻辑与上面的事件对象基本吻合。

#include<iostream>
#include<Windows.h>
#include<process.h> 
using namespace std;

int iTickets = 100;
CRITICAL_SECTION g_cs;

//A窗口
DWORD WINAPI SellTicketA(void* lpParam) {
    while (1) {
        EnterCriticalSection(&g_cs);//进入临界区
        if (iTickets > 0) {
            iTickets--;
            printf("A买了一张票,剩余票数为:%d\n", iTickets);
        }
        else {
            LeaveCriticalSection(&g_cs);
            break;
        }
        LeaveCriticalSection(&g_cs);
        Sleep(10);
    }
    return 0;
}

//B窗口
DWORD WINAPI SellTicketB(void* lpParam) {
    while (1) {
        EnterCriticalSection(&g_cs);
        if (iTickets > 0) {
            iTickets--;
            printf("B买了一张票,剩余票数为:%d\n", iTickets);
        }
        else {
            LeaveCriticalSection(&g_cs);
            break;
        }
        LeaveCriticalSection(&g_cs);
        Sleep(10);
    }
    return 0;
}

int main() {
    HANDLE hThreadA, hThreadB;
    hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL);
    hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL);

    CloseHandle(hThreadA);
    CloseHandle(hThreadB);

    InitializeCriticalSection(&g_cs);//初始化关键代码
    Sleep(6000);

    DeleteCriticalSection(&g_cs);
    system("pause");
    return 0;
}

 

         

  • 21
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值