Linux C/C++ 并发下的计数(技术)方案(互斥锁、自旋锁、原子操作)

欢迎大家来到别爱的优快云


前言

线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程,但轻量进程更多指内核线程,而把用户线程称为线程。

一、为什么要使用锁?

下面是一个主线程count计数的方案,分别通过它的10个子线程进行count的自增。

mkdir LOCK	//创建LOCK文件夹

cd LOCK		//转到LOCK目录下

touch Lock.c	//创建Lock.c文件

gcc -o lock Lock.c -lpthread//编译的时候需要用pthread动态库

然后这里是Lock.c的代码

#include<stdio.h>
#include<pthread.h>

#define THREAD_COUNT   10


void* thread_callback(void* arg) {

	int* pcount = (int*)arg;
	int i = 0;
	while (i++ < 1000000) {
		(*pcount)++;
		usleep(1);
	}

}




int main() {

	pthread_t threadid[THREAD_COUNT] = { 0 };
	int i = 0;
	int count = 0;
	for (i = 0; i < THREAD_COUNT; i++) {
		pthread_create(&threadid[i], NULL, thread_callback, &count);//创建线程(线程id,线程属性,线程入口函数,主线程往子线程传的参数)
	}

	for (i = 0; i < 100; i++) {
		printf("count: %d\n", count);
		sleep(1);
	}

	
	getchar();
}

因为电脑配置不同,所以CPU的处理不同。
如果每个子线程使count自增10万次
最后count值可能等于100万(或者小于100w),
由于我的电脑每个子线程使count自增100万次,才会小于1000万,所以我以每个子线程自增100万次作为演示。
在这里插入图片描述
在这里可以发现,明明count应该达到1000万才对,但是由于操作系统进程的切换导致count++这条语句出现了问题。
在这里插入图片描述
这是count++转化为汇编的语句,分为三条,在正常情况下,线程一执行完这三条语句再执行线程二.
在这里插入图片描述
但是异常的情况下,线程一可能三条语句执行不完,就会进行线程二的执行。
导致的结果就是明明应该使count自增2次,但实践上只自增了1次,这样的结果就会导致1000万条数据有所衰减。

在这里插入图片描述
所以为了避免这种情况的出现,我们可以使用,互斥锁、自旋锁、原子操作等方法解决这个问题。

二、解决方法(互斥锁、自旋锁、原子操作)

1.互斥锁

代码如下(示例):

#include<stdio.h>
#include<pthread.h>

#define THREAD_COUNT   10

pthread_mutex_t mutex;//添加一个互斥锁

void* thread_callback(void* arg) {

	int* pcount = (int*)arg;
	int i = 0;

	while (i++ < 1000000) {
#if 0


#else
		pthread_mutex_lock(&mutex);//线程调用函数让互斥锁上锁
		(*pcount)++;
		pthread_mutex_unlock(&mutex);//解除互斥锁的锁定
#endif
		usleep(1);
	}

}


int main() {

	pthread_t threadid[THREAD_COUNT] = { 0 };

	pthread_mutex_init(&mutex, NULL);//线程初始化

	int i = 0;
	int count = 0;
	for (i = 0; i < THREAD_COUNT; i++) {
		pthread_create(&threadid[i], NULL, thread_callback, &count);//创建线程(线程id,线程属性,线程入口函数,主线程往子线程传的参数)
	}
	
	for (i = 0; i < 100; i++) {
		printf("count: %d\n", count);
		sleep(1);
	}

	getchar();
}

2.自旋锁

代码如下(示例):

#include<stdio.h>
#include<pthread.h>

#define THREAD_COUNT   10


pthread_spinlock_t spinlock;//添加一个自旋锁


void* thread_callback(void* arg) {

	int* pcount = (int*)arg;
	int i = 0;

	while (i++ < 1000000) {
#if 0


#else
		pthread_spin_lock(&spinlock);//线程调用函数
		(*pcount)++;
		pthread_spin_unlock(&spinlock);
		
#endif

		usleep(1);
	}

	
}


int main() {

	pthread_t threadid[THREAD_COUNT] = { 0 };

	pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);

	int i = 0;
	int count = 0;
	for (i = 0; i < THREAD_COUNT; i++) {
		pthread_create(&threadid[i], NULL, thread_callback, &count);//创建线程(线程id,线程属性,线程入口函数,主线程往子线程传的参数)
	}

	for (i = 0; i < 100; i++) {
		printf("count: %d\n", count);
		sleep(1);
	}

	
	getchar();
}

3.原子操作

代码如下(示例):

#include<stdio.h>
#include<pthread.h>

#define THREAD_COUNT   10

int inc(int* value, int add) {

	int old;

	__asm__ volatile(		//汇编语句
		"lock; xaddl %2,%1;"	//使 %1的结果为%2+%1
		: "=a" (old)
		: "m" (*value), "a"(add)
		: "cc", "memory"
	);

	return old;
}





void* thread_callback(void* arg) {

	int* pcount = (int*)arg;
	int i = 0;

	while (i++ < 1000000) {
#if 0


#else
		inc(pcount, 1);
#endif
		usleep(1);
	}

}




int main() {

	pthread_t threadid[THREAD_COUNT] = { 0 };
	int i = 0;
	int count = 0;
	for (i = 0; i < THREAD_COUNT; i++) {
		pthread_create(&threadid[i], NULL, thread_callback, &count);//创建线程(线程id,线程属性,线程入口函数,主线程往子线程传的参数)
	}

	for (i = 0; i < 100; i++) {
		printf("count: %d\n", count);
		sleep(1);
	}


	getchar();
}

4.三种方法的比较(个人理解)

自旋锁:线程调用时相当于执行while(1)语句,直到获取锁内内容执行完,才进行下一个线程。
互斥锁:如果当前线程没有执行完,引起线程切换,就会执行下一条线程,当再次回来的时候重新执行。
原子操作:把多条语句合并成一条语句。
自旋锁适合锁的内容很少的时候使用,而互斥锁适合锁的内容较多的时候使用。


总结

今天通过这个例子,我让大家理解了,主线程和子线程之间可能会发生一些事情,导致程序最后不能达到我们预期的想法,因此就需要通过锁定语句的方法来使程序正常执行。

参考资料

C/C++ Linux高级开发课程

<think>好的,用户现在想了解原子操作和锁机制。之前他们问过内存分区,可能是在学习操作系统并发编程的基础知识。现在他们转向并发控制,这两个概念在多线程环境中非常重要。 首先,我需要确定用户的基础。他们可能已经了解内存管理,现在想深入并发问题。原子操作和锁都是用来解决数据竞争和同步问题的,但两者的实现和适用场景不同。需要解释清楚它们的区别和各自的优缺点。 用户可能想知道为什么需要这两种机制,以及何时使用哪一种。他们可能在实际编程中遇到了同步问题,或者在学习相关概念时感到困惑。需要举例说明,比如计数器问题,用锁和原子操作分别如何解决,这样更直观。 另外,用户可能对底层实现感兴趣,比如原子操作是如何在CPU指令层面实现的,而锁机制又是如何通过操作系统或库函数来管理的。需要提到CAS(比较并交换)这样的指令,以及自旋锁互斥锁的区别。 还要注意现代编程中的应用,比如C++的std::atomic和Java的synchronized关键字,以及可能的问题比如死锁、性能开销等。需要给出实际案例,帮助用户理解如何避免常见错误。 最后,要比较两者的使用场景,帮助用户做出选择。比如高并发低竞争时用原子操作,复杂操作时用锁。可能还需要提到复合场景下的混合使用,比如锁结合原子操作优化性能。 需要确保回答结构清晰,分点说明,逐步深入,同时保持语言简洁易懂。避免使用过多专业术语,或在必要时解释清楚。检查之前的回答风格,保持一致性,比如使用加粗、代码块和表格来增强可读性。</think>以下是原子操作与锁机制的对比解析: --- ### 一、核心目标 二者均用于 **解决多线程/多进程环境下的数据竞争问题**,但实现方式和适用场景不同。 --- ### 二、原子操作(Atomic Operations) #### 1. 定义 - **不可分割性**:单条CPU指令完成操作,不会被线程调度打断。 - **硬件支持**:依赖CPU指令(如x86的`LOCK`前缀、ARM的`LDREX/STREX`)。 - **典型操作**:`CAS`(Compare-And-Swap)、`Fetch-And-Add`。 #### 2. 特点 | **优势** | **局限性** | |----------|------------| | 无锁设计,避免死锁 | 仅支持简单操作(如加减、位运算) | | 性能极高(无上下文切换) | 无法处理复杂逻辑(如链表插入) | | 无需内存屏障(部分场景) | 需要CPU指令集支持 | #### 3. 代码示例 ```cpp // C++ std::atomic std::atomic<int> counter(0); counter.fetch_add(1); // 原子递增 // 对比非原子操作(可能产生竞态条件) int unsafe_counter = 0; unsafe_counter++; // 编译为多条汇编指令 ``` #### 4. 应用场景 - 计数器增减 - 标志位更新(如`bool is_ready`) - 无锁数据结构(如无锁队列) --- ### 三、锁机制(Locking Mechanisms) #### 1. 定义 - **互斥访问**:通过软件层协议控制资源独占访问。 - **类型**: - **互斥锁(Mutex)**:阻塞等待(如`pthread_mutex_lock`) - **自旋锁(Spinlock)**:循环检测(消耗CPU,适用于短临界区) - **读写锁(RWLock)**:区分读/写操作(如`pthread_rwlock_rdlock`) #### 2. 特点 | **优势** | **风险** | |----------|----------| | 支持复杂操作 | 可能死锁(如交叉锁请求) | | 通用性强 | 性能开销大(上下文切换、锁竞争) | | 可组合使用 | 优先级反转问题(实时系统) | #### 3. 代码示例 ```cpp // C++ std::mutex std::mutex mtx; int shared_data = 0; void thread_func() { mtx.lock(); shared_data += 10; // 受保护操作 mtx.unlock(); } ``` #### 4. 应用场景 - 文件读写 - 数据库事务 - 复杂数据结构修改 --- ### 四、关键对比 | **维度** | 原子操作 | 锁机制 | |----------------|-----------------------------------|---------------------------------| | 实现层级 | CPU指令级 | 操作系统/库函数级 | | 性能 | 纳秒级(无阻塞) | 微秒级(涉及线程调度) | | 适用操作复杂度 | 简单(单变量) | 复杂(多步骤逻辑) | | 内存开销 | 低(仅变量本身) | 高(锁对象+内核数据结构) | | 典型问题 | ABA问题* | 死锁、饥饿 | > *ABA问题示例: > 线程1读取值A → 线程2将A→B→A → 线程1的CAS误判无变化,导致逻辑错误。 --- ### 五、混合使用策略 1. **锁+原子标记优化** ```cpp std::atomic<bool> flag(false); std::mutex mtx; void safe_write() { if (!flag.exchange(true)) { // 原子操作快速检测 std::lock_guard<std::mutex> lock(mtx); // 临界区操作 flag.store(false); } } ``` 2. **无锁队列设计** 结合`CAS`实现入队/出队: $$ \text{Head} \xrightarrow{CAS} \text{NewNode} \rightarrow \text{Tail} $$ --- ### 六、现代发展 1. **事务内存(Transactional Memory)** - 硬件支持(如Intel TSX) - 声明式编程:`atomic { ... }`块自动处理冲突 2. **RCU(Read-Copy-Update)** Linux内核采用,读操作无锁,写操作复制更新。 --- ### 七、选择原则 | **场景** | 推荐方案 | |------------------------|-------------------| | 高频简单操作(如计数) | 原子操作 | | 低频复杂操作(如转账) | 互斥锁 | | 读多写少 | 读写锁/RCU | | 实时性要求高 | 自旋锁(短临界区)| --- 理解二者差异,可显著提升并发程序性能和可靠性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值