魏晓东 发表于 2023-10-11 23:02:32

9.2 运用API实现线程同步

Windows 线程同步是指多个线程一同访问共享资源时,为了避免资源的并发访问导致数据的不一致或程序崩溃等问题,需要对线程的访问进行协同和控制,以保证程序的正确性和稳定性。Windows提供了多种线程同步机制,以适应不同的并发编程场景。主要包括以下几种:

[*]事件(Event):用于不同线程间的信号通知。包括单次通知事件和重复通知事件两种类型。
[*]互斥量(Mutex):用于控制对共享资源的访问,具有独占性,可避免线程之间对共享资源的非法访问。
[*]临界区(CriticalSection):和互斥量类似,也用于控制对共享资源的访问,但是是进程内部的,因此比较适用于同一进程中的线程同步控制。
[*]信号量(Semaphore):用于基于计数器机制,控制并发资源的访问数量。
[*]互锁变量(Interlocked Variable):用于对变量的并发修改操作控制,可提供一定程度的原子性操作保证。
以上同步机制各有优缺点和适用场景,开发者应根据具体应用场景进行选择和使用。在线程同步的实现过程中,需要注意竞争条件和死锁的处理,以确保程序中的线程能协同工作,共享资源能够正确访问和修改。线程同步是并发编程中的重要基础,对于开发高效、稳定的并发应用至关重要。
9.2.1 CreateEvent

CreateEvent 是Windows API提供的用于创建事件对象的函数之一,该函数用于创建一个事件对象,并返回一个表示该事件对象的句柄。可以通过SetEvent函数将该事件对象设置为有信号状态,通过ResetEevent函数将该事件对象设置为无信号状态。当使用WaitForSingleObject或者WaitForMultipleObjects函数等待事件对象时,会阻塞线程直到事件状态被置位。对于手动重置事件,需要调用ResetEvent函数手动将事件状态置位。
CreateEvent 函数常用于线程同步和进程间通信,在不同线程或者进程之间通知事件状态的改变。例如,某个线程完成了一项任务,需要通知其它等待该任务完成的线程;或者某个进程需要和另一个进程进行协调,需要通知其它进程某个事件的发生等等。
CreateEvent 函数的函数原型如下:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL                  bManualReset,
BOOL                  bInitialState,
LPCTSTR               lpName
);参数说明:

[*]lpEventAttributes:指向SECURITY_ATTRIBUTES结构体的指针,指定事件对象的安全描述符和访问权限。通常设为NULL,表示使用默认值。
[*]bManualReset:指定事件对象的类型,TRUE表示创建的是手动重置事件,FALSE表示创建的是自动重置事件。
[*]bInitialState:指定事件对象的初始状态,TRUE表示将事件对象设为有信号状态,FALSE表示将事件对象设为无信号状态。
[*]lpName:指定事件对象的名称,可以为NULL。
CreateEvent 是实现线程同步和进程通信的重要手段之一,应用广泛且易用。在第一章中我们创建的多线程环境可能会出现线程同步的问题,此时使用Event事件机制即可很好的解决,首先在初始化时通过CreateEvent将事件设置为False状态,进入ThreadFunction线程时再次通过SetEvent释放,以此即可实现线程同步顺序执行的目的。
#include <stdio.h>
#include <process.h>
#include <windows.h>

// 全局资源
long g_nNum = 0;

// 子线程个数
const int THREAD_NUM = 10;

CRITICAL_SECTIONg_csThreadCode;
HANDLE g_hThreadEvent;

unsigned int __stdcall ThreadFunction(void *ptr)
{
int nThreadNum = *(int *)ptr;

// 线程函数中触发事件
SetEvent(g_hThreadEvent);

// 进入线程锁
EnterCriticalSection(&g_csThreadCode);
g_nNum++;
printf("线程编号: %d --> 全局资源值: %d --> 子线程ID: %d \n", nThreadNum, g_nNum, GetCurrentThreadId());

// 离开线程锁
LeaveCriticalSection(&g_csThreadCode);
return 0;
}

int main(int argc,char * argv[])
{
unsigned int ThreadCount = 0;
HANDLEhandle;

// 初始化自动将事件设置为False
g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
InitializeCriticalSection(&g_csThreadCode);

for (int each = 0; each < THREAD_NUM; each++)
{
    handle = (HANDLE)_beginthreadex(NULL, 0, ThreadFunction, &each, 0, &ThreadCount);

    // 等待线程事件被触发
    WaitForSingleObject(g_hThreadEvent, INFINITE);
}

WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);

// 销毁事件
CloseHandle(g_hThreadEvent);
DeleteCriticalSection(&g_csThreadCode);

system("pause");
return 0;
}当然了事件对象同样可以实现更为复杂的同步机制,在如下我们在创建对象时,可以设置non-signaled状态运行的auto-reset模式,当我们设置好我们需要的参数时,可以直接使用SetEvent(hEvent)设置事件状态,则会自动执行线程函数。
要创建一个manual-reset模式并且初始状态为not-signaled的事件对象,需要按照以下步骤:
首先定义一个SECURITY_ATTRIBUTES结构体变量,设置其中的参数为NULL表示使用默认安全描述符,例如。
SECURITY_ATTRIBUTES sa = {0};
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = FALSE;接着调用CreateEvent函数创建事件对象,将bManualReset和bInitialState参数设置为FALSE,表示创建manual-reset模式的事件对象并初始状态为not-signaled。例如:
HANDLE hEvent = CreateEvent(
                      &sa,         // 安全属性
                      TRUE,          // Manual-reset模式
                      FALSE,         // Not-signaled 初始状态
                      NULL         // 事件对象名称
                      );这样,我们就创建了一个名为hEvent的manual-reset模式的事件对象,初始状态为not-signaled。可以通过SetEvent函数将事件对象设置为signaled状态,通过ResetEvent函数将事件对象设置为non-signaled状态,也可以通过WaitForSingleObject或者WaitForMultipleObjects函数等待事件对象的状态变化。
#include <windows.h>
#include <stdio.h>
#include <process.h>
#define STR_LEN 100

// 存储全局字符串
static char str;

// 设置事件句柄
static HANDLE hEvent;

// 统计字符串中是否存在A
unsigned WINAPI NumberOfA(void *arg)
{
int cnt = 0;
// 等待线程对象事件
WaitForSingleObject(hEvent, INFINITE);
for (int i = 0; str != 0; i++)
{
    if (str == 'A')
      cnt++;
}
printf("Num of A: %d \n", cnt);
return 0;
}

// 统计字符串总长度
unsigned WINAPI NumberOfOthers(void *arg)
{
int cnt = 0;
// 等待线程对象事件
WaitForSingleObject(hEvent, INFINITE);
for (int i = 0; str != 0; i++)
{
    if (str != 'A')
      cnt++;
}
printf("Num of others: %d \n", cnt - 1);
return 0;
}

int main(int argc, char *argv[])
{
HANDLE hThread1, hThread2;

// 以non-signaled创建manual-reset模式的事件对象
// 该对象创建后不会被立即执行,只有我们设置状态为Signaled时才会继续
hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);
hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);

fputs("Input string: ", stdout);
fgets(str, STR_LEN, stdin);

// 字符串读入完毕后,将事件句柄改为signaled状态
SetEvent(hEvent);

WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);

// non-signaled 如果不更改,对象继续停留在signaled
ResetEvent(hEvent);

CloseHandle(hEvent);

system("pause");
return 0;
}9.2.2 CreateSemaphore

CreateSemaphore 是Windows API提供的用于创建信号量的函数之一,用于控制多个线程之间对共享资源的访问数量。该函数常用于创建一个计数信号量对象,并返回一个表示该信号量对象的句柄。可以通过ReleaseSemaphore函数将该信号量对象的计数加1,通过WaitForSingleObject或者WaitForMultipleObjects函数等待信号量对象的计数变成正数以后再将其减1,以实现对共享资源访问数量的控制。
CreateSemaphore 函数常用于实现生产者消费者模型、线程池、任务队列等并发编程场景,用于限制访问共享资源的线程数量。信号量机制更多时候被用于限制资源的数量而不是限制线程的数量,但也可以用来实现一些线程同步场景。
该函数的函数原型如下:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG                  lInitialCount,
LONG                  lMaximumCount,
LPCTSTR               lpName
);参数说明:

[*]lpSemaphoreAttributes:指向SECURITY_ATTRIBUTES结构体的指针,指定信号量对象的安全描述符和访问权限。通常设为NULL,表示使用默认值。
[*]lInitialCount:指定信号量对象的初始计数,表示可以同时访问共享资源的线程数量。
[*]lMaximumCount:指定信号量对象的最大计数,表示信号量对象的计数上限。
[*]lpName:指定信号量对象的名称,可以为NULL。
总的来说,CreateSemaphore 是实现线程同步和进程通信,控制对共享资源的访问数量的重要手段之一,如下一段演示代码片段则通过此方法解决了线程通过问题,首先调用CreateSemaphore初始化时将信号量设置一个最大值,每次进入线程函数内部时,则ReleaseSemaphore信号自动加1,如果大于指定的数值则WaitForSingleObject等待释放信号.
#include <stdio.h>
#include <process.h>
#include <windows.h>

// 全局资源
long g_nNum = 0;

// 子线程个数
const int THREAD_NUM = 10;

CRITICAL_SECTIONg_csThreadCode;
HANDLE g_hThreadParameter;

unsigned int __stdcall ThreadFunction(void *ptr)
{
int nThreadNum = *(int *)ptr;

// 信号量++
ReleaseSemaphore(g_hThreadParameter, 1, NULL);

// 进入线程锁
EnterCriticalSection(&g_csThreadCode);
g_nNum++;
printf("线程编号: %d --> 全局资源值: %d --> 子线程ID: %d \n", nThreadNum, g_nNum, GetCurrentThreadId());

// 离开线程锁
LeaveCriticalSection(&g_csThreadCode);
return 0;
}

int main(int argc,char * argv[])
{
unsigned int ThreadCount = 0;
HANDLEhandle;

// 初始化信号量当前0个资源,最大允许1个同时访问
g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL);
InitializeCriticalSection(&g_csThreadCode);

for (int each = 0; each < THREAD_NUM; each++)
{
    handle = (HANDLE)_beginthreadex(NULL, 0, ThreadFunction, &each, 0, &ThreadCount);

    // 等待信号量>0
    WaitForSingleObject(g_hThreadParameter, INFINITE);
}

// 关闭信号
CloseHandle(g_hThreadParameter);

// 等待所有进程结束
WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
DeleteCriticalSection(&g_csThreadCode);

system("pause");
return 0;
}如下所示代码片段,是一个应用了两个线程的案例,初始化信号为0,利用信号量值为0时进入non-signaled状态,大于0时进入signaled状态的特性即可实现线程同步。
执行WaitForSingleObject(semTwo, INFINITE);会让线程函数进入类似挂起的状态,当接到ReleaseSemaphore(semOne, 1, NULL);才会恢复执行。
#include <windows.h>
#include <stdio.h>

static HANDLE semOne,semTwo;
static int num;

// 线程函数A用于接收参书
DWORD WINAPI ReadNumber(LPVOID lpParamter)
{
int i;
for (i = 0; i < 5; i++)
{
    fputs("Input Number: ", stdout);

    // 临界区的开始 signaled状态
    WaitForSingleObject(semTwo, INFINITE);
   
    scanf("%d", &num);

    // 临界区的结束 non-signaled状态
    ReleaseSemaphore(semOne, 1, NULL);
}
return 0;
}

// 线程函数B: 用户接受参数后完成计算
DWORD WINAPI Check(LPVOID lpParamter)
{
int sum = 0, i;
for (i = 0; i < 5; i++)
{
    // 临界区的开始 non-signaled状态
    WaitForSingleObject(semOne, INFINITE);
    sum += num;

    // 临界区的结束 signaled状态
    ReleaseSemaphore(semTwo, 1, NULL);
}
printf("The Number IS: %d \n", sum);
return 0;
}

int main(int argc, char *argv[])
{
HANDLE hThread1, hThread2;

// 创建信号量对象,设置为0进入non-signaled状态
semOne = CreateSemaphore(NULL, 0, 1, NULL);

// 创建信号量对象,设置为1进入signaled状态
semTwo = CreateSemaphore(NULL, 1, 1, NULL);

hThread1 = CreateThread(NULL, 0, ReadNumber, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, Check, NULL, 0, NULL);

// 关闭临界区
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);

CloseHandle(semOne);
CloseHandle(semTwo);

system("pause");
return 0;
}9.2.3 CreateMutex

CreateMutex 是Windows API提供的用于创建互斥体对象的函数之一,该函数用于创建一个互斥体对象,并返回一个表示该互斥体对象的句柄。可以通过WaitForSingleObject或者WaitForMultipleObjects函数等待互斥体对象,以确保只有一个线程能够访问共享资源,其他线程需要等待该线程释放互斥体对象后才能继续访问。当需要释放互斥体对象时,可以调用ReleaseMutex函数将其释放。
CreateMutex 函数常用于对共享资源的访问控制,避免多个线程同时访问导致数据不一致的问题。有时候,互斥体也被用于跨进程同步访问共享资源。
该函数的函数原型如下:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL                  bInitialOwner,
LPCTSTR               lpName
);参数说明:

[*]lpMutexAttributes:指向SECURITY_ATTRIBUTES结构体的指针,指定互斥体对象的安全描述符和访问权限。通常设为NULL,表示使用默认值。
[*]bInitialOwner:指定互斥体的初始状态,TRUE表示将互斥体设置为有所有权的状态,FALSE表示将互斥体设置为没有所有权的状态。
[*]lpName:指定互斥体的名称,可以为NULL。
该函数是实现线程同步和进程通信,控制对共享资源的访问的重要手段之一,应用广泛且易用。
如下案例所示,使用互斥锁可以实现单位时间内,只允许一个线程拥有对共享资源的独占权限,从而实现了互不冲突的线程同步。
#include #include using namespace std;// 创建互斥锁HANDLE hMutex = NULL;// 线程函数DWORD WINAPI Func(LPVOID lpParamter){for (int x = 0; x < 10; x++){    // 请求获得一个互斥锁    WaitForSingleObject(hMutex, INFINITE);    cout
页: [1]
查看完整版本: 9.2 运用API实现线程同步