c语言中实现线程同步的操作
线程
常见问题
同步权限
在多线程 / 多进程并发时,为避免共享资源(如内存变量、硬件设备、文件)被同时修改导致的数据不一致,需要通过 “同步机制” 控制谁能访问资源 ——“获取同步权限” 就是线程 / 进程申请这种访问资格的过程。
v4 = _InterlockedCompareExchange(a1, 1, 0);
- 第一个参数
a1
:指向目标内存地址的指针(通常是一个共享变量,如LONG*
类型),即要操作的 “共享资源标记”。 - 第二个参数
1
:当比较成功时,要写入目标内存地址的 “新值”。 - 第三个参数
0
:“预期值”,即我们认为目标内存当前应该有的值。 - 返回值是操作前目标内存地址(
a1
指向的地址)中的原始值,
// 定义一个共享的“锁标记”,0表示未占用,1表示已占用
LONG lock_flag = 0;// 线程A尝试获取锁
LONG original = _InterlockedCompareExchange(&lock_flag, 1, 0);
if (original == 0) {//获取到权限// 执行临界区操作...// 操作完成后释放锁(如将lock_flag设回0)
} else {// 获取锁失败(锁已被其他线程占用)// 可选择等待、重试或放弃
}
临界区
临界区(Critical Section) 指的是一段 “不能被多个线程同时执行” 的代码片段,当一个线程正在执行临界区代码时,其他线程必须等待该线程执行完毕,才能进入同一临界区。
原子性
“原子”(Atomic)描述的是一个不可分割、不可中断的操作单元。一个 “原子操作” 要么完整地执行完毕,要么完全不执行,中间不会被任何其他线程、进程或中断打断,不存在 “执行到一半” 的中间状态。
假设两个线程(Thread A、Thread B)同时对共享变量 count
(初始值为 0)执行 count += 1
操作。
在底层会拆分为 3 个 CPU 指令:
- 从内存读取
count
的值到 CPU 寄存器(如mov eax, [count]
); - 寄存器中的值加 1(如
inc eax
); - 将寄存器的值写回内存(如
mov [count], eax
)。
如果操作不原子,可能出现以下 “交错执行”:
- Thread A 执行步骤 1:读取
count=0
到寄存器; - 此时 CPU 切换到 Thread B,Thread B 执行步骤 1-3:读取
count=0
→ 加 1→ 写回count=1
; - CPU 切回 Thread A,继续执行步骤 2-3:寄存器值加 1(0→1)→ 写回
count=1
;
最终 count
的结果是 1,但预期是 2。
原子操作的本质
“原子性” 需要硬件(CPU)提供底层支持,再配合软件(操作系统、编程语言库)封装成易用的接口。
硬件层:CPU 的原子指令支持
不同架构的 CPU 会提供专门的 “原子操作指令”,确保单个指令的不可分割性:
- x86/x86_64 架构:通过
lock
前缀实现原子性(如lock cmpxchg
、lock inc
)。lock
前缀会让 CPU 在执行指令期间 “锁定系统总线”,阻止其他 CPU 核心同时访问该内存地址,确保指令执行不被打断; - ARM 架构:提供
ldrex
(原子加载)、strex
(原子存储)等指令,通过 “独占访问内存” 机制实现原子性; - RISC-V 架构:通过
amoswap.w
、amoadd.w
等 “原子内存操作指令”(AMO 指令)实现原子性。
这些硬件指令是 “原子操作” 的基石 —— 软件层面的原子接口(如 C++ 的 std::atomic
、Windows 的 _InterlockedXXX
)本质都是对这些 CPU 指令的封装。
软件层:原子操作的封装与扩展
硬件指令通常只支持 “单个内存地址的简单操作”(如加 1、比较交换),软件会在此基础上封装更灵活的原子操作:
原子操作
原子的交换操作
_InterlockedExchange(state_lock, 2);
- 第一个参数
state_lock
:指向目标内存地址的指针(通常是一个共享变量,如LONG*
类型),即要被修改的 “状态标记”。 - 第二个参数
2
:要写入目标内存地址的 “新值”。 - 返回值:原子地将
state_lock
指向的内存值更新为2
,同时返回该内存地址在更新前的原始值
原子比较交换操作
_InterlockedCompareExchange(&lock_flag, 1, 0);
参数1:类型为 volatile LONG*
(指向 32 位有符号整数的指针),要操作的目标内存地址
参数2:类型为 LONG
(32 位有符号整数),比较成功后要写入目标地址的值
参数3:类型为 LONG
,表示预期的目标地址当前值(即 “旧值”)。
返回值:返回值为 LONG
类型,即目标地址在操作执行前的原始值:
// 定义一个共享的“锁标记”,0表示未占用,1表示已占用
LONG lock_flag = 0;// 线程A尝试获取锁
LONG original = _InterlockedCompareExchange(&lock_flag, 1, 0);
if (original == 0) {//获取到权限// 执行临界区操作...// 操作完成后释放锁(如将lock_flag设回0)
} else {// 获取锁失败(锁已被其他线程占用)// 可选择等待、重试或放弃
}
原子地将目标变量的值加
_InterlockedIncrement(dword_14002BFF0);
参数:dword_14002BFF0
是一个 LONG
类型(32 位)的共享变量(通常是全局或多线程可见的变量),表示要进行递增操作的目标。
返回值:函数返回递增后的新值(LONG
类型)。
原子减 1
_InterlockedDecrement(a1)
参数:类型为 volatile LONG*
(指向 32 位有符号整数的指针)
返回值:类型为 LONG
(32 位有符号整数),表示减 1 操作完成后的结果值(即 *a1 - 1
的结果)。
线程同步
SRW 锁
SRW 锁支持两种获取模式:
- 共享模式(Shared Mode):多个线程可同时获取,适用于 “只读操作” 场景(多个读者可并行访问资源)。
- 独占模式(Exclusive Mode):仅允许一个线程获取,适用于 “修改操作” 场景(写者需独占资源)。
初始化SRW
void InitializeSRWLock(PSRWLOCK SRWLock
);
参数:PSRWLOCK
是 SRWLOCK*
的类型别名,指向 SRWLOCK
结构体(轻量级读写锁的核心数据结构)。
核心作用:初始化 SRW 锁对象的内部状态,初始化后的 SRW 锁可通过以下函数实现读写分离的同步
- 读操作:
AcquireSRWLockShared
(获取共享锁)和ReleaseSRWLockShared
(释放共享锁)。 - 写操作:
AcquireSRWLockExclusive
(获取独占锁)和ReleaseSRWLockExclusive
(释放独占锁)。
读操作
void AcquireSRWLockShared(PSRWLOCK SRWLock
);
参数 SRWLock
是指向 SRWLOCK
结构体的指针,让线程以 “共享模式”(只读模式)安全地获取轻量级读写锁(SRW Lock)。
void ReleaseSRWLockShared(PSRWLOCK SRWLock
);
参数 SRWLock
是指向 SRWLOCK
结构体的指针(PSRWLOCK
即 SRWLOCK*
),表示要释放的轻量级读写锁对象。
写操作
void AcquireSRWLockExclusive(PSRWLOCK SRWLock
);
让线程以 “独占模式” 获取 SRW 锁,确保对共享资源的修改操作(写操作)具有原子性
临界区
进出临界区
EnterCriticalSection(&stru_14002C030):
参数:类型为LPCRITICAL_SECTION
(即 CRITICAL_SECTION*
,指向临界区结构体的指针)。
无返回值
作用:让当前线程 “获取临界区的访问权”
LeaveCriticalSection(&stru_14002C030);
作用:释放临界区
初始化临界区
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection
);
参数 lpCriticalSection
是指向 CRITICAL_SECTION
结构体的指针(LPCRITICAL_SECTION
是 CRITICAL_SECTION*
的类型别名),表示要初始化的临界区对象。
异常:如果初始化失败(通常是由于系统资源不足),函数会触发一个异常(而非返回错误码)。因此在实际使用中,可能需要配合异常处理(如 __try
/__except
)捕获潜在错误。
作用:初始化临界区对象的内部状态,初始化后的临界区可通过 EnterCriticalSection
(进入临界区)和 LeaveCriticalSection
(离开临界区)实现。
使用示例
// 定义临界区对象(全局或栈上)
CRITICAL_SECTION CriticalSection;// 初始化临界区(通常在程序启动或模块初始化时调用)
InitializeCriticalSection(&CriticalSection);// 多线程场景中使用
void ThreadFunc() {// 进入临界区(获取同步权限)EnterCriticalSection(&CriticalSection);// 执行需要同步的操作(如访问共享资源)AccessSharedResource();// 离开临界区(释放同步权限)LeaveCriticalSection(&CriticalSection);
}// 程序退出前销毁临界区(释放资源)
DeleteCriticalSection(&CriticalSection);
临界区同步
TryEnterCriticalSection()
参数:LPCRITICAL_SECTION lpCriticalSection
指向 CRITICAL_SECTION
结构体的指针(与 EnterCriticalSection
相同),表示要尝试进入的临界区对象,需提前通过 InitializeCriticalSection
初始化。
返回值: TRUE
(非 0 值):表示成功进入临界区,FALSE
(0):表示未能进入临界区。
作用:非阻塞尝试进入临界区,与 EnterCriticalSection
的 “阻塞等待” 不同,TryEnterCriticalSection
的核心特点是 “尝试进入,失败立即返回”,适用于以下场景:
- 当线程只需 “短暂尝试” 获取临界区,若失败则执行其他任务(而非等待),避免线程阻塞。
- 实现 “超时等待” 逻辑(结合循环和
Sleep
,多次尝试后放弃)。
临界区状态结构体
00000000 struct _RTL_CRITICAL_SECTION // sizeof=0x28
00000000 { // XREF: .data:CriticalSection/r
00000000 // .data:__rtl_critical_section/r
00000000 PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
00000008 LONG LockCount;
0000000C LONG RecursionCount;
00000010 HANDLE OwningThread;
00000018 HANDLE LockSemaphore;
00000020 ULONG_PTR SpinCount;
00000028 };
线程调度
CPU 时间片
SwitchToThread()
返回值:
- 返回
TRUE
(非 0 值):表示当前有其他 “就绪状态” 的线程(属于同一优先级或更高优先级)被调度执行; - 返回
FALSE
(0):表示当前没有其他就绪线程可调度(即系统中只有当前线程可运行)。
作用:
- 当前线程主动放弃剩余的 CPU 时间片,让操作系统调度器重新选择一个就绪线程(通常是同优先级的其他线程)运行。
自旋等待
while ( 1 )
{v6 = _InterlockedCompareExchange(a1, 1, 0); // 原子比较交换:尝试将a1从0改为1if ( !v6 ) // v6=0表示成功获取权限(a1原本为0,已改为1)break;if ( v6 != 2 ) // 若a1当前为1(被其他线程占用),则主动让出CPUSwitchToThread();if ( *a1 == 2 ) // 若等待期间a1变为2(终止),则返回0return 0LL;
}