大家好,今天给各位分享Linux环境下C++并发编程:原子操作与无锁策略详解的一些知识,其中也会对进行解释,文章篇幅可能偏长,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在就马上开始吧!
原子操作:顾名思义,是不可分割的操作。该操作只有未开始和完成两种状态,没有中间状态;
原子类型:原子库中定义的数据类型。对这些类型的所有操作都是原子的,包括通过原子类模板std:atomic T 实例化的数据类型,也支持原子操作。
2. 如何使用原子类型
2.1 原子库atomic支持的原子操作
原子库atomic提供了一些基本的原子类型,也可以通过原子类模板实例化原子对象。下面列出了一些基本的原子类型和相应的专用模板:
对原子类型最重要的访问是读和写,但是原子库提供的相应原子操作是load()和store(val)。原子类型支持的原子操作如下:
2.2 原子操作中的内存访问模型
原子操作确保对数据的访问只能处于两种状态:未开始和已完成。将不会访问中间状态。然而,我们通常需要特定的顺序来访问数据。例如,如果我们想在写入后读取最新的数据,原子操作函数支持控制读写顺序,即它有一个数据同步内存模型参数std:memory_order,用于对读写操作进行排序同时。 C++11定义的六种类型如下:
memory_order_relaxed: 宽松的操作,没有同步或顺序约束,该操作只需要原子性;
memory_order_release memory_order_acquire: 两个线程AB,线程A Release后,线程B Acquire可以保证读取到最新修改的值;这个模型更强大的地方在于它可以保证A-Release之前发生的所有写操作。B-Acquire后即可读取最新值;
memory_order_release memory_order_consume: 前面模型的同步是针对所有对象的,这个模型只针对依赖于涉及到的操作的对象:比如这个操作发生在变量a上,则s=a + b;即s 依赖于a,但b 不依赖于a;当然,这里也存在循环依赖问题,例如:t=s + 1,因为s依赖于a,那么t实际上也依赖于a;
memory_order_seq_cst: 顺序一致性模型,这是C++11原子操作的默认模型;大致的行为是对每个变量执行一次Release-Acquire操作,当然这也是最慢的同步模型;
内存访问模型是一个相对底层的控制接口。如果不了解编译原理和CPU指令执行流程,很容易引入bug。内存模型不是本章的重点,这里不再介绍。后续代码使用默认的顺序一致性模型或更可靠的Release-Acquire模型。
需要C/C++ Linux服务器架构师学习资料,添加qun563998835即可获取(资料包括C/C++、Linux、golang技术、Nginx、ZeroMQ、MySQL、Redis、fastdfs、MongoDB、ZK、流媒体、CDN、P2P、 K8S、Docker、TCP/IP、协程、DPDK、ffmpeg等),免费分享
2.3 使用原子类型代替互斥锁编程
为了方便对比,直接修改了上一篇文章:线程同步的互斥锁中的示例程序。用原子库替换互斥体库的代码如下:
//atomic1.cpp使用原子库而不是互斥库实现线程同步
#include #include #include #include std:chrono:毫秒间隔(100);
std:原子readyFlag(假); //原子布尔类型,替换互斥锁
std:atomic job_shared(0); //两个线程都可以修改"job_shared"并将变量专门化为原子类型
int job_exclusive=0; //只有一个线程可以修改"job_exclusive",不需要保护
//该线程只能修改"job_shared"
void job_1(){ std:this_thread:sleep_for(5* 间隔);
job_shared.fetch_add(1);
std:cout"job_1共享("job_shared.load()")n";
ReadyFlag.store(true); //将布尔标志状态更改为true
} //该线程可以修改"job_shared" 和"job_exclusive"
void job_2(){while(true) { //无限循环,直到"job_shared"可以被访问和修改
if(readyFlag.load()) { //判断布尔flag状态是否为true,如果为true则修改"job_shared"
job_shared.fetch_add(1);
std:cout"job_2共享("job_shared.load()")n";
返回;
}else{ //如果布尔标志为false,则修改"job_exclusive"
++job_exclusive; std:cout"job_2 Exclusive("job_exclusive")n";
std:this_thread:sleep_for(间隔); } }}int main() { std:thread thread_1(job_1); std:thread thread_2(job_2); thread_1.join(); thread_2.join(); getchar();return0;
}
从示例程序中可以看出,原子布尔类型可以实现互斥锁的部分功能,但是在使用条件变量时,互斥锁仍然需要保护条件变量的消耗,即使条件变量是原子的目的。
2.4 使用原子类型实现自旋锁
自旋锁(spinlock)类似于互斥锁(mutex)。任何时候最多可以有一个持有者。但是,如果资源已经被占用,则互斥锁会让资源申请者进入休眠状态,而自旋锁则不会导致调用者休眠,并且会继续循环判断是否成功获取锁。自旋锁是为了防止多处理器并发而专门引入的一种锁。广泛应用于中断处理等内核其他部分(对于单处理器来说,为了防止中断处理并发,可以简单地关闭中断,即关闭/打开标志寄存器中的中断标志位,不需要自旋锁)。
对于多核处理器来说,检测锁可用和设置锁状态这两个动作需要实现为原子操作。如果分成两个原子操作,有可能一个线程在获取锁之后和设置锁之前可能被其他线程抢到。锁,导致执行错误。这就需要原子库提供对原子变量的“读-修改-写(Read-Modify-Write)”的原子操作。在上述原子类型支持的操作中,提供了RMW(Read-Modify-Write)原子操作,例如a.exchange(val)和a.compare_exchange(expected,desired)。
标准库还提供了一个原子布尔类型std:atomic_flag。与std:atomic的所有特化不同,它保证是无锁的,并且不提供load()和store(val)操作,但提供test_and_set()和clear()操作。其中test_and_set()是支持RMW的原子操作。 std:atomic_flag可以用来实现自旋锁功能。代码如下:
//atomic2.cpp使用atomic布尔类型实现自旋锁的功能
包括
包括
包括
包括
std:atomic_flag 锁=ATOMIC_FLAG_INIT; //初始化原子布尔类型
无效f(int n)
{
for (int cnt=0; cnt 100; ++cnt) {
while (lock.test_and_set(std:memory_order_acquire)) //获取锁
; //旋转
std:cout n " 线程Output: " cnt "n";
lock.clear(std:memory_order_release); //释放锁
}
}
int main()
{
std:向量v; //实例化一个元素类型为std:thread的向量
for (int n=0; n 10; ++n) {
v.emplace_back(f, n); //以参数(f,n)为初始值的元素放在向量的末尾,相当于启动一个新的线程f(n)
}
for (auto t : v) { //遍历向量v中的元素,基于范围的for循环,auto自动推导变量类型并引用指针指向的内容
t.join(); //阻塞主线程,直到子线程执行完成
}
getchar();
返回0;
}
自旋锁除了使用atomic_flag的TAS(Test And Set)原子操作实现之外,还可以使用普通原子类型std:atomic来实现:a.exchange(val)支持TAS原子操作,a.compare_exchange(expected,desired)支持CAS (比较和交换)原子操作。如果您有兴趣,可以自己实现。其中,CAS原子操作是无锁编程的主要实现方式。接下来我们将介绍无锁编程。
3. 如何进行无锁编程
3.1 什么是无锁编程?
在原子操作出现之前,读写共享数据可能会产生不确定的结果。因此,在多线程并发编程时,必须使用锁机制来保护共享数据的访问过程。但锁的申请和释放增加了访问共享资源的消耗,可能会导致线程阻塞、锁竞争、死锁、优先级反转、调试困难等问题。
现在有了对原子操作的支持,对单一基本数据类型的读写访问就不需要锁保护了。然而,对于链表等复杂数据类型,多个核有可能同时在链表中的同一位置添加和删除节点。这将导致操作失败或出现故障。因此,我们在对某个节点进行操作之前,首先需要判断该节点的值是否与期望值一致。如果一致,则执行操作。如果不一致,则更新期望值。这些步骤仍然需要实现为RMW(Read-Modify-Write)原子操作,这就是前面提到的CAS(Compare And Swap)原子操作,这是无锁编程中最常用的操作。
由于无锁编程是为了解决锁机制带来的一些问题而出现的,无锁编程可以理解为不使用锁机制就能保证多个线程之间原子变量同步的编程。无锁实现只是将多条指令合并为一条指令,形成逻辑上完整的最小单元。它是与CPU指令执行逻辑兼容而形成的多线程编程模型。
无锁编程基于原子操作。对基本原子类型的共享访问可以通过load()和store(val)保证并发同步。对抽象复杂类型的共享访问需要更复杂的CAS来保证并发同步。并发访问过程只是不使用锁机制,但仍然可以理解为具有加锁行为,粒度小,性能更高。对于无法实现为原子操作的并发访问过程,仍然需要使用锁机制来实现。
3.1 CAS原子操作实现无锁编程
CAS原子操作主要通过函数a.compare_exchange(expected,desired)来实现,其语义是“我认为V的值应该是A,如果是,则将V的值更新为B,否则不修改并告诉V 实际价值是多少?” CAS算法的实现伪代码如下:
bool Compare_exchange_strong(T 预期,T 期望)
{if(this-load()==预期) {
这家商店(所需);
返回真;
}别的{
预期=this-load();
返回假;
} }
让我们尝试实现一个无锁堆栈。代码如下:
//atomic3.cpp使用CAS操作实现无锁堆栈
#include #include templateclass lock_free_stack{private: 结构节点{ T 数据;节点*下一个;
节点(const T数据):数据(数据),下一个(nullptr){}
}; std:原子头; public: lock_free_stack(): head(nullptr) {} void push(const T data) { 节点* new_node=新节点(data);do{
new_node-next=head.load(); //将head的当前值放入new_node-next
}while(!head.compare_exchange_strong(new_node-next, new_node));
//如果新元素new_node的下一个与栈顶的头相同,则证明在你之前没有人操作过它,只需用新元素替换栈顶并退出即可;
//如果不同,证明有人在你之前操作过,并且栈顶发生了变化。该函数会自动将新元素的下一个值更新到改变后的栈顶;
//然后继续循环检测,直到状态1成立并退出;
} T pop() { 节点* 节点;do{
节点=head.load();
}while(node !head.compare_exchange_strong(node, node-next));
如果(节点)
返回节点数据;
}}; int main(){ lock_free_stack s; s.push(1);
s.push(2);
s.push(3);
std:cout s.pop() std:endl; std:cout s.pop() std:endl; getchar();返回0;
}
程序注释中已经解释得很清楚了。在将数据压入堆栈之前,首先通过比较原子类型头与新元素的下一个指向对象来判断头是否已被其他线程修改。根据判断结果,选择是否继续操作。更新期望,而这一切都是在原子操作中完成的,确保共享数据的并发同步,而无需使用锁。
【Linux环境下C++并发编程:原子操作与无锁策略详解】相关文章:
2.米颠拜石
3.王羲之临池学书
8.郑板桥轶事十则
用户评论
终于可以好好学习学习Linux下的多线程了!
有13位网友表示赞同!
C++和多线程一直想深入了解,这篇文章正好能帮我入门。
有15位网友表示赞同!
原子操作?无锁编程?听着都很有意思,期待学习新知识!
有12位网友表示赞同!
感觉这篇文章应该会很实用,我最近项目里也要用到多线程.
有10位网友表示赞同!
对Linux下C++的并发处理一直不太了解,看到这篇文章真是太好了。
有8位网友表示赞同!
我已经开始准备学习多线程了,希望这篇文章能给我一些启发。
有13位网友表示赞同!
无锁编程真的比传统锁更方便吗?期待本文详解。
有14位网友表示赞同!
想了解更多关于原子操作的细节,这篇文章看起来很有帮助。
有7位网友表示赞同!
这门技术我想要学习很久了,终于有人写了详细讲解的文章!
有5位网友表示赞同!
多线程并发确实让人头疼,希望这篇博客能让我更好地理解。
有7位网友表示赞同!
学习C++和Linux编程一直是我的目标,这篇文章应该可以帮我进步。
有16位网友表示赞同!
我正在找相关技术资料,这篇文章刚好满足我的需求!
有11位网友表示赞同!
对多线程开发越来越感兴趣,希望能从这篇文章中学到更多知识!
有15位网友表示赞同!
分享一下学习经验,感觉原子操作和无锁编程非常有用。
有7位网友表示赞同!
看标题就觉得很专业,期待作者能把复杂的技术讲得通俗易懂。
有15位网友表示赞同!
我觉得开源社区上缺少这类详细的教程,这篇文章很有意义!
有18位网友表示赞同!
多线程并发在实际开发中非常实用,这种技术方法很有提升效果。
有12位网友表示赞同!
文章内容看起来很系统,相信可以帮助我快速掌握相关知识。
有10位网友表示赞同!
学习新技能总需要付出时间和精力,但像这篇文章这样好的资料能帮我更快地上手。
有6位网友表示赞同!