线程同步是指在多线程编程中,为了保证多个线程之间的数据访问和操作的有序性以及正确性,需要采取一些机制来协调它们的执行。在多线程环境下,由于线程之间是并发执行的,可能会出现竞争条件(Race Condition)等问题,从而导致程序的不稳定性和错误。
示例1:
#include
#include
int g;
DWORD WINAPI MyThreadProc (LPVOID lp){
for(int i = 0; i < 100000000; i++){
g++;
}
return 0;
}
int main(){
HANDLE h = CreateThread(NULL, 0, MyThreadProc, NULL, 0, 0);
for(int i = 0; i < 100000000; i++){
g++;
}
std::cout << g << std::endl;
CloseHandle(h);
return 0;
}
结果:
这里测试了两个次,当多个线程同时对公共资源进行操作时,会发生错误,该示例结果始终处在
100000000~200000000之间
示例2:
因此在这里继续添加一个等待线程函数(WaitForSingleObject)等待线程结束试试
发现依旧无法达到预期。
猜测二:线程与线程之间存在同步,使得g在某一个或某一些值时,g在主线程与线程中,只增加一次。
1. 互斥锁(Mutex):互斥锁是一种保护共享资源的机制,它确保在任意时刻只有一个线程能够访问被保护的资源。当一个线程获得了互斥锁,其他线程就需要等待锁的释放才能访问资源。
2. 信号量(Semaphore):信号量是一种用于控制同时访问某一资源的线程数目的方法。它可以允许多个线程同时访问资源,但是可以通过信号量的计数来控制同时访问的线程数量。
3. 条件变量(Condition Variable):条件变量用于在某些特定条件下使线程等待或唤醒。它通常与互斥锁一起使用,以实现在满足特定条件时线程的阻塞和唤醒。
4. 读写锁(Read-Write Lock):读写锁允许多个线程同时读取共享资源,但是在写操作时需要互斥锁来保护资源,以避免多个线程同时写入导致数据不一致性。
5. 原子操作(Atomic Operations):原子操作是一种不可分割的操作,能够保证在多线程环境下的执行不会被中断,从而避免竞争条件。
线程同步的目的是确保线程之间的协调和有序执行,以避免数据竞争和其他并发问题。选择合适的线程同步机制取决于具体的应用场景和需求。
PS:临界区同样存在计数机制,进入几次临界区,就要退出几次临界区
示例:
#include
#include
int g;
CRITICAL_SECTION g_cs;//创建互斥锁
DWORD WINAPI MyThreadProc (LPVOID lp){
for(int i = 0; i < 100000000; i++){
EnterCriticalSection(&g_cs); //进入临界区
g++;
LeaveCriticalSection(&g_cs); //离开临界区
}
return 0;
}
int main(){
InitializeCriticalSection(&g_cs);//初始化互斥锁
HANDLE h = CreateThread(NULL, 0, MyThreadProc, NULL, 0, 0);
for(int i = 0; i < 100000000; i++){
EnterCriticalSection(&g_cs); //进入临界区
g++;
LeaveCriticalSection(&g_cs); //进入临界区
}
WaitForSingleObject(h, INFINITE);
std::cout << g << std::endl;
CloseHandle(h);
DeleteCriticalSection(&g_cs);
return 0;
}
结果:
需要注意的是,过多地使用互斥锁可能会导致性能问题,因为只有一个线程能够执行临界区代码,其他线程需要等待
信号量用于限制多个线程对共享资源的访问数量。在 Windows 中,你可以使用 CreateSemaphore
或 std::semaphore
来创建信号量。
CreateSemaphore函数:用来创建信号
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 指向安全描述符的指针,控制信号量的安全性,通常设置为 NULL
LONG lInitialCount, // 初始计数值,即初始的信号量计数
LONG lMaximumCount, // 最大计数值,即信号量的最大值
LPCTSTR lpName // 信号量的名称,可以为 NULL
);
初始计数(Initial Count): 初始计数是信号量在创建时的初始状态。它指定了在创建信号量时可以立即使用的可用信号量数量。初始计数告诉你在开始时有多少个许可证(信号量),可供线程或进程使用。
最大计数(Maximum Count): 最大计数指定了信号量允许的最大并发访问数量。它限制了在信号量上等待的线程或进程的数量。当信号量的计数达到最大值时,任何试图等待该信号量的线程将会被阻塞,直到有其他线程释放信号量。
ReleaseSemaphore函数: 释放信号量
BOOL ReleaseSemaphore(
HANDLE hSemaphore, // 信号量的句柄
LONG lReleaseCount, // 释放的信号量计数值
LPLONG lpPreviousCount // 指向变量的指针,用于存储之前的信号量计数值(可选)
);
信号量为0时,表示无信号,返回false;反之,整数表示有信号,返回true;
示例:
#include
#include
int g;
HANDLE semaphore;
DWORD WINAPI myThreadProc(LPVOID lp) {
WaitForSingleObject(semaphore, INFINITE);
for (int i = 0; i < 10000000; i++) {
g++;
}
ReleaseSemaphore(semaphore, 1, NULL);
return 0;
}
int main() {
semaphore = CreateSemaphore(NULL, 1, 1, NULL);
HANDLE h = CreateThread(NULL, 0, myThreadProc, NULL, 0, NULL);
WaitForSingleObject(semaphore, INFINITE);
for (int i = 0; i < 10000000; i++) {
g++;
}
ReleaseSemaphore(semaphore, 1, NULL);
WaitForSingleObject(h, INFINITE);
std::cout << g << std::endl;
CloseHandle(h);
CloseHandle(semaphore);
return 0;
}
结果:
友情提醒:如果把WaitForSignalObject()放在循环里面,会特别慢,因为每一次循环都要等待信号与释放信号
在 Windows 平台上,条件变量的常见实现是使用事件对象(Event Object)来实现。下面是一个使用事件对象作为条件变量的示例:
#include
#include
int g;
HANDLE event;
DWORD WINAPI MyThreadProc(LPVOID lp)
{
for (int i = 0; i < 10000000; i++)
{
g++;
}
SetEvent(event); // 设置事件,通知主线程
return 0;
}
int main()
{
event = CreateEvent(NULL, FALSE, FALSE, NULL); // 创建事件对象,初始状态为未触发状态
HANDLE h = CreateThread(NULL, 0, MyThreadProc, NULL, 0, NULL);
WaitForSingleObject(event, INFINITE); // 等待事件触发
for (int i = 0; i < 10000000; i++)
{
g++;
}
SetEvent(event);
CloseHandle(h);
CloseHandle(event);
std::cout << g << std::endl;
return 0;
}
CreateEvent函数参数如下:
lpEventAttributes
:一个指向SECURITY_ATTRIBUTES
结构的指针,用于指定事件对象的安全属性。可以设置为NULL
,表示使用默认的安全属性。
bManualReset
:一个布尔值,指定事件对象的重置方式。
- 如果为
TRUE
,则表示事件对象是手动重置的。这意味着当一个线程通过调用SetEvent
函数将事件设置为触发状态后,事件将保持触发状态,直到某个线程显式地通过调用ResetEvent
函数将其重置为非触发状态。- 如果为
FALSE
,则表示事件对象是自动重置的。这意味着当一个线程通过调用SetEvent
函数将事件设置为触发状态后,事件将自动在第一个等待线程成功等待后被重置为非触发状态。
bInitialState
:一个布尔值,指定事件对象的初始状态。
- 如果为
TRUE
,表示事件对象初始状态为触发状态(对于手动重置和自动重置事件都一样)。- 如果为
FALSE
,表示事件对象初始状态为非触发状态。
lpName
:一个指向以 null 结尾的字符串的指针,用于指定事件对象的名称。可以设置为NULL
,表示事件没有名称。
示例如下:
#include
#include
SRWLOCK srwLock; // SRW 锁对象
DWORD WINAPI ReaderThread(LPVOID lp)
{
AcquireSRWLockShared(&srwLock); // 获取共享锁(读锁)
// 读取共享资源...
ReleaseSRWLockShared(&srwLock); // 释放共享锁
return 0;
}
DWORD WINAPI WriterThread(LPVOID lp)
{
AcquireSRWLockExclusive(&srwLock); // 获取独占锁(写锁)
// 修改共享资源...
ReleaseSRWLockExclusive(&srwLock); // 释放独占锁
return 0;
}
int main()
{
InitializeSRWLock(&srwLock); // 初始化 SRW 锁对象
HANDLE readerThread = CreateThread(NULL, 0, ReaderThread, NULL, 0, NULL);
HANDLE writerThread = CreateThread(NULL, 0, WriterThread, NULL, 0, NULL);
WaitForSingleObject(readerThread, INFINITE);
WaitForSingleObject(writerThread, INFINITE);
CloseHandle(readerThread);
CloseHandle(writerThread);
return 0;
}
PS:再vscode下AcquireSRWLockShared()会报错,再vs下没问题
SRW(Slim Reader/Writer)是一种轻量级的读写锁
在这个示例中,我们使用了 AcquireSRWLockShared
函数来获取共享锁(读锁),并使用 ReleaseSRWLockShared
函数来释放共享锁。同时,我们使用了 AcquireSRWLockExclusive
函数来获取独占锁(写锁),并使用 ReleaseSRWLockExclusive
函数来释放独占锁。
要注意,SRW 锁适用于一种常见的读多写少的情况,它具有较低的开销。如果你需要更复杂的读写锁机制,例如支持多个读者和写者的同步,可能需要使用更高级的同步机制,如互斥锁和条件变量的组合等。