基础组件-POSIX互斥锁自旋锁


基础组件

  • 基础组件-POSIX互斥锁自旋锁
  • 前言
  • 一、互斥锁
  • 二、自旋锁
    • 特点:
    • 场景:
    • 使用原则
    • 自旋锁属性
  • 三、两把锁的区别
    • 1. 调度策略
    • 2.使用场景
  • 四、常用函数
    • 1.互斥锁
    • 2.自旋锁
      • 2.0 自旋锁常用接口
      • 2.1 创建自旋锁
      • 2.2 自旋锁初始化
      • 2.2 自旋锁上锁
      • 2.3 自旋锁释放
      • 2.4 自旋锁销毁
  • 总结
  • 例子

前言

内核当发生访问资源冲突的时候,可以有两种锁的解决方案选择:

  1. 一个是原地等待,spinlock
  2. 一个是挂起当前进程,调度其他进程执行(睡眠)mutex

提示:以下是本篇文章正文内容,下面案例可供参考

一、互斥锁

互斥锁是保护临界资源,在并发机制的情况下,多个线程同时访问同一块资源,为了保护数据操作的准确性就需要通过加锁来进行保护。
保持操作互斥: 在并发机制的情况下,多个线程同时访问同一块资源,但是同一个时间只能有一个操作使用临界资源。

二、自旋锁

自旋锁( Spin lock )是线程间互斥的一种机制。自旋锁本质是一把锁,实现的功能与互斥锁完全一样,都是任一时刻只允许一个线程持有锁,达到互斥访问共享资源的目的; 自旋锁一开始是为防止多核处理器(SMP)并发带来竞态而引入的一种互斥机制。
自旋锁是SMP架构中的一种low-level的同步机制。
当线程A想要获取一把自旋锁而该锁又被其它线程锁持有时,线程A会在一个循环中自旋以检测锁是不是已经可用了。对于自选锁需要注意:

  1. 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间.
  2. 持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁。(在内核编程中,如果持有自旋锁的代码sleep了就可能导致整个系统挂起)

使用任何锁需要消耗系统资源(内存资源和CPU时间),这种资源消耗可以分为两类:

  1. 建立锁所需要的资源
  2. 当线程被阻塞时锁所需要的资源

特点:

自旋锁是“原地等待”的方式:线程获取不到锁时就是一直处于忙等待(原地打转?)状态,占用cpu的同时又不能处理任何任务;

自旋锁是一种轻量级的锁,相比互斥锁,资源开销更小,在极短时间的加锁,自旋锁是最理想的选择,可以提高效率。

  1. 用于线程互斥
  2. 阻塞一直占用cpu资源
  3. 不可引起线程睡眠
  4. 轻量级的锁 资源开销小,包括创建、持有、释放过程
  5. 单处理器非抢占内核下:自旋锁会在编译时被忽略;
  6. 单处理器抢占内核下:自旋锁仅仅当作一个设置内核抢占的开关;
  7. 多处理器下:此时才能完全发挥出自旋锁的作用,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。

场景:

自旋锁适用于占用锁时间极短的场景,长时间占用自旋锁会降低系统性能。如果访问资源比较耗时,需长时间持有锁的场景,则需考虑其他互斥机制。

  1. 互斥资源访问时间极短(加锁时间短),小于2次上下文切换的时间
  2. 特殊场景,不希望挂起线程
  3. 中断上下文要用锁,首选 spinlock

使用原则

自旋锁与互斥锁一样,自旋锁使用原则可以参考互斥锁的使用原则,互斥锁的使用原则也是自旋锁的基本使用原则。

  1. 加锁时间极短,并及时释放锁
  2. 禁止嵌套(递归)申请持有自旋锁,否则导致死锁
  3. 避免过多的自旋锁申请,防止cpu资源浪费
  4. 如果临界区可能包含引起睡眠的代码则不能使用自旋锁,否则可能引起死锁。
注:
申请持有自旋锁时会一直占用cpu,如果嵌套或者递归申请自旋锁,在第二层申请锁时,由于锁被第一层持有,第二层获取不到锁一直处于等待状态并占用cpu,程序也无法跳出到最外层释放锁,导致死锁发生。因此,递归程序中使用自旋锁需谨慎

自旋锁属性

自旋锁是一种轻量级的锁,属性只有一个“作用域”,在调用pthread_spin_init函数初始化自旋锁时指定作用域范围。自旋锁作用域表示自旋锁的互斥作用范围,分为进程内(创建者)作用域``THREAD_PROCESS_PRIVATE跨进程作用域PTHREAD_PROCESS_SHARED进程内作用域只能用于进程内线程互斥,跨进程可以用于系统所有线程间互斥

三、两把锁的区别

1. 调度策略

线程申请不到互斥锁时,会使线程睡眠让出cpu资源,获得互斥锁后线程唤醒继续执行;而自旋锁阻塞后不会引起线程睡眠,一直占用cpu资源直至获得自旋锁。

2.使用场景

自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。

从实现原理上来讲,Mutex属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞(blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止
如果大家去查阅Linux glibc中对pthreads API的实现NPTL(Native POSIX Thread Library) 的源码的话(使用”getconf GNU_LIBPTHREAD_VERSION”命令可以得到我们系统中NPTL的版本号),就会发现pthread_mutex_lock()操作如果没有锁成功的话就会调用system_wait()的系统调用并将当前线程加入该mutex的等待队列里。而spin lock则可以理解为在一个while(1)循环中用内嵌的汇编代码实现的锁操作(印象中看过一篇论文介绍说在linux内核中spin lock操作只需要两条CPU指令,解锁操作只用一条指令就可以完成)。有兴趣的朋友可以参考另一个名为sanos的微内核中pthreds API的实现:mutex.c spinlock.c,尽管与NPTL中的代码实现不尽相同,但是因为它的实现非常简单易懂,对我们理解spin lock和mutex的特性还是很有帮助的。
对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。
对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。

四、常用函数

1.互斥锁

互斥锁常用接口:

pthread_mutex_t lock; /* 互斥锁定义 */
pthread_mutex_init(&lock, NULL); /* 动态初始化,   成功返回0,失败返回非0 */
pthread_mutex_t thread_mutex = PTHREAD_MUTEX_INITIALIZER; /* 静态初始化 */pthread_mutex_lock(&lock); /* 阻塞的锁定互斥锁 */
pthread_mutex_trylock(&thread_mutex);/* 非阻塞的锁定互斥锁,成功获得互斥锁返回0,如果未能获得互斥锁,立即返回一个错误码 */
pthread_mutex_unlock(&lock); /* 解锁互斥锁 */
pthread_mutex_destroy(&lock) /* 销毁互斥锁 */

2.自旋锁

2.0 自旋锁常用接口

自旋锁接口:

int pthread_spin_init(pthread_spinlock_t *, int);
int pthread_spin_lock(pthread_spinlock_t *);
int pthread_spin_trylock(pthread_spinlock_t *);
int pthread_spin_unlock(pthread_spinlock_t *);
int pthread_spin_destroy(pthread_spinlock_t *);

2.1 创建自旋锁

自旋锁静态创建:
posix线程自旋锁以pthread_spinlock_t数据结构表示

pthread_spinlock_t spinlock;

2.2 自旋锁初始化

自旋锁初始化:用来申请使用自旋锁所需要的资源并且将它初始化为非锁定状态。

int pthread_spin_init(pthread_spinlock_t *spinlock, int pshared);spinlock,自旋锁实例地址,不能为NULL
pshared,自旋锁作用域PTHREAD_PROCESS_PRIVATE,进程内(创建者)作用域,只能用于进程内线程互斥 仅初始化本自旋锁的线程所在的进程内的线程才能够使用该自旋锁。PTHREAD_PROCESS_SHARED,跨进程作用域,用于系统所有进程包含线程间互斥。
返回,成功返回0,参数无效返回 EINVAL

2.2 自旋锁上锁

自旋锁申请持有分为阻塞方式和非阻塞方式,常用的一般是阻塞方式。

自旋锁上锁阻塞方式。
 用来获取(锁定)指定的自旋锁。如果自旋锁还没有被其他线程持有(上锁),则申请持有自旋锁的线程获得锁。如果自旋锁被其他线程持有,则线程一直处于等待状态(占用cpu),直到持自旋锁的线程解锁后,线程获得锁继续执行。不允许递归嵌套申请自旋锁,否则导致死锁。

int pthread_spin_lock(pthread_spinlock_t *spinlock);spinlock,自旋锁实例地址,不能为NULL
返回,成功返回0,参数无效返回 EINVAL

自旋锁上锁阻非塞方式

int pthread_spin_trylock(pthread_spinlock_t *spinlock);
spinlock,自旋锁实例地址,不能为NULL
返回值
返回值 描述
0 成功
EINVAL 参数无效
EDEADLK 死锁
EBUSY 锁被其他线程持有

调用该函数会立即返回,不会阻塞等待。实际应用可以根据返回状态执行不同的任务操作。

2.3 自旋锁释放

释放指定的自旋锁。

int pthread_spin_unlock(pthread_spinlock_t *spinlock);spinlock,自旋锁实例地址,不能为NULL
返回
返回值 描述
0 成功
EINVAL 参数无效
EDEADLK 死锁
EBUSY 锁被其他线程持有

自旋锁持有后必须及时释放,不允许多次释放锁

2.4 自旋锁销毁

自旋锁释销毁。

int pthread_spinlock_destroy(pthread_spinlock_t *spinlock);spinlock,自旋锁实例地址,不能为NULL
返回
返回值 描述
0 成功
EINVAL spinlock已被销毁过,或者spinlock为空
EBUSY 锁被其他线程持有

pthread_spinlock_destroy用于销毁一个已经使用动态初始化的自旋锁并释放所有相关联的资源(所谓的所有指的是由pthread_spin_init自动申请的资源)。销毁后的自旋锁处于未初始化状态,自旋锁的属性和控制块参数处于不可用状态。使用销毁函数需要注意几点:

  1. 已销毁的自旋锁,可以使用pthread_spinlock_init重新初始化使用
  2. 不能重复销毁已销毁的自旋锁
  3. 没有线程持有自旋锁时,才能销毁

总结

(1)Mutex适合对锁操作非常频繁的场景,并且具有更好的适应性。尽管相比spin lock它会花费更多的开销(主要是上下文切换),但是它能适合实际开发中复杂的应用场景,在保证一定性能的前提下提供更大的灵活度。

(2)spin lock的lock/unlock性能更好(花费更少的cpu指令),但是它只适应用于临界区运行时间很短的场景。而在实际软件开发中,除非程序员对自己的程序的锁操作行为非常的了解,否则使用spin lock不是一个好主意(通常一个多线程程序中对锁的操作有数以万次,如果失败的锁操作(contended lock requests)过多的话就会浪费很多的时间进行空等待)。

例子

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>#undef USER_SPINLOCK
#define USER_SPINLOCK 0
#define LOOP_MAX     5pthread_spinlock_t spinlock;
pthread_mutex_t mutex;int g_count = 0;void *pfuncf_call(void *arg){int i = 0;pthread_detach(pthread_self());printf("Producer :");#if USER_SPINLOCKpthread_spin_lock(&spinlock);
#elsepthread_mutex_lock(&mutex);
#endiffor (i=0; i < LOOP_MAX; i++){printf("%d ",g_count++);}printf("\n");#if USER_SPINLOCK  pthread_spin_unlock(&spinlock);
#elsepthread_mutex_unlock(&mutex);
#endif
}void *pfunc_call(void *argv) {printf("Cosumer: count:%d ",g_count);#if USER_SPINLOCKpthread_spin_lock(&spinlock);
#elsepthread_mutex_lock(&mutex);
#endifwhile(g_count > 0) {printf("%d", g_count--);}printf("\n");#if USER_SPINLOCK    pthread_spin_unlock(&spinlock);
#elsepthread_mutex_unlock(&mutex);
#endif
}int main(int argc, char **argv){// pthread_mutex_init(&mutex, NULL);#if USER_SPINLOCKpthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);#elsepthread_mutex_init(&mutex, NULL);#endifpthread_t thid[3] = {0};pthread_t thc;int i = 0;int ret = 0;for (i; i < 3; i++){ret += pthread_create(&thid[i], NULL, pfuncf_call, NULL);}sleep(1);printf("-----%d\n",g_count);ret += pthread_create(&thc, NULL, pfunc_call, NULL);//  pthread_join(thid[0], NULL);
//  pthread_join(thid[1], NULL);
//  pthread_join(thid[2], NULL);pthread_join(thc, NULL);return 0;
}

POSIX互斥锁自旋锁相关推荐

  1. 第二季:5公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁【Java面试题】

    第二季:5值传递和引用传递[Java面试题] 前言 推荐 值传递 说明 题目 24 TransferValue醒脑小练习 第二季:5公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自 ...

  2. 常⻅锁策略(1. 乐观锁 悲观锁2. 公平锁 非公平锁3. 读写锁4. 可重入锁 自旋锁)

    目录 1. 乐观锁 & 悲观锁 1.1乐观锁定义 1.2 乐观锁实现 -- CAS 1.3 悲观锁定义和应⽤ 2. 公平锁 & 非公平锁 3. 读写锁 3.1 读写锁 3.2 独占锁 ...

  3. Java面试之锁-自旋锁

    Java锁之自旋锁 自旋锁:spinlock,是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁 这样的优点:是减少线程上下文切换的消耗, 缺点是循环会消耗CPU 原来提到的比较并交换, ...

  4. java多线程- 互斥锁 自旋锁

    如果一个资源会被不同的线程访问修改,那么我们把这个资源叫做临界资源(<操作系统>),那么对于该资源访问修改相关的代码就叫做临界区.引入互斥锁即解决多个线程之间共享同一个共享资源,这是多线程 ...

  5. 什么是自旋锁+自旋锁和互斥锁的区别

    文章目录 本文链接 什么是自旋锁 参考链接 自旋锁和互斥锁的区别 参考链接 本文链接 击打 什么是自旋锁 多线程对共享资源访问, 为防止并发引起的相关问题, 常引入锁的机制来处理并发问题.   获取到 ...

  6. 从 class 文件 看 synchronize 锁膨胀过程(偏向锁 轻量级锁 自旋锁 重量级锁)

    大家好,我是烤鸭: 前几天看马士兵老师的并发的课,里边讲到了 synchronize 锁的膨胀过程,今天想用代码演示一下. 1.  简单介绍 关于synchronize jdk 1.5 以后的优化,由 ...

  7. java重入锁 自旋锁_java 自旋锁(可重入且无死锁)

    java自旋锁 的实现原理:如果自旋锁被另外一个线程对象持有,那么当前获取锁的线程将陷入while循环等待,直到那个持有自旋锁的线程对象释放它所持有的自旋锁,那么那些想要获取该自旋锁的线程对象 将会有 ...

  8. java锁 -- 自旋锁

    1.概念 自旋锁:spinlock,是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU. 之前文章提到的比较并交换,底层使用 ...

  9. php 自旋锁,自旋锁、排队自旋锁、MCS锁、CLH锁(转)

    自旋锁(Spin lock) 转:http://coderbee.net/index.php/concurrent/20131115/577 自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程 ...

最新文章

  1. w7计算机的工具栏爱那里,Win7系统如何在任务栏中添加爱心图标图文教程
  2. 图数据库并非要取代区块链,而是让区块链如虎添翼
  3. keras从入门到放弃(十一)电影评价预测
  4. 虚拟打印的实现-EMF转换成BMP
  5. 全国计算机一级考试介绍难不难,全国计算机一级考试内容 计算机一级考试难吗...
  6. js实现modbus_nodejs中使用modbus-serial库创建Modbus TCP读取设备的数据
  7. 阿诺德图像加密c语言,基于Arnold置乱的数字图像加密算法(二)
  8. 复杂sql优化步骤与技巧
  9. 在阿里云服务器中安装配置mysql数据库完整教程
  10. ps怎么撤销参考线_入门板绘怎么练习?怎么提高板绘技巧?(干货)
  11. ansible常用ad hoc操作
  12. 【路面分类】基于matlab灰度共生矩阵图形纹理检测+SVM路面状况分类【含Matlab源码 1519期】
  13. **网页静态化解决方案_Freemarker*
  14. jvm如何排查生产环境cpu飙高的问题
  15. ps裁剪和裁切的区别_PS剪切、裁剪、裁切的区别
  16. 整合阿里云域名 + 腾讯云 CDN + 又拍云存储的使用流程
  17. EMV规范(五)——脱机数据认证
  18. Java学习笔记:Word中创建图表如此简单
  19. 二叉树(Binary Trees)
  20. 出行者信息服务器,出行者信息服务系统解析.ppt

热门文章

  1. 关于“短”的算法(二)
  2. 2021-12-1 学习的打卡学习第九天(头插法和一些函数)
  3. vba 全拼_[求助]如何把中文名字转换为拼音(全拼、首字母)
  4. MUD游戏编程 Socket API
  5. 从“富士康员工泄密iPad2后壳设计图”谈起
  6. Android Design 官方文档离线版(英文)!
  7. JFinal快速入门
  8. 在springboot遇到的 Access denied for user ‘***‘@‘localhost‘ (using password: YES)问题
  9. 《Java核心技术》(第12版)笔记(二)
  10. 怎么证明期刊是不是EI检索期刊