线程同步和原子操作是windows开发中必不可少的一个话题,线程同步一般分为用户模式下的线程同步和内核模式下的线程同步。其中用户模式下的线程同步方案一般无法跨进程使用,所以性能也是最好的,如果没有跨进程线程同步的需求,推荐使用用户模型下的线程同步方案。
用户模式下的线程同步方案主要有以下几种:Interlocked系列函数同步、关键段同步、旋转锁同步、关键段同步
Interlocked系列函数
Interlocked系列函数是 Windows API 提供的一组原子操作函数,用于在多线程环境中安全地操作共享变量。当我们执行这些Interlocked系列函数的时候 ,函数会对总线发出一个硬件信号,这个硬件信号可以防止CPU同一时刻访问同一个内存地址,从而大达到原子访问的目的。
Interlocked系列函数的主要作用是同一时刻只会有一个线程访问一个变量
以下是一些常用的 Interlocked 系列函数:
1. InterlockedIncrement:递增指定的变量。
2. InterlockedDecrement:递减指定的变量。
3. InterlockedExchange:将指定的变量设置为指定的值,并返回该变量的原值。
4. InterlockedExchangeAdd:将指定的值加到指定的变量上,并返回该变量的原值。
5. InterlockedCompareExchange:如果指定的变量的当前值等于指定的比较值,则将该变量设置为指定的值。
6. InterlockedAnd:对指定的变量执行按位与操作。
7. InterlockedOr:对指定的变量执行按位或操作。
8. InterlockedXor:对指定的变量执行按位异或操作。
下面是这些Interlocked函数的原型,可以看到,Interlocked函数只是对LONG类型的变量进行同步和操作。所以,如果开发中,对一个整数变量有同步需求的话,可以使用该方案。我在开发中会经常使用该方案。
LONG InterlockedIncrement(
_Inout_ LONG volatile *Addend
);
LONG InterlockedDecrement(
_Inout_ LONG volatile *Addend
);
LONG InterlockedExchange(
_Inout_ LONG volatile *Target,
_In_ LONG Value
);
LONG InterlockedExchangeAdd(
_Inout_ LONG volatile *Addend,
_In_ LONG Value
);
LONG InterlockedCompareExchange(
_Inout_ LONG volatile *Destination,
_In_ LONG Exchange,
_In_ LONG Comperand
);
LONG InterlockedAnd(
_Inout_ LONG volatile *Destination,
_In_ LONG Value
);
LONG InterlockedOr(
_Inout_ LONG volatile *Destination,
_In_ LONG Value
);
LONG InterlockedXor(
_Inout_ LONG volatile *Destination,
_In_ LONG Value
);
接下来,我们用一个简单的案例来演示一些这些函数如何使用,当然我并没有在多线程中使用这些函数,而是在一个main函数中直接演示:
#include <iostream>
#include <Windows.h>
#include <process.h>
int main()
{
long value = 0;
InterlockedIncrement(&value);//变量值加1
std::cout << "Value: " << value << std::endl;//输出1
InterlockedDecrement(&value);//变量值减1
std::cout << "Value: " << value << std::endl;//输出0
InterlockedExchange(&value, 10);//将变量值设置为10
std::cout << "Value: " << value << std::endl;//输出10
InterlockedExchangeAdd(&value, 5);//将变量值加5
std::cout << "Value: " << value << std::endl;//输出15
InterlockedCompareExchange(&value, 20, 15);//如果变量值等于15,则将变量值设置为20
std::cout << "Value: " << value << std::endl;//输出20
return 0;
}
关键段
关键段中的段指的是一段代码,这种方案允许我们对一段代码上锁,同一时刻,只会有一个线程执行一段代码。我们把这种同步方案称为关键段,关键段运行在用户模式下,性能极高,而且非常符合我们的编码习惯(对一段代码加锁),所以这是我非常推荐使用的一种线程同步方案。C#中的lock锁
本质上就是通过代码段来实现的。
关键段的特点
1. 高性能:关键段在用户模式下工作,不涉及内核模式的切换,因此性能较高。
2. 单进程:关键段只能用于同一进程内的线程同步,不能跨进程使用。
3. 独占访问:当一个线程进入关键段时,其他试图进入该关键段的线程将被阻塞,直到第一个线程离开关键段。
关键段的使用
使用关键段进行线程同步通常包括以下步骤:
1. 初始化关键段:在使用关键段之前,需要先初始化它。调用InitializeCriticalSection
函数实现关键段初始化。
2. 进入关键段:线程在访问共享资源之前,必须先进入关键段。调用EnterCriticalSection
函数进入关键段。
3. 离开关键段:线程在访问完共享资源后,必须离开关键段。调用LeaveCriticalSection
离开关键段。
4. 删除关键段:在不再需要关键段时,删除它以释放资源。调用DeleteCriticalSection
删除关键段。
下面代码,我们使用两个线程来计算从0到100的累加结果。但是这个累加过程,我们用到了全局变量g_num
用户保存累加结果,这个写法有点勉强,但是是为了演示关键段的线程同步效果。如果我们使用了关键段,两个线程执行的结果都是正确的结果4950.但是如果我们不适用关键段,就会发现每次执行的结果都不一样。因为对于全局变量g_num
的访问,没有做线程同步。
#include <windows.h>
#include <iostream>
#include <string>
#include <vector>
int g_num = 0;
CRITICAL_SECTION g_criticalSection;
DWORD WINAPI ThreadProc1(LPVOID lpParam)
{
EnterCriticalSection(&g_criticalSection);
g_num = 0;
for (int i = 0; i < 100; i++)
{
g_num += i;
Sleep(10);
}
std::cout << "ThreadProc1: " << g_num << std::endl;
LeaveCriticalSection(&g_criticalSection);
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParam)
{
EnterCriticalSection(&g_criticalSection);
g_num = 0;
for (int i = 0; i < 100; i++)
{
g_num += i;
Sleep(10);
}
std::cout << "ThreadProc2: " << g_num << std::endl;
LeaveCriticalSection(&g_criticalSection);
return 0;
}
int main()
{
//初始化关键段
InitializeCriticalSection(&g_criticalSection);
HANDLE hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
HANDLE hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);
// 等待线程结束
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
// 关闭线程句柄
CloseHandle(hThread1);
CloseHandle(hThread2);
// 删除关键段
DeleteCriticalSection(&g_criticalSection);
return 0;
}
运行结果如下:

旋转锁
对于关键段来说,如果一个线程尝试进入关键段的时候,发现资源无法访问。操作系统会将这个线程挂起,随后等待其他线程释放资源锁这个事件发生之后,才可以进入关键段执行代码。
对于自旋锁来说,如果尝试进入一个关键的时候,发现资源无法访问,它会通过不断循环(自旋)的方式获取锁的状态,直到其他线程释放资源锁。这么做的好处是不用线程的上下文切换,因为线程并没有被挂起。但缺点是会浪费大量的CPU资源。所以自旋锁,不适用于加锁时间比较长的场景。
下面代码我们使用C++的std::atomic_flag
来实现一个自旋锁类Spinlock
,然后同样是计算0到100的累加和。每次进入代码段之前,都要通过Spinlock.lock()
加锁,然后通过spinlock.unlock();
释放锁,可以和明显看到,在自旋锁Spinlock
的内部,使用了一个while死循环来不断循环(自旋)的方式获取锁(std::atomic_flag
)的状态。当然这里不讲解C++的语法,如果对std::atomic_flag
不了解的话,可以使用AI工具自己学习。
#include <atomic>
#include <thread>
#include <iostream>
// 自旋锁
class Spinlock {
private:
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag.test_and_set(std::memory_order_acquire)) {
// 自旋等待
}
}
void unlock() {
flag.clear(std::memory_order_release);
}
};
Spinlock spinlock;
int g_num = 0;
void ThreadProc1() {
spinlock.lock();
g_num = 0;
for (int i = 0; i < 100; i++) {
g_num += i;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
std::cout << "ThreadProc1: " << g_num << std::endl;
spinlock.unlock();
}
void ThreadProc2() {
spinlock.lock();
g_num = 0;
for (int i = 0; i < 100; i++) {
g_num += i;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
std::cout << "ThreadProc2: " << g_num << std::endl;
spinlock.unlock();
}
int main() {
std::thread t1(ThreadProc1);
std::thread t2(ThreadProc2);
t1.join();
t2.join();
return 0;
}
上述代码的执行效果如下:

读写锁
对于上面的线程来说,一个线程一旦拿到这个资源锁,其他线程将无法执行相关代码。但是,对于读写锁来说,允许多个线程同时读取资源,同时在一个线程写的时候,其他线程可以进行读取资源。
读写锁的主要目的是提高并发性,特别是在读操作远多于写操作的情况下。
以下是读写锁的实现逻辑:
读锁:
1、当一个线程请求读锁时,如果没有线程持有写锁,则允许该线程获取读锁。
2、允许多个线程同时持有读锁。
3、如果有线程持有写锁,读锁请求将被阻塞,直到写锁被释放。
写锁:
1、当一个线程请求写锁时,如果没有线程持有读锁或写锁,则允许该线程获取写锁。
2、只允许一个线程持有写锁。
3、如果有线程持有读锁或写锁,写锁请求将被阻塞,直到所有读锁和写锁被释放。
下面我们使用简单的代码来实现读写锁,同样是计算0-100的累加和,但是两个线程负责累加,一个线程负责读取。
#include <windows.h>
#include <iostream>
#include <string>
#include <vector>
int g_num = 0;
SRWLOCK g_srwLock;
DWORD WINAPI ReaderThreadProc(LPVOID lpParam)
{
for (int i = 0; i < 10; i++)
{
// 获取读锁
AcquireSRWLockShared(&g_srwLock);
std::cout << "ReaderThreadProc read: " << g_num << std::endl;
// 释放读锁
ReleaseSRWLockShared(&g_srwLock);
Sleep(10);
}
return 0;
}
DWORD WINAPI WriterThreadProc(LPVOID lpParam)
{
for (int i = 0; i < 10; i++)
{
//获取写锁
AcquireSRWLockExclusive(&g_srwLock);
g_num++;
std::cout << "WriterThreadProc write: " << g_num << std::endl;
//释放写锁
ReleaseSRWLockExclusive(&g_srwLock);
Sleep(10);
}
return 0;
}
int main()
{
// 初始化读写锁
InitializeSRWLock(&g_srwLock);
// 创建读线程
HANDLE hReaderThread1 = CreateThread(NULL, 0, ReaderThreadProc, NULL, 0, NULL);
HANDLE hReaderThread2 = CreateThread(NULL, 0, ReaderThreadProc, NULL, 0, NULL);
// 创建写线程
HANDLE hWriterThread = CreateThread(NULL, 0, WriterThreadProc, NULL, 0, NULL);
// 等待线程结束
WaitForSingleObject(hReaderThread1, INFINITE);
WaitForSingleObject(hReaderThread2, INFINITE);
WaitForSingleObject(hWriterThread, INFINITE);
// 关闭线程句柄
CloseHandle(hReaderThread1);
CloseHandle(hReaderThread2);
CloseHandle(hWriterThread);
return 0;
}
条件变量
条件变量一般配合锁来使用,它 允许在某些场景下,需要等待其他线程触发一些事件才能执行。我们以生产着-消费者来说。生产者线程在写数据的时候,获取写锁,阻塞其他所有线程读取数据。这个逻辑是没问题的。但是如果生产者一直没有写数据。而消费者却要不断的获取读锁,并且获取成功,执行代码(无用代码)。这个时候,我们需要消费者等待生产者触发写数据这个操作的时候,通知消费者继续执行,请看下面的代码。
主要通过SleepConditionVariableCS
函数阻塞线程执行,只有当生产者线程WriterThreadProc
函数调用WakeAllConditionVariable
函数之后,线程才能继续往下执行。
#include <windows.h>
#include <iostream>
#include <string>
#include <vector>
int g_num = 0;
CONDITION_VARIABLE cv; // 条件变量
CRITICAL_SECTION cs; // 关键段
bool ready = false; // 条件变量的条件
DWORD WINAPI ReaderThreadProc(LPVOID lpParam)
{
for (int i = 0; i < 10; i++)
{
EnterCriticalSection(&cs);
while (!ready)
{
// 等待条件变量
SleepConditionVariableCS(&cv, &cs, INFINITE);
}
std::cout << "ReaderThreadProc read: " << g_num << std::endl;
LeaveCriticalSection(&cs);
Sleep(10);
}
return 0;
}
DWORD WINAPI WriterThreadProc(LPVOID lpParam)
{
for (int i = 0; i < 10; i++)
{
EnterCriticalSection(&cs);
g_num++;
std::cout << "WriterThreadProc write: " << g_num << std::endl;
ready = true;
// 唤醒所有等待的线程
WakeAllConditionVariable(&cv);
LeaveCriticalSection(&cs);
Sleep(10);
}
return 0;
}
int main()
{
// 初始化条件变量和关键段
InitializeConditionVariable(&cv);
InitializeCriticalSection(&cs);
// 创建读线程
HANDLE hReaderThread1 = CreateThread(NULL, 0, ReaderThreadProc, NULL, 0, NULL);
HANDLE hReaderThread2 = CreateThread(NULL, 0, ReaderThreadProc, NULL, 0, NULL);
// 创建写线程
HANDLE hWriterThread = CreateThread(NULL, 0, WriterThreadProc, NULL, 0, NULL);
// 等待线程结束
WaitForSingleObject(hReaderThread1, INFINITE);
WaitForSingleObject(hReaderThread2, INFINITE);
WaitForSingleObject(hWriterThread, INFINITE);
// 关闭线程句柄
CloseHandle(hReaderThread1);
CloseHandle(hReaderThread2);
CloseHandle(hWriterThread);
// 删除关键段
DeleteCriticalSection(&cs);
return 0;
}