并发中的各种锁的概念
并发vs并行
- 并行是指两个或者多个事件在同一时刻发生
- 并发是指两个或多事件在同一时间段内发生
原子锁
原子锁是指原子操作,原子操作是不会被中断的,常用的原子操作:
InterLockedAdd
InterLockedExchange
InterLockedCompareExchange
InterLockedIncrement
InterLockedDecrement
InterLockedAnd
InterLockedOr
自旋锁:spinlock
非阻塞锁,也就是说,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁; 自选锁是一种互斥锁的实现方式而已,相比一般的互斥锁会在等待期间放弃cpu,自旋锁(spinlock)则是不断循环并测试锁的状态,这样就一直占着cpu; 用在以下情况:锁持有的时间短,而且线程并不希望在重新调度上花太多的成本。
互斥量/互斥锁:mutex
是阻塞锁,当某线程无法获取互斥量时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放互斥量后,操作系统会激活那个被挂起的线程,让其投入运行。 互斥量的加锁和解锁必须在同一线程内完成,避免死锁。只有一个线程可以使用共享资源。mutex可以说是semaphore在仅取值0/1时的特例
信号量:semaphore
信号允许多个线程同时使用共享资源,但是需要限制在同一时刻访问此资源的最大线程数目;信号量包含互斥量
临界区:critical section
访问共享资源的一段代码;同一时刻,只能有一个线程进入临界区,实现共享资源的访问
资源共享:跨进程vs跨线程
— 临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
- 使用互斥锁不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享
- 使用信号量和事件机制也可以实现跨进程的线程通信
进程vs线程
- 进程的内存空间是天然独立的,线程的内存空间是天然共享的。正因为如此,进程通信/线程同步才是系统编程的很大一块内容
- 进程的执行时间:CPU先加载上下文(程序的执行环境:比如内存存储,比如读写文件就会涉及硬盘I/O,网络通讯就会涉及网络I/O,玩游戏就会涉及显卡资源等)->CPU执行代码片段->CPU保存上下文, 以便还没有执行结束程序获得CPU时间片后继续执行。
- 线程的执行时间:线程由于共享进程资源(各种I/O,内存资源等),就少了这部分的时间消耗,当然线程之间切换也会消耗少许时间,但相比进程切换消耗的时间,就小很多了。
- 进程是cpu资源分配的最小单位,线程是cpu调度的最小单位
- 可以理解为:进程是一个资源的容器,为进程里的所有线程提供共享资源
- 进程有自己独立的地址空间,而线程没有,线程必须依赖于进程而存在
- 进程可以没有线程,但是没有线程的进程就无法获取CPU时间片,所以至少有一个主线程
- 线程他自身也有栈,寄存器等。这里可以从Java的内存模型去看,线程栈中拥有外部变量的拷贝,线程对这个拷贝进行处理后,再把这个修改后的拷贝刷新回主内存。
- 进程是资源竞争的基本单位,比如竞争CPU的调度,以及申请内存(物理地址空间)
- 进程之间相互独立安全性高,如果两个进程之间需要进行(事件通知,数据传输,资源共享,进程控制)那么就需要通过进程间通信(管道,消息队列,共享内存,信号量等)的方式来达成
- 进程有自己的内存,通过分页将虚拟地址空间映射到物理地址空间来存储数据
- 线程共享进程的虚拟地址空间(共享段、数据段)、用户ID和组ID、文件描述符表、当前工作目录,但是线程也有自己的一部分数据,例如一组寄存器(用于线程切换上下文)、用户栈(保存私有数据)、线程优先级等
- 参考http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html 及其讨论
以下内容来自知乎:
作者:胖君
链接:https://www.zhihu.com/question/39850927/answer/242109380
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 核心矛盾是“竞态条件”,即多个线程同时读写某个字段。
- 竞态条件下多线程争抢的是“竞态资源”。
- 涉及读写竟态资源的代码片段叫“临界区”。
- 保证竟态资源安全的最朴素的一个思路就是让临界区代码“互斥”,即同一时刻最多只能有一个线程进入临界区。
- 最朴素的互斥手段:在进入临界区之前,用if检查一个bool值,条件不满足就“忙等”。这叫“锁变量”。
- 但锁变量不是线程安全的。因为“检查-占锁”这个动作不具备“原子性”。
- “TSL指令”就是原子性地完成“检查-占锁”的动作。
- 就算不用TSL指令,也可以设计出线程安全的代码,有一种既巧妙又简洁的结构叫“自旋锁”。当然还有其他更复杂的锁比如“Peterson锁”。
- 但自旋锁的缺点是条件不满足时会“忙等待”,需要后台调度器重新分配时间片,效率低。
- 解决忙等待问题的是:“sleep”和“wakeup”两个原语。sleep阻塞当前线程的同时会让出它占用的锁。wakeup可以唤醒在目标锁上睡眠的线程。
- 使用sleep和wakeup原语,保证同一时刻只有一个线程进入临界区代码片段的锁叫“互斥量”。
- 把互斥锁推广到"N"的空间,同时允许有N个线程进入临界区的锁叫“信号量”。
- 互斥量和信号量的实现都依赖TSL指令保证“检查-占锁”动作的原子性。
- 把互斥量交给程序员使用太危险,有些编程语言实现了“管程”的特性,从编译器的层面保证了临界区的互斥,比如Java的synchronized关键字。
- 并没有“同步锁”这个名词,Java的synchronized正确的叫法应该是“互斥锁”,“独占锁”或者“内置锁”。但有的人“顾名思义”叫它同步锁。
几个点稍微展开一下,首先,不是所有变量都可以是竞态资源。以Java为例,表示对象状态的成员字段可以构成竞态资源。 方法内部的局部变量就不是竞态资源,因为局部变量的生命周期仅局限于方法栈,不能横跨多个线程。
public class EvenGenerator {
private int even = 0; // 竟态资源
public int nextEven() { // |
++even; // | -> 临界区
++even; // |
return even; // |
}
}
上面代码里的EvenGenerator专门生成偶数。但并发场景下,它会错误返回奇数。原因就是当多个线程拿到同一个EvenGenerator对象的引用以后, 比如线程A刚执行完第一次自增操作被挂起,线程B接手进行2次自增以后,返回的就是奇数。然后线程A继续执行,再自增一次以后,也返回奇数。 此时even成员字段构成“竞态资源”。访问竟态资源的代码nextEven()函数就是临界区。并发环境中,对象的公有状态(能通过公有方法访问也算) 暴露给多个线程就构成竞态条件,是很危险的。然后最直观的保护nextEven()函数的代码会把调用写成下面这样,就是“锁变量”,
if (!occupied) { // 检查
occupied = true; // 占锁
critical_rigion(); // 临界区
occupied = false; // 释放锁
}
但这个做法并没有卵用。因为A线程完全可能在检查完occupied锁变量,确认锁没有被占用以后立刻被挂起。B线程抢占锁。这时候再切回A线程, 因为已经检查过锁变量,它也占锁,进入临界区。这时候就同时有两个线程站在锁上,互斥失败。自旋锁的关键就是用一个while轮询,代替if检查状态, 这样就算线程切出去,另一个线程也因为条件不满足循环忙等,不会进入临界区。这是一个非常常用的结构,不光用在自旋锁,基本是使用条件变量wait(),notifyAll()时候的一种惯用法。
// 线程A
while (true) {
while (turn != 0) {} // 锁被占,循环忙等。
critical_rigion();
turn = 1; // 释放锁
noncritical_rigion();
}
// 线程B
while (true) {
while (turn != 1) {} // 锁被占,循环忙等
critical_rigion();
turn = 0; // 释放锁
noncritical_rigion();
}
但刚才说了自旋锁的缺点是循环忙等。如果并发的线程不像进程调度那样在时间片用完以后会自动切换上下文,就会形成死锁。 所以最好在条件不满足的时候,让出线程的控制权,让其他线程有机会执行来使条件满足。这就是sleep原语做的事情。 并且配套的wakeup原语会在条件满足的情况下唤醒。结合TSL指令原子性的“检查-占锁”,以及sleep阻塞并让出线程执行权的思想,就是“互斥量”做的事。 下面是pthread_mutex_lock的实现(摘自《现代操作系统》)
mutex_lock:
TSL REGISTER,MUTEX |将互斥量复制到寄存器,并且将互斥量重置为1
CMP REGISTER,#0 |互斥量是0吗?
JZE ok |如果互斥量为0,解锁,返回
CALL thread_yield |互斥量忙,调度另一线程
JMP mutex_lock |稍后再试
ok: RET |返回调用这,进入临界区