Linux 同步机制

1. 多线程

有些处理器的硬件核心有两个(或多个)寄存器集,可以相应地执行两条或多条指令流。当一条指令流停顿时(如需要访问主内存),处理器核心能够执行另外一条指令流上的指令。具有这种特性的处理器核心的行为就像是有两个(或多个)核心一样,这样一个“四核处理器”就能够真正地处理八个硬件线程。,因为最高效地使用软件线程的方法是让软件线程数量与硬件线程数量匹配。

互斥量(mutex)

    互斥量本质上是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。
  对互斥量进行加锁以后,任何其它试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为运行状态的线程可以对互斥量加锁,其它线程将会看到互斥锁依然被锁住,只能回去再次等待它重新变为可用。在这种情况下,每次只有一个线程可以向前执行。

信号量(semaphore)

  互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。

读写锁

   读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁。
  读写锁可以由三种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
  在读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须阻塞直到所有的线程释放读锁。虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。
  读写锁非常适合于对数据结构读的次数远大于写的情况。当读写锁在写模式下时,它所保护的数据结构就可以被安全地修改,因为当前只有一个线程可以在写模式下拥有这个锁。当读写锁在读状态下时,只要线程获取了读模式下的读写锁,该锁所保护的数据结构可以被多个获得读模式锁的线程读取。
  读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当他以写模式锁住时,它是以独占模式锁住的。

信号量(semaphore)

    条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
  条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其它线程在获得互斥量之前不会察觉到这种改变,因此必须锁定互斥量以后才能计算条件。

2. 多进程

    进程是并发的执行流,这些执行流有它们自己的受保护的虚拟内存空间。进程之间通过管道、队列、网络 I/O 或是其他不共享的机制 进行通信。
    
  线程使用同步原语或是通过等待输入(即发生阻塞直至输入变为可用状态)来进行同步。
  
  有些操作系统允许在进程间共享指定的内存块。这些在进程间共享内存的机制非常神秘,而且依赖于操作系统

   进程的主要优点是操作系统会隔离各个进程。如果一个进程崩溃了,其他进程依然活着,尽管它们可能什么也不会做。

  进程最大的缺点是它们有太多的状态:虚内存表、多执行单元上下文、所有暂停线程的上下文。进程的启动、停止以及互相之间的切换都比线程慢

3. 原子

atomic

   atomic<T>模板类:生成一个T类型的原子对象,并提供了系列原子操作函数。
   
   其中T是trivially  copyable type满足:要么全部定义了拷贝/移动/赋值函数,要么全部没定义;没有虚成员;基类或其它任何非static成员都是trivally copyable。
   
   典型的内置类型boolint等属于trivally copyable。
   
   再如class triviall
   {
    public:
         int x
   };
   也是。
   T能够被memcpy、memcmp函数使用,从而支持compare/exchange系列函数。
   
   有一条规则:
   不要在保护数据中通过用户自定义类型T通过参数指针或引用使得共享数据超出保护的作用域。atomic<T>编译器通常会使用一个内部锁保护,而如果用户自定义类型T通过参数指针或引用可能产生死锁。总之限制T可以更利于原子指令。
   
   注意某些原子操作可能会失败,比如atomic<float>、atomic<double>compare_exchange_strong()时和expected相等但是内置的值表示形式不同于expected,还是返回false,没有原子算术操作针对浮点数;同理一些用户自定义的类型T由于内存的不同表示形式导致memcmp失败,从而使得一些相等的值仍返回false
   ref https://blog.csdn.net/liuxuejiang158blog/category_9261852.html 

memory_order_relexed

只保证当前操作的原子性。

不考虑线程间的同步,其它线程可能读到旧值(因为不指定内存屏障,所以内存操作执行时可能是乱序的)

memory_order_acquire

对读取(load)实施acquire语义。相当于插入一个内存读屏障,保证之后的读操作不会被重排到该操作之前。

类似于mutex的lock操作,但不会阻塞。只是保证如果其它线程的release己经发生,则本线程其后对该原子变量的load操作一定会获取到该变量release之前发生的写入。因此,acuquire一般需要用循环,等待其他线程release的发生。

理解为“通过其他线程完成所有工作”的意思。它确保随后的加载不会被移动到当前的加载或是前面的加载之前。自相矛盾的是,它是通过等待在处理器和主内存之间的当前的存储操作完成来实现这一点的。如果没有栅栏,当一次存储还处于处理器和主内存之间,它的线程就在相同的地址进行了一次加载时,该线程会得到旧的数据,仿佛这次在程序中加载被移动到了存储之前。

memory_order_acquire 可能会比默认的完全栅栏高效。例如,在原子性地读取在繁忙等待 while 循环中的标识位时,可以使用 memory_order_acquire 。

memory_order_release

对写入(store)实施release语义(类似于mutex的unlock操作)。保证之前的写操作不会被重排到该操作之后,同时确保该操作之前的store操作将数据写入cache或memory中,确保cache或memory是最新数据。(相当于插入一个写屏障)。

该操作之前当前线程内的所有写操作,对于其他对这个原子变量进行acquire或assume的线程可见。(对于assume,写操作指对依赖变量的写操作)

该操作本身是原子操作。

理解为“通过这个线程将所有工作释放到这个位置”的意思。它确保这个线程完成的之前的加载和存储不会被移动到当前的存储之后。它是通过等待这个线程内部的当前存储操作完成来实现这一点的。

memory_order_release 可能会比默认的完全栅栏高效。例如,在自定义的互斥量的尾部设置标识位时,可以使用 memory_order_release 。

memory_order_consume

类似memory_order_acquire,但只对与这块内存有关的读写操作起作用。
memory_order_consume 是 memory_order_acquire 的一种弱化(但更快)的形式,它只要求当前的加载发生在其他依赖这次加载数据的操作之前。例如,当一个指针的加载被标记为 memory_order_consume 时,紧接着的解引这个指针的操作就不会被移动它之前

memory_order_acq_rel

对读取和写入施加acquire-release语义,无法被重排(相当于同时插入读写两种内存屏障)。

可以看见其他线程施加release语义的所有写入,同时自己release结束后所有写入对其他acquire线程可见。

这会结合之前的两种“确保”,创建一个完全栅栏。

memory_order_seq_cst

如果是读取就是acquire语义,写入就是release语义,读写就施加acquire-release语义。

同时会对所有使用此memory-order的原子操作建立一个全局顺序。这样,所有线程都将看到同样一个的内存操作顺序。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值