亲宝软件园·资讯

展开

C++实现线程同步的四种方式总结

霸道小明 人气:0

内核态

互斥变量 

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

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

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

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

创建互斥对象函数

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

代码示例

下面这段程序声明了一个全局整型变量,并初始化为0。一个线程函数对这个变量进行+1操作,执行50000次;另一个线程函数对这个变量-1操作,执行50000次。两个线程函数各创建25个。因为我们使用了互斥变量,50个线程会按照一定顺序对这变量操作,因此最后结果为0。 

#include <stdio.h>
#include <windows.h>
#include <process.h>
 
#define NUM_THREAD    50
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);
    printf("sizeof long long: %d \n", sizeof(long long));
    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 < 500000; i++)
        num += 1;
    //释放
    ReleaseMutex(hMutex);
    return 0;
}
unsigned WINAPI threadDes(void* arg){
    int i;
    //请求
    WaitForSingleObject(hMutex, INFINITE);
    for (i = 0; i < 500000; i++)
        num -= 1;
    //释放
    ReleaseMutex(hMutex);
    return 0;
}

事件对象

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

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

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); 
 
    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,  //能够处理的最大的资源数量   3
    _In_opt_ LPCWSTR lpName   //NULL 信号量的名称
);

增加/释放信号量

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

关闭句柄

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("%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.初始化关键代码段

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

InitialzieCriticalSection(
    _Out_ LPRRITICAL_SECTION lpCriticalSection
);

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

2进入关键代码

VOID
WINAPI
EnterCriticalSection(
    _Inout_ LPCRITICAL_SECTION lpCriticalSection
);

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

3.退出关键代码段

VOID
WINAPI
LeaveCriticalSection(
    _Inout_ LPCRITICAL_SECTION lpCriticalSection
);

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

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) {
            Sleep(1);
            iTickets--;
            printf("A买了一张票,剩余票数为:%d\n", iTickets);
            LeaveCriticalSection(&g_cs);
        }
        else {
            LeaveCriticalSection(&g_cs);
            break;
        }
    }
    return 0;
}
 
//B窗口
DWORD WINAPI SellTicketB(void* lpParam) {
    while (1) {
        EnterCriticalSection(&g_cs);
        if (iTickets > 0) {
            Sleep(1);
            iTickets--;
            printf("B买了一张票,剩余票数为:%d\n", iTickets);
            LeaveCriticalSection(&g_cs);
        }
        else {
            LeaveCriticalSection(&g_cs);
            break;
        }
    }
    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(1000);
 
    DeleteCriticalSection(&g_cs);
    system("pause");
    return 0;
}

加载全部内容

相关教程
猜你喜欢
用户评论