多线程编程的锁问题解析(锁竞争死锁活锁及Date Race等)
本文是学习了 Guancheng 大神的文章后,根据文中的相关问题,进行代码分析,并且总结出这篇博客。
原文地址:http://blog.csdn.net/freeelinux/article/details/54091140
大神文章地址:http://www.parallellabs.com/2011/10/02/lock-in-parallel-programming/#comment-1245。
在并行程序中,锁的使用主要会引发两类难题,一类是诸如死锁、活锁等引起的多线程 bug;另一类是由锁竞争引起的性能瓶颈。本文的分析主要是大神的分析,中间穿插我的验证以及总结,可以说是一篇 ”读博笔记“,可以直接点上方链接看原文。
1.用锁来防止 Data Race
#include <pthread.h>
int counter = 0;
void *func(void *params)
{counter++; //数据竞跑
}
void main()
{pthread_t thread1, thread2;pthread_create(&thread1, 0, func, 0);pthread_create(&thread2, 0, func, 0);pthread_join(thread1, 0 );pthread_join(thread2, 0 );
}
load [%counter], rax; // 线程1从counter读取0到寄存器rax
add rax, 1; // 线程1对寄存器rax进行加1
load [%counter], rbx; // 线程2从counter读取0到寄存器rbx
store rax [%counter]; // 线程1把1写入counter的主存地址
add rbx, 1; // 线程2对寄存器rbx进行加1
store rbx, [%counter]; // 线程2把1写入counter的主存地址
为了防止例1中的数据竞跑现象,我们可以使用锁来保证每个线程对counter++操作的独占访问(即保证该操作是原子的)。在例3的程序中,我们使用mutex锁将counter++操作放入临界区中,这样同一时刻只有获取锁的线程能访问该临界区,保证了counter++的原子性:即只有在线程1执行完counter++的三条指令之后线程2才能执行counter++操作,保证了counter的最终值必定为2。
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>int counter = 0;
std::mutex mt; void thread_task()
{
// mt.lock();for(int i=0; i<1000; ++i)counter++;
// mt.unlock();
}int main()
{std::vector<std::thread> workers;for(int i=0; i<5; ++i){auto t = std::thread(thread_task);workers.push_back(std::move(t));} std::for_each(workers.begin(), workers.end(), std::mem_fn(&std::thread::join));std::cout<<counter<<std::endl;return 0;
}
2.死锁和活锁
例4:
// 线程 1
void func1()
{ LOCK(&mutex_a); LOCK(&mutex_b);//线程1停滞在此 counter++; UNLOCK(&mutex_b); UNLOCK(&mutex_a);
} // 线程 2
void func2()
{LOCK(&mutex_b);LOCK(&mutex_a);//线程2停滞在此counter++;UNLOCK(&mutex_a);UNLOCK(&mutex_b);
}
例4中的死锁其实是最简单的情形,在实际的程序中,死锁往往发生在复杂的函数调用过程中。在下面这个例子中,线程1在func1()中获取了mutex_a锁,之后调用func_call1()并在其函数体中尝试获取mutex_b锁;与此同时线程2在func2()中获取了mutex_b锁之后再在func_call2()中尝试获取mutex_a锁从而造成死锁。可以想象,随着程序复杂度的增加,想要正确的检测出死锁会变得越来越困难。
例5:
// 线程 1
void func1()
{
LOCK(&mutex_a);
...
func_call1();
UNLOCK(&mutex_a);
} func_call1()
{ LOCK(&mutex_b); ... UNLOCK(&mutex_b); ...
} // 线程 2
void func2()
{LOCK(&mutex_b);...func_call2()UNLOCK(&mutex_b);
}func_call2()
{LOCK(&mutex_a);...UNLOCK(&mutex_b);...
}
其实避免死锁的方法非常简单,其基本原则就是保证各个线程加锁操作的执行顺序是全局一致的。例如,如果上例中的线程1和线程2都是先对mutex_a加锁再对mutex_b进行加锁就不会产生死锁了。在实际的软件开发中,除了严格遵守相同加锁顺序的原则防止死锁之外,我们还可以使用RAII(Resource Acquisition Is Initialization,即“资源获取即初始化”)的手段来封装加锁解锁操作,从而帮助减少死锁的发生[1]。
#include <iostream>
#include <unistd.h>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>int counter = 0;
std::mutex mt_1;
std::mutex mt_2;void thread_task1()
{mt_1.lock();sleep(1); //review the deadlockmt_2.lock();counter++;mt_2.unlock();mt_1.unlock();
}void thread_task2()
{mt_2.lock();sleep(1);mt_1.lock();counter++;mt_1.unlock();mt_2.unlock();
}int main()
{std::thread t1(thread_task1);std::thread t2(thread_task2);t1.join();t2.join();return 0;
}
在验证中为了更容易复现死锁,我采用了 sleep(1) 的方法,这样容易保证出现持有且交叉申请的情况,即出现死锁。如果不适用 sleep(1) 的情况,死锁很难出现。我执行了多遍都没有出现,可见如果由于死锁的存在,你开发的程序很可能一直测试都是好的,有一天运行突然死锁了,就是这种低概率事件发生了(悲哀)。
例6:
// 线程 1
void func1()
{ int done = 0; while(!done) { LOCK(&mutex_a); if (TRYLOCK(&mutex_b)) { counter++; UNLOCK(&mutex_b); UNLOCK(&mutex_a); done = 1; } else { UNLOCK(&mutex_a); } }
} // 线程 2
void func2()
{int done = 0;while(!done) {LOCK(&mutex_b);if (TRYLOCK(&mutex_a)) {counter++;UNLOCK(&mutex_a);UNLOCK(&mutex_b);done = 1; }else {UNLOCK(&mutex_b);}}
}
下面是我对活锁的验证:
#include <iostream>
#include <unistd.h>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>
#include <functional>
#include <chrono>int counter = 0;
std::mutex mutex_1;
std::mutex mutex_2;void thread_task1()
{int done = 0;while(!done){mutex_1.lock();if(mutex_2.try_lock()) {//return -1 on successcounter++;mutex_2.unlock();mutex_1.unlock();done = 1;} else{std::cout<<"is task 1 alive?"<<std::endl;mutex_1.unlock();} }
}void thread_task2()
{int done = 0;while(!done){mutex_2.lock();if(mutex_1.try_lock()){counter++;mutex_1.unlock();mutex_2.unlock();done = 1;}else{std::cout<<"is task 2 alive?"<<std::endl;mutex_2.unlock();}}
}int main()
{std::thread t1(thread_task1);std::thread t2(thread_task2);t1.join();t2.join();return 0;
}
我使用 Python 的脚本去执行这段代码编译的可执行文件,基本上都是出现活锁。但是自己手动运行 ./main,活锁则很少出现,直到手动 N 次之后(手快残废了),活锁才出现,截图如下:
不停的 try lock 之后,两个线程各自获得了自己想要的锁,然后程序执行完毕了。不过由上述的 is task 1 alive 执行了很多遍,并且 is task 2 alive 也执行了多遍(截图没有展示出来,多遍有多少?反正我的屏幕一直往上滑都几屏幕都是这几句话),两个线程才获得各自的锁,程序才运行完毕,不用说,使用 try lock 可能造成的活锁对性能产生了不必要的延迟。
3. 锁竞争性能瓶颈
在多线程程序中锁竞争是最主要的性能瓶颈之一。在前面我们也提到过,通过使用锁来保护共享变量能防止数据竞跑,保证同一时刻只能有一个线程访问该临界区。但是我们也注意到,正是因为锁造成的对临界区的串行执行导致了并行程序的性能瓶颈。
3.1阿姆达尔法则(Amdahl’s Law)
总体执行时间T = S + P/N
根据这个公式,我们可以得到一些非常有意思的结论。例如,如果一个程序全部代码都可以被并行执行,那么它的加速比会非常好,即随着线程数(CPU核数)的增多该程序的加速比会线性递增。换句话说,如果单线程执行该程序需要16秒钟,用16个线程执行该程序就只需要1秒钟。
然而,如果这个程序只有80%的代码可以被并行执行,它的加速比却会急剧下降。根据阿姆达尔法则,如果用16个线程并行执行次程序可并行的部分,该程序的总体执行时间T = S + P/N = (16*0.2) + (16*0.8)/16 = 4秒,这比完全并行化的情况(只需1秒)足足慢了4倍!实际上,如果该程序只有50%的代码可以被并行执行,在使用16个线程时该程序的执行时间仍然需要8.5秒!
从阿姆达尔法则我们可以看到,并行程序的性能很大程度上被只能串行执行的部分给限制住了,而由锁竞争引起的串行执行正是造成串行性能瓶颈的主要原因之一。
3.2锁竞争的常用解决办法
3.2.1 避免使用锁
#include <iostream>
#include <thread>
#include <mutex>
#include <string>
#include <unistd.h>thread_local int counter = 0;
std::mutex mt; void thread_task(const std::string& thread_name)
{++counter;{ std::lock_guard<std::mutex> lock(mt);std::cout<<"counter for "<<thread_name<<" :"<<counter<<std::endl;}
}int main()
{std::thread t1(thread_task, "t1");std::thread t2(thread_task, "t2");{ std::lock_guard<std::mutex> lock(mt);std::cout<<"counter for main: "<<counter<<std::endl; //注意同步cout,因为一个语句多次向缓冲区写,期间不能有其他线程写入} t1.join();t2.join();return 0;
}
3.2.2 使用读写锁
需要注意的是,并不是所有的场合读写锁都具备更好的性能,大家应该根据Profling的测试结果来判断使用读写锁是否能真的提高性能,特别是要注意写操作虽然很少但很耗时的情况。(Profiling 请参考:(1)使用 GNU profiler 来提高代码运行速度(2)Linux下gprof和oprofiling性能测试工具)
3.2.3 保护数据而不是操作
正是因为临界区的执行时间大大影响了并行程序的整体性能,我们必须尽量少在临界区中做耗时的操作,例如函数调用,数据查询,I/O操作等。简而言之,我们需要保护的只是那些共享资源,而不是对这些共享资源的操作,尽可能的把对共享资源的操作放到临界区之外执行有助于减少锁竞争带来的性能损失。
3.2.4 尽量使用轻量级的原子操作
3.2.5 粗粒度锁与细粒度锁
为了减少串行部分的执行时间,我们可以通过把单个锁拆成多个锁的办法来较小临界区的执行时间,从而降低锁竞争的性能损耗,即把“粗粒度锁”转换成“细粒度锁”。但是,细粒度锁并不一定更好。这是因为粗粒度锁编程简单,不易出现死锁等Bug,而细粒度锁编程复杂,容易出错;而且锁的使用是有开销的(例如一个加锁操作一般需要100个CPU时钟周期),使用多个细粒度的锁无疑会增加加锁解锁操作的开销。在实际编程中,我们往往需要从编程复杂度、性能等多个方面来权衡自己的设计方案。事实上,在计算机系统设计领域,没有哪种设计是没有缺点的,只有仔细权衡不同方案的利弊才能得到最适合自己当前需求的解决办法。例如,Linux内核在初期使用了Big Kernel Lock(粗粒度锁)来实现并行化。从性能上来讲,使用一个大锁把所有操作都保护起来无疑带来了很大的性能损失,但是它却极大的简化了并行整个内核的难度。当然,随着Linux内核的发展,Big Kernel Lock已经逐渐消失并被细粒度锁而取代,以取得更好的性能。(大内核锁参考:Linux 大内核锁原理)
3.2.6 使用无锁算法、数据结构
首先要强调的是,笔者并不推荐大家自己去实现无锁算法。为什么别去造无锁算法的轮子呢?因为高性能无锁算法的正确实现实在是太难了。有多难呢?Doug Lea提到java.util.concurrent库中一个Non Blocking的算法的实现大概需要1个人年,总共约500行代码。事实上,我推荐大家直接去使用一些并行库中已经实现好了的无锁算法、无锁数据结构,以提高并行程序的性能。典型的无锁算法的库有java.util.concurrent,Intel TBB等,它们都提供了诸如Non-blocking concurrent queue之类的数据结构以供使用。
多线程编程的锁问题解析(锁竞争死锁活锁及Date Race等)相关推荐
- (转)互斥对象锁和临界区锁性能比较 .
在Win32平台上进行多线程编程,常会用到锁.下边用C++实现了互斥对象(Mutex)锁和临界区(CRITICAL_SECTION)锁,以加深理解和今后方便使用.代码已在VS2005环境下编译测试通过 ...
- 【转】使用python进行多线程编程
1. python对多线程的支持 1)虚拟机层面 Python虚拟机使用GIL(Global Interpreter Lock,全局解释器锁)来互斥线程对共享资源的访问,暂时无法利用多处理器的优势.使 ...
- Linux 多线程编程(三)
1 线程安全 多线程编程环境中,多个线程同时调用某些函数可能会产生错误结果,这些函数称为非线程安全函数.如果库函数能够在多个线程中同时执行并且不会互相干扰,那么这个库函数就是线程安全( thread- ...
- 【锁】Oracle锁系列
2019独角兽企业重金招聘Python工程师标准>>> [锁]Oracle锁系列 1 BLOG文档结构图 2 前言部分 2.1 导读和注意事项 各位技术爱好者,看完本文后,你可 ...
- Java 多线程编程(锁优化)
来自:老九学堂 并发环境下进行编程时,需要使用锁机制来同步多线程间的操作,保证共享资源的互斥访问. 加锁会带来性能上的损坏,似乎是众所周知的事情. 然而,加锁本身不会带来多少的性能消耗,性能主要是在线 ...
- python多线程编程(2): 使用互斥锁同步线程
上一节的例子中,每个线程互相独立,相互之间没有任何关系.现在假设这样一个例子:有一个全局的计数num,每个线程获取这个全局的计数,根据num进行一些处理,然后将num加1.很容易写出这样的代码: # ...
- synchronized不能锁静态变量_多线程编程不可错过——彻底理解synchronized
持续分享互联网研发技术,欢迎关注我.本人是一线架构师,有问题可以沟通. 1. synchronized简介 在学习知识前,我们先来看一个现象: public class SynchronizedDem ...
- java同步锁synchronized_Java对象锁和类锁全面解析(多线程synchronized关键字)
本文主要是将synchronized关键字用法作为例子来去解释Java中的对象锁和类锁.特别的是希望能帮大家理清一些概念. 一.synchronized关键字 synchronized关键字有如下两种 ...
- Java多线程编程 — 锁优化
2019独角兽企业重金招聘Python工程师标准>>> 阅读目录 一.尽量不要锁住方法 二.缩小同步代码块,只锁数据 三.锁中尽量不要再包含锁 四.将锁私有化,在内部管理锁 五.进行 ...
最新文章
- 【Qt】ubuntu14.04+qt5.6+opencv2.4编程注意事项
- ME_DIRECT_INPUT_INFORECORD 信息记录批量修改程序
- java float f1=0.5_Java Math类静态float copySign(float f1,float f2)与示例
- [MEGA DEAL]终极Java捆绑包(95%折扣)
- Python 第十一篇:开发堡垒机
- 做人不能太心软,这三件事上,越狠心就越受益
- Linux Suspend过程【转】
- python 设计模式之组合模式Composite Pattern
- 浅谈ajax同步、异步的问题
- 向日葵远程控制连接服务器黑屏原因
- win7用友u8安装教程_用友u8怎么安装?分享一组用友u8安装教程(图文)
- 微信小程序实现星星评分
- mac 安装 Adobe CC XD
- Eclipse IDE for Java EE Developers 下载地址
- php网站加广告位,织梦广告位的添加调用
- refseq数据库的特点_eureka如何剔除服务
- 和印度人交往的一点感受
- C/C++趣味代码-------狸猫换太子
- lterator 迭代器 静态属性Symbol.iterator Symbol(Symbol.iterator)
- 【文献阅读4】Position-Based User-Centric Radio Resource Management in 5G UDN for URLLC Vehicular Commu
热门文章
- 有源rc电压放大器实验报告_有源元件和无源元件的区别
- Android 应用程序的优先级和进程状态
- 祝《玩转股票量化交易》星友们2022年股市收益高涨、财源滚滚!
- ROSNOTE : 键盘控制机器人运动
- 三言二拍之3377(二)--3377事件注释
- 鲸鱼优化算法(WOA)及其优秀变体(含MATLAB代码)
- 未能从程序集“**\Microsoft.Build.Tasks.v12.0.dll”加载任务工厂“CodeTaskFactor”
- matlab杜芬振子,研电赛2018准备记录
- android vudroid如何进行PDF文件的目录解析
- python3爬虫源代码_爬虫代理池源代码测试-Python3WebSpider