C语言多线程编程 (二)

C语言实现线程的之间的同步和互斥,通过临界区、互斥量、事件、信号量实现线程的同步互斥,同时讲解一下进程之间的同步互斥。

临界区实现同步互斥

  1. 首先我们要声明一个临界区的变量,利用临界区变量来实现临界区。

    1
    CRITICAL_SECTION global_cirtical_sectioin;
  2. 使用临界区变量需要先初始临界区变量。

    1
    InitializeCriticalSection(&global_cirtical_sectioin);
  3. 进入临界区,利用临界区变量上锁。

    1
    EnterCriticalSection(&global_cirtical_sectioin);
  4. 出临界区,解锁。

    1
    LeaveCriticalSection(&global_cirtical_sectioin);
  5. 使用结束后,删除临界变量。

    1
    DeleteCriticalSection(&global_cirtical_sectioin);

完成的使用流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
CRITICAL_SECTION global_cirtical_sectioin;
unsigned int WINAPI thread_task(LPVOID paramter)
{
// 进入临界区
EnterCriticalSection(&global_thread_parameter);
// 临界区代码
...
// 出临界区
LeaveCriticalSection(&global_thread_parameter);
return 0;
}
void critical_zone_synchronization()
{
HANDLE hThread[5];
// 初始化临界区变量
InitializeCriticalSection(&global_thread_code);
for (int i = 0; i < 5; i++) {
hThread[i] = (HANDLE)_beginthreadex(NULL, 0, thread_task, NULL, 0, NULL);
}
WaitForMultipleObjects(5, hThread, TRUE, INFINITE);
for (int i = 0; i < 5; i++) {
CloseHandle(hThread[i]);
}
DeleteCriticalSection(&global_thread_code);
}

这里的临界区的设置,只允许同一时间只有一个线程能够执行临界区代码。

互斥量实现同步互斥

大致步骤和临界区相同。

  1. 初始化互斥量。

    1
    HANDLE mutex = CreateMutex(NULL, FALSE, NULL);

    CreateMutex的作用是找出当前系统是否存在指定进程的示例,如果没有则创建一个互斥体。

    • 互斥对象是系统内核维护的一种数据结构,保证对象对单个线程的访问权。
    • 互斥对象结构包括:使用数量(多少个线程调用该对象),线程ID(互斥对象的维护线程ID),计数器(当前线程调用该对象的次数)。
    • 参数说明:
    1
    2
    3
    4
    5
    CreateMutexA(
    _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, // 指向安全属性的指针
    _In_ BOOL bInitialOwner, // 初始化互斥对象的所有者 如果为 TRUE 表示互斥量为创建线程所有
    _In_opt_ LPCSTR lpName // 指向互斥对象名的指针
    );
  2. 等待互斥量被触发。

    1
    WaitForSingleObject(mutex, INFINITE);
  3. 触发互斥量。

    1
    ReleaseMutex(mutex);
  4. 撤销互斥量。

    1
    CloseHandle(mutex);

事件实现同步互斥

  1. 初始化事件

    1
    HANDLE event = CreateEvent(NULL, false, false, NULL);
    1
    2
    3
    4
    5
    6
    HANDLE CreateEvent(
    LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性结构体指针
    BOOL bManualReset, // 是否手动复位
    BOOL bInitialState, // 初始状态
    LPCTSTR lpName // 事件名称
    );
    • lpEventAttributes:一个指向SECURITY_ATTRIBUTES结构体的指针,用于设置事件对象的安全属性。如果该参数为NULL,则事件对象会继承调用进程的安全属性。
    • bManualReset:一个布尔值,用于指定事件对象的类型。如果该参数为TRUE,则表示创建的是手动复位事件对象;如果该参数为FALSE,则表示创建的是自动复位事件对象。手动复位事件对象必须通过调用ResetEvent函数来将事件状态复位(即重置为未激发状态);而自动复位事件对象则会在有信号触发时自动将其状态复位。
    • bInitialState:一个布尔值,用于指定事件对象的初始状态。如果该参数为TRUE,则表示在创建事件对象时立即将其设置为已激发状态(signaled);如果该参数为FALSE,则表示事件对象初始状态为未激发状态(nonsignaled)。
    • lpName:一个字符串指针,用于指定事件对象的名称。如果该参数为NULL,则表示创建一个无名事件对象;否则表示创建命名的事件对象。需要注意的是,如果同时存在同名的命名事件对象,则会返回该事件对象的句柄。
  2. 等待事件发生

    1
    WaitForSingleObject(event, INFINITE);
  3. 触发事件

    1
    SetEvent(event);
  4. 撤销事件

    1
    CloseHandle(event);

信号量同步

  1. 初始化信号量

    1
    2
    // 当前0个资源、最大允许1个同时访问
    HANDLE semaphore = CreateSemaphore(NULL, 0, 1, NULL);
  2. 等待$信号量>1$

    1
    WaitForSingleObject(semaphore, INFINITE);
  3. 释放一个资源,信号量加1

    1
    ReleaseSemaphore(semaphore, 1, NULL);
  4. 撤销信号量

    1
    CloseHandle(semaphore);

进程之间互斥信号量实现互斥

和线程之间原理一样,这里多了一步需要打开互斥对象,互斥对象有操作内核管理,通过名称查找到。

互斥对象存在时,同名的进程时不能运行的。

  1. 创建互斥量

    1
    HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEX_NAME);
  2. 打开互斥量

    1
    HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, TRUE, MUTEX_NAME);
  3. 触发互斥量

    1
    ReleaseMutex(hMutex);
  4. 等待互斥量触发

    1
    WaitForSingleObject(hMutex, 10000);

存在问题

WaitForSingleObject()

WaitForSingleObject函数的执行流程如下:

  1. 线程调用WaitForSingleObject函数并传入需要等待的对象的句柄以及等待时间的长度。
  2. 系统检查对象的当前状态:
    • 如果对象已经是 signaled 状态(已激发),则直接返回WAIT_OBJECT_0,线程可以继续执行后续操作。
    • 如果对象不是 signaled 状态(未激发),则线程进入等待状态,并将该线程从可执行状态转换为等待状态,直到以下三种情况之一发生:
      • 对象被激发,即对象状态变为 signaled。此时,线程会被唤醒,并返回WAIT_OBJECT_0,线程可以继续执行后续操作。
      • 等待超时,即指定的等待时间到达。此时,线程会被唤醒,并返回WAIT_TIMEOUT,线程可以根据需要进行相应处理。
      • 等待的对象被放弃。这通常出现在使用互斥体时,当互斥体的所有者线程意外终止而没有正确释放互斥体时,其他线程在等待该互斥体时会返回WAIT_ABANDONED
  3. 线程根据返回值进行相应的处理。

不同线程之间临界区和互斥量不起作用