之前我们给大家介绍过用户模式下的线程同步方案。主要有Interlocked系列函数、关键段、旋转锁、读写锁和条件变量。用户模式的优点是同步速度非常快,性能优异,只需要非常短的CPU周期即可完成同步。但是也有其局限性,就是用户模式下的线程同步只能在局限在单进程,无法跨进程同步线程。

为了实现跨进程的线程同步,Windows允许我们通过内核对象的方式实现同步。可以用来同步的内核对象主要有事件、可等待定时器、互斥对象、信号量。这些内核状态都有两种状态,一种是未触发状态,一种是已触发状态。当内核对象处于未触发状态的时候,试图等待内核对象的线程都会被阻塞,直到内核对象被其他线程触发。

等待内核对象触发的函数

WaitForSingleObjectWaitForMultipleObjects是Windows提供的两个等待内核对象触发的函数,这两个函数会使线程进入等待状态,直到指定的内核对象变为已触发状态,线程才会继续往下执行。其中WaitForSingleObject用于等待单个内核对象变为已触发,WaitForMultipleObjects用户等待一组内核对象种的单个或者全部变为已触发状态。

WaitForSingleObject的原型如下:

DWORD

WaitForSingleObject(
    HANDLE hHandle,//要等待的内核对象句柄
    DWORD dwMilliseconds//等待的时长,INFINITE表示无限等待
    );

WaitForMultipleObjects的原型如下:

DWORD

WaitForMultipleObjects(
    DWORD nCount,//要等待的内核对象的数量
    CONST HANDLE* lpHandles,//要等待的内核对象的数组
    BOOL bWaitAll,//指定是否等待所有对象都变为已触发状态。TRUE:所有内核对象都变为已触发状态的时候,线程才会继续执行。
    DWORD dwMilliseconds//等待的时长,INFINITE表示无限等待
    );

接下来我们来依次介绍用于线程同步的内核对象。

事件内核对象

事件内核对象Event是最基本也是最简单的内核对象。每个事件对象有一个布尔值表示事件为自动重置事件还是手动重置事件,还有一个布尔值用于表示事件的状态(已触发状态或者未触发)。

通过CreateEvent或者CreateEventEx函数可以创建一个事件内核对象,如果创建成功,函数会返回事件内核对象的句柄。

CreateEvent的函数原型如下:

HANDLE CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes,//内核事件安全属性,所有内核对象都有。默认为NULL即可
  BOOL                  bManualReset,//是否为手动重置事件,TRUE:手动  FALSE:自动
  BOOL                  bInitialState,//事件默认状态 TRUE:已触发 FALSE:未触发
  LPCSTR                lpName//事件对象名称
);

lpEventAttributes: 指向一个 SECURITY_ATTRIBUTES 结构的指针,指定返回的句柄是否可以被子进程继承。如果为 NULL,则事件对象不能被子进程继承。
bManualReset: 指定事件对象是手动重置还是自动重置。如果为 TRUE,事件对象是手动重置的;如果为 FALSE,事件对象是自动重置的(自动重置为未触发状态)。
bInitialState: 指定事件对象的初始状态。如果为 TRUE,事件对象的初始状态为有信号状态;如果为 FALSE,事件对象的初始状态为无信号状态。
lpName: 指向以 null 结尾的字符串,指定事件对象的名称。如果为 NULL,则事件对象是未命名的。

这里需要给大家介绍一下自动重置事件和手动重置事件的区别。他们主要区别是,当事件变为已触发状态的时候,是否会自从重置事件为为触发。

对于手动重置事件来说,所有调用WaitForSingleObject的线程都会被唤醒开始执行。

对于自动重置事件来说,所有调用WaitForSingleObject的线程中,只有一个线程会被唤醒往下执行。

下面代码,创建了一个手动重置的事件对象MyEvent,我们用两个线程分别来等待这个事件内核对象。当手动调用SetEvent 设置事件状态为已触发的时候,线程1和线程2会同时收到信号,并且继续往下执行。

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

// 全局事件句柄
HANDLE hEvent;

void ThreadFunction1()
{
    std::cout << "子线程1等待事件信号..." << std::endl;
    // 等待事件对象变为有信号状态
    WaitForSingleObject(hEvent, INFINITE);
    std::cout << "子线程1收到事件信号,继续执行..." << std::endl;
}

void ThreadFunction2()
{
    std::cout << "子线程2等待事件信号..." << std::endl;
    // 等待事件对象变为有信号状态
    WaitForSingleObject(hEvent, INFINITE);
    std::cout << "子线程2收到事件信号,继续执行..." << std::endl;
}

int main()
{
    // 创建一个手动重置事件对象,初始状态为无信号
    hEvent = CreateEvent(NULL, TRUE, FALSE, TEXT("MyEvent"));
    if (hEvent == NULL)
    {
        std::cerr << "CreateEvent failed (" << GetLastError() << ")" << std::endl;
        return 1;
    }

    // 创建一个线程
    std::thread t1(ThreadFunction1);
    std::thread t2(ThreadFunction2);
    // 主线程等待一段时间,然后设置事件对象为有信号状态
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "主线程设置事件为有信号状态" << std::endl;
    SetEvent(hEvent);

    //等待子线程完成
    t1.join();
    t2.join();
    // 关闭事件句柄
    CloseHandle(hEvent);

    system("pause");
    return 0;
}

事件对象可以跨进程使用,我们可以在其他进程或者当前进程中,通过OpenEvent打开事件对象,然后进行线程间的同步。

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

int main()
{
    HANDLE hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE, TEXT("Local\\MyEvent"));
	WaitForSingleObject(hEvent, INFINITE);
    system("pause");
    system("pause");
}

互斥体内核对象

互斥体(Mutex)是 Windows 提供的一种内核对象,用于线程同步。互斥体确保在同一时间只有一个线程可以访问共享资源,从而防止数据竞争和不一致性。互斥体可以跨进程使用,因此适用于需要跨进程同步的场景。

使用CreateMutex创建互斥体对象,原型如下:

互斥体的主要操作步骤:
1.    创建互斥体:使用 CreateMutex 函数创建一个互斥体对象。
2.    获取互斥体:使用 WaitForSingleObject 或 WaitForMultipleObjects 函数等待互斥体对象变为有信号状态,从而获取互斥体。
3.    释放互斥体:使用 ReleaseMutex 函数释放互斥体,使其他等待的线程可以获取互斥体。
4.    关闭互斥体:使用 CloseHandle关闭互斥体对象。

下面代码我们创建一个互斥体,用两个多线程等待互斥体,两个线程会依次按顺序获取互斥体访问权限,然后执行代码。

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

// 全局互斥体句柄
HANDLE hMutex;

void ThreadFunction1()
{
    std::cout << "子线程1等待互斥体..." << std::endl;
    // 等待互斥体对象
    WaitForSingleObject(hMutex, INFINITE);
    std::cout << "子线程1获得互斥体,开始执行..." << std::endl;
    // 模拟一些工作......
    std::cout << "子线程1释放互斥体..." << std::endl;
    // 释放互斥体
    ReleaseMutex(hMutex);
}

void ThreadFunction2()
{
    std::cout << "子线程2等待互斥体..." << std::endl;
    // 等待互斥体对象
    WaitForSingleObject(hMutex, INFINITE);
    std::cout << "子线程2获得互斥体,开始执行..." << std::endl;

    // 模拟一些工作......

    std::cout << "子线程2释放互斥体..." << std::endl;
    // 释放互斥体
    ReleaseMutex(hMutex);
}

int main()
{
    // 创建一个互斥体对象
    hMutex = CreateMutex(NULL, FALSE, L"Local\\MyMutex");
    if (hMutex == NULL)
    {
        std::cerr << "CreateMutex failed (" << GetLastError() << ")" << std::endl;
        return 1;
    }

    // 创建两个线程
    std::thread t1(ThreadFunction1);
    std::thread t2(ThreadFunction2);

    // 等待子线程完成
    t1.join();
    t2.join();

    system("pause");
    // 关闭互斥体句柄
    CloseHandle(hMutex);

    system("pause");
    return 0;
}

我们可以在其他进程或者当前进程中打开互斥体对象CreateMutex或者OpenMutex,获取到互斥体的句柄,然后进行线程间同步。

#include <windows.h>
#include <iostream>
int main()
{

    HANDLE hMutex = CreateMutex(NULL, FALSE, L"Local\\MyMutex");
    system("pause");
    system("pause");
}

信号量互斥体对象

信号量允许多个线程同时访问共享资源,但可以限制同时访问资源的线程数量。信号量通常用于控制对资源的访问数量,例如限制同时访问某个资源的线程数。每个信号量内核对象都会维护一个可访问资源的计数,我们需要设置一个最大可访问 的资源计数。

使用CreateSemaphore创建互斥体内核对象,原型如下:

HANDLE CreateSemaphore(
  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,//内核对象安全描述符号
  LONG                  lInitialCount,//当前可以使用的计数。必须在 0 到 lMaximumCount 之间。
  LONG                  lMaximumCount,//信号量对象最大的计数
  LPCSTR                lpName//信号量名称
);

下面代码创建一个最大访问计数为2的互斥体并且设置默认计数为2。然后用三个线程依次使用信号量。执行结果的时候,我们会看见最后一个线程(第三个)执行的时候,需要等待。

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

// 全局信号量句柄
HANDLE hSemaphore;

void ThreadFunction(int threadNum)
{
    std::cout << "线程 " << threadNum << " 等待信号量..." << std::endl;
    // 等待信号量对象
    WaitForSingleObject(hSemaphore, INFINITE);
    std::cout << "线程 " << threadNum << " 获得信号量,开始执行..." << std::endl;

    // 模拟一些工作
    std::this_thread::sleep_for(std::chrono::seconds(2));

    std::cout << "线程 " << threadNum << " 释放信号量..." << std::endl;
    // 释放信号量
    ReleaseSemaphore(hSemaphore, 1, NULL);
}

int main()
{
    // 创建一个信号量对象,初始计数为2,最大计数为2
    hSemaphore = CreateSemaphore(NULL, 2, 2, L"Local\\MySemaphore");
    if (hSemaphore == NULL)
    {
        std::cerr << "CreateSemaphore failed (" << GetLastError() << ")" << std::endl;
        return 1;
    }

    // 创建多个线程
    std::thread t1(ThreadFunction, 1);
    std::thread t2(ThreadFunction, 2);
    std::thread t3(ThreadFunction, 3);

    // 等待子线程完成
    t1.join();
    t2.join();
    t3.join();

    // 关闭信号量句柄
    //CloseHandle(hSemaphore);

    system("pause");
    return 0;
}

其他内核对象

对于内核对象来说,都可以使用WaitForSingleObject函数进行阻塞等待,实现线程同步的效果。除了上面说的事件对象、互斥体对象、信号量之外,对于常用的内核对象,如进程、线程、文件、可等待计时器、作业等内核对象也可以实现线程间的同步效果。