Linux C++多线程同步的四种方式

Linux C++多线程同步的四种方式,第1张

From : https://blog.csdn.net/qq_39382769/article/details/960753461.同一个线程内部,指令按照先后顺序执行;但不同线程之间的指令很难说清楚是哪一个先执行,在并发情况下,指令执行的先后顺序由内核决定。 如果运行的结果依赖于不同线程执行的先后的话,那么就会形成竞争条件,在这样的情况下,计算的结果很难预知,所以应该尽量避免竞争条件的形成。 2.最常见的解决竞争条件的方法是:将原先分离的两个指令构成一个不可分割的原子 *** 作,而其他任务不能插入到原子 *** 作中! 3.对多线程来说,同步指的是在一定时间内只允许某一个线程访问某个资源,而在此时间内,不允许其他线程访问该资源! 互斥锁 条件变量 读写锁 信号量 一种特殊的全局变量,拥有lock和unlock两种状态。 unlock的互斥锁可以由某个线程获得,一旦获得,这个互斥锁会锁上变成lock状态,此后只有该线程由权力打开该锁,其他线程想要获得互斥锁,必须得到互斥锁再次被打开之后。 1.互斥锁的初始化, 分为静态初始化和动态初始化. 2.互斥锁的相关属性及分类 (1) attr表示互斥锁的属性 (2) pshared表示互斥锁的共享属性,由两种取值: 1)PTHREAD_PROCESS_PRIVATE:锁只能用于一个进程内部的两个线程进行互斥(默认情况) 2)PTHREAD_PROCESS_SHARED:锁可用于两个不同进程中的线程进行互斥,使用时还需要在进程共享内存中分配互斥锁,然后为该互斥锁指定属性就可以了。 互斥锁存在缺点: (1)某个线程正在等待共享数据内某个条件出现。 (2)重复对数据对象加锁和解锁(轮询),但是这样轮询非常耗费时间和资源,而且效率非常低,所以互斥锁不太适合这种情况。 当线程在等待满足某些条件时,使线程进入睡眠状态;一旦条件满足,就换线因等待满足特定条件而睡眠的线程。 程序的效率无疑会大大提高。 1)创建 静态方式:pthread_cond_t cond PTHREAD_COND_INITIALIZER 动态方式:int pthread_cond_init(&cond,NULL) Linux thread 实现的条件变量不支持属性,所以NULL(cond_attr参数) 2)注销 int pthread_cond_destory(&cond) 只有没有线程在该条件变量上,该条件变量才能注销,否则返回EBUSY 因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程!(请参考条件变量的底层实现) 3)等待 条件等待:int pthread_cond_wait(&cond,&mutex) 计时等待:int pthread_cond_timewait(&cond,&mutex,time) 1.其中计时等待如果在给定时刻前条件没有被满足,则返回ETIMEOUT,结束等待 2.无论那种等待方式,都必须有一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait形成竞争条件! 3.在调用pthread_cond_wait前必须由本线程加锁 4)激发 激发一个等待线程:pthread_cond_signal(&cond) 激发所有等待线程:pthread_cond_broadcast(&cond) 重要的是,pthread_cond_signal不会存在惊群效应,也就是是它最多给一个等待线程发信号,不会给所有线程发信号唤醒,然后要求他们自己去争抢资源! pthread_cond_broadcast() 唤醒所有正在pthread_cond_wait()的同一个条件变量的线程。注意:如果等待的多个现场不使用同一个锁,被唤醒的多个线程执行是并发的。pthread_cond_broadcast &pthread_cond_signal1.读写锁比互斥锁更加具有适用性和并行性 2.读写锁最适用于对数据结构的读 *** 作读 *** 作次数多余写 *** 作次数的场合! 3.锁处于读模式时可以线程共享,而锁处于写模式时只能独占,所以读写锁又叫做共享-独占锁。 4.读写锁有两种策略:强读同步和强写同步 强读同步: 总是给读者更高的优先权,只要写者没有进行写 *** 作,读者就可以获得访问权限 强写同步: 总是给写者更高的优先权,读者只能等到所有正在等待或者执行的写者完成后才能进行读 1)初始化的销毁读写锁 静态初始化:pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER 动态初始化:int pthread_rwlock_init(rwlock,NULL),NULL代表读写锁采用默认属性 销毁读写锁:int pthread_rwlock_destory(rwlock) 在释放某个读写锁的资源之前,需要先通过pthread_rwlock_destory函数对读写锁进行清理。释放由pthread_rwlock_init函数分配的资源 如果你想要读写锁使用非默认属性,则attr不能为NULL,得给attr赋值 int pthread_rwlockattr_init(attr),给attr初始化 int pthread_rwlockattr_destory(attr),销毁attr 2)以写的方式获取锁,以读的方式获取锁,释放读写锁 int pthread_rwlock_rdlock(rwlock),以读的方式获取锁 int pthread_rwlock_wrlock(rwlock),以写的方式获取锁 int pthread_rwlock_unlock(rwlock),释放锁 上面两个获取锁的方式都是阻塞的函数,也就是说获取不到锁的话,调用线程不是立即返回,而是阻塞执行,在需要进行写 *** 作的时候,这种阻塞式获取锁的方式是非常不好的,你想一下,我需要进行写 *** 作,不但没有获取到锁,我还一直在这里等待,大大拖累效率 所以我们应该采用非阻塞的方式获取锁: int pthread_rwlock_tryrdlock(rwlock) int pthread_rwlock_trywrlock(rwlock) 互斥锁只允许一个线程进入临界区,而信号量允许多个线程进入临界区。 1)信号量初始化 int sem_init(&sem,pshared, v) pshared为0,表示这个信号量是当前进程的局部信号量。 pshared为1,表示这个信号量可以在多个进程之间共享。 v为信号量的初始值。 返回值: 成功:0,失败:-1 2)信号量值的加减 int sem_wait(&sem):以原子 *** 作的方式将信号量的值减去1 int sem_post(&sem):以原子 *** 作的方式将信号量的值加上1 3)对信号量进行清理 int sem_destory(&sem)

Linux内核设计与实现 十、内核同步方法

手把手教Linux驱动5-自旋锁、信号量、互斥体概述

== 基础概念: ==

并发 :多个执行单元同时进行或多个执行单元微观串行执行,宏观并行执行

竞态 :并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问而导致的竟态状态。

临界资源 :多个进程访问的资源

临界区 :多个进程访问的代码段

== 并发场合: ==

1、单CPU之间进程间的并发 :时间片轮转,调度进程。 A进程访问打印机,时间片用完,OS调度B进程访问打印机。

2、单cpu上进程和中断之间并发 :CPU必须停止当前进程的执行中断

3、多cpu之间

4、单CPU上中断之间的并发

== 使用偏向: ==

==信号量用于进程之间的同步,进程在信号量保护的临界区代码里面是可以睡眠的(需要进行进程调度),这是与自旋锁最大的区别。==

信号量又称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。它负责协调各个进程,以保证他们能够正确、合理的使用公共资源。它和spin lock最大的不同之处就是:无法获取信号量的进程可以睡眠,因此会导致系统调度。

1、==用于进程与进程之间的同步==

2、==允许多个进程进入临界区代码执行,临界区代码允许睡眠;==

3、信号量本质是==基于调度器的==,在UP和SMP下没有区别;进程获取不到信号量将陷入休眠,并让出CPU;

4、不支持进程和中断之间的同步

5、==进程调度也是会消耗系统资源的,如果一个int型共享变量就需要使用信号量,将极大的浪费系统资源==

6、信号量可以用于多个线程,用于资源的计数(有多种状态)

==信号量加锁以及解锁过程:==

sema_init(&sp->dead_sem, 0)/ 初始化 /

down(&sema)

临界区代码

up(&sema)

==信号量定义:==

==信号量初始化:==

==dowm函数实现:==

==up函数实现:==

信号量一般可以用来标记可用资源的个数。

举2个生活中的例子:

==dowm函数实现原理解析:==

(1)down

判断sem->count是否 >0,大于0则说明系统资源够用,分配一个给该进程,否则进入__down(sem)

(2)__down

调用__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT)其中TASK_UNINTERRUPTIBLE=2代表进入睡眠,且不可以打断;MAX_SCHEDULE_TIMEOUT休眠最长LONG_MAX时间;

(3)list_add_tail(&waiter.list, &sem->wait_list)

把当前进程加入到sem->wait_list中;

(3)先解锁后加锁

进入__down_common前已经加锁了,先把解锁,调用schedule_timeout(timeout),当waiter.up=1后跳出for循环;退出函数之前再加锁;

Linux内核ARM构架中原子变量的底层实现研究

rk3288 原子 *** 作和原子位 *** 作

原子变量适用于只共享一个int型变量;

1、原子 *** 作是指不被打断的 *** 作,即它是最小的执行单位。

2、最简单的原子 *** 作就是一条条的汇编指令(不包括一些伪指令,伪指令会被汇编器解释成多条汇编指令)

==常见函数:==

==以atomic_inc为例介绍实现过程==

在Linux内核文件archarmincludeasmatomic.h中。 执行atomic_read、atomic_set这些 *** 作都只需要一条汇编指令,所以它们本身就是不可打断的。 需要特别研究的是atomic_inc、atomic_dec这类读出、修改、写回的函数。

所以atomic_add的原型是下面这个宏:

atomic_add等效于:

result(%0) tmp(%1) (v->counter)(%2) (&v->counter)(%3) i(%4)

注意:根据内联汇编的语法,result、tmp、&v->counter对应的数据都放在了寄存器中 *** 作。如果出现上下文切换,切换机制会做寄存器上下文保护。

(1)ldrex %0, [%3]

意思是将&v->counter指向的数据放入result中,并且(分别在Local monitor和Global monitor中)设置独占标志。

(2)add %0, %0, %4

result = result + i

(3)strex %1, %0, [%3]

意思是将result保存到&v->counter指向的内存中, 此时 Exclusive monitors会发挥作用,将保存是否成功的标志放入tmp中。

(4) teq %1, #0

测试strex是否成功(tmp == 0 ??)

(5)bne 1b

如果发现strex失败,从(1)再次执行。

Spinlock 是内核中提供的一种比较常见的锁机制,==自旋锁是“原地等待”的方式解决资源冲突的==,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地“打转”(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 —— 自旋锁不应该被长时间的持有(消耗 CPU 资源),一般应用在==中断上下文==。

1、spinlock是一种死等机制

2、信号量可以允许多个执行单元进入,spinlock不行,一次只能允许一个执行单元获取锁,并且进入临界区,其他执行单元都是在门口不断的死等

3、由于不休眠,因此spinlock可以应用在中断上下文中;

4、由于spinlock死等的特性,因此临界区执行代码尽可能的短;

==spinlock加锁以及解锁过程:==

spin_lock(&devices_lock)

临界区代码

spin_unlock(&devices_lock)

==spinlock初始化==

==进程和进程之间同步==

==本地软中断之间同步==

==本地硬中断之间同步==

==本地硬中断之间同步并且保存本地中断状态==

==尝试获取锁==

== arch_spinlock_t结构体定义如下: ==

== arch_spin_lock的实现如下: ==

lockval(%0) newval(%1) tmp(%2) &lock->slock(%3) 1 <<TICKET_SHIFT(%4)

(1)ldrex %0, [%3]

把lock->slock的值赋值给lockval;并且(分别在Local monitor和Global monitor中)设置独占标志。

(2)add %1, %0, %4

newval =lockval +(1<<16)相当于next+1;

(3)strex %2, %1, [%3]

newval =lockval +(1<<16)相当于next+1;

意思是将newval保存到 &lock->slock指向的内存中, 此时 Exclusive monitors会发挥作用,将保存是否成功的标志放入tmp中。

(4) teq %2, #0

测试strex是否成功

(5)bne 1b

如果发现strex失败,从(1)再次执行。

通过上面的分析,可知关键在于strex的 *** 作是否成功的判断上。而这个就归功于ARM的Exclusive monitors和ldrex/strex指令的机制。

(6)while (lockval.tickets.next != lockval.tickets.owner)

如何lockval.tickets的next和owner是否相等。相同则跳出while循环,否则在循环内等待判断;

* (7)wfe()和smp_mb() 最终调用#define barrier() asm volatile ("": : :"memory") *

阻止编译器重排,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行。

== arch_spin_unlock的实现如下: ==

退出锁时:tickets.owner++

== 出现死锁的情况: ==

1、拥有自旋锁的进程A在内核态阻塞了,内核调度B进程,碰巧B进程也要获得自旋锁,此时B只能自旋转。 而此时抢占已经关闭,(单核)不会调度A进程了,B永远自旋,产生死锁。

2、进程A拥有自旋锁,中断到来,CPU执行中断函数,中断处理函数,中断处理函数需要获得自旋锁,访问共享资源,此时无法获得锁,只能自旋,产生死锁。

== 如何避免死锁: ==

1、如果中断处理函数中也要获得自旋锁,那么驱动程序需要在拥有自旋锁时禁止中断;

2、自旋锁必须在可能的最短时间内拥有

3、避免某个获得锁的函数调用其他同样试图获取这个锁的函数,否则代码就会死锁;不论是信号量还是自旋锁,都不允许锁拥有者第二次获得这个锁,如果试图这么做,系统将挂起;

4、锁的顺序规则(a) 按同样的顺序获得锁;b) 如果必须获得一个局部锁和一个属于内核更中心位置的锁,则应该首先获取自己的局部锁 c) 如果我们拥有信号量和自旋锁的组合,则必须首先获得信号量;在拥有自旋锁时调用down(可导致休眠)是个严重的错误的;)

== rw(read/write)spinlock: ==

加锁逻辑:

1、假设临界区内没有任何的thread,这个时候任何的读线程和写线程都可以键入

2、假设临界区内有一个读线程,这时候信赖的read线程可以任意进入,但是写线程不能进入;

3、假设临界区有一个写线程,这时候任何的读、写线程都不可以进入;

4、假设临界区内有一个或者多个读线程,写线程不可以进入临界区,但是写线程也无法阻止后续的读线程继续进去,要等到临界区所有的读线程都结束了,才可以进入,可见:==rw(read/write)spinlock更加有利于读线程;==

== seqlock(顺序锁): ==

加锁逻辑:

1、假设临界区内没有任何的thread,这个时候任何的读线程和写线程都可以键入

2、假设临界区内没有写线程的情况下,read线程可以任意进入;

3、假设临界区有一个写线程,这时候任何的读、写线程都不可以进入;

4、假设临界区内只有read线程的情况下,写线程可以理解执行,不会等待,可见:==seqlock(顺序锁)更加有利于写线程;==

读写速度 CPU >一级缓存 >二级缓存 >内存 ,因此某一个CPU0的lock修改了,其他的CPU的lock就会失效;那么其他CPU就会依次去L1 L2和主存中读取lock值,一旦其他CPU去读取了主存,就存在系统性能降低的风险;

mutex用于互斥 *** 作。

互斥体只能用于一个线程,资源只有两种状态(占用或者空闲)

1、mutex的语义相对于信号量要简单轻便一些,在锁争用激烈的测试场景下,mutex比信号量执行速度更快,可扩展

性更好,

2、另外mutex数据结构的定义比信号量小、

3、同一时刻只有一个线程可以持有mutex

4、不允许递归地加锁和解锁

5、当进程持有mutex时,进程不可以退出。

• mutex必须使用官方API来初始化。

• mutex可以睡眠,所以不允许在中断处理程序或者中断下半部中使用,例如tasklet、定时器等

==常见 *** 作:==

struct mutex mutex_1

mutex_init(&mutex_1)

mutex_lock(&mutex_1)

临界区代码;

mutex_unlock(&mutex_1)

==常见函数:==

=


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/8431301.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-16
下一篇 2023-04-16

发表评论

登录后才能评论

评论列表(0条)

保存