linux多线程同步概览


临界区

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。

互斥锁 mutex

基本函数

#include <phread.h>
pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZER;  // 静态锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);  // 动态锁

// 锁操作

int pthread_mutex_lock(pthread_mutex_t *mutex);  // 如果lock的时候改锁已经被占用,那么会阻塞到可用为止,成功后返回0,失败后返回错误代码
int pthread_mutex_unlock(pthread_mutex_t *mutex);  // 如果unlock一个没有被lock的锁, 那么会导致没有定义的行为或者返回错误 https://blog.csdn.net/maray/article/details/4071096
int pthread_mutex_trylock(pthread_mutex_t *muytex);  // 如果锁已经被占用,那么不是阻塞,而是直接返回EBUSY.  <-- int 返回值, 不是errno
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

pthread_mutex_destroy 何时调用?

  1. 如果使用静态锁PTHREAD_MUTEX_INITIALIZER,那么没有必要调用pthread_mutex_destroy.
  2. 如果使用动态锁,在destroy之前必须先调用pthread_mutex_lock() 加锁。
  3. 注意! 是destroy,而不是destory

互斥锁类型?

互斥量和自旋锁的区别

  • 从实现原理上来讲,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资源。
  • 因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。

条件变量 condition variable

条件变量要搭配mutex使用

基本函数

#include <pthread.h>
#include <sys/time.h>  // for timespec.
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  // 静态创建
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);  // 动态创建, 条件变量没有attr, 所以第二个参数传入NULL
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);  // wait之后程序会阻塞在这里, 等待signal的触发
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);  // 如果在timeout时间前条件没有满足, 则返回ETIMEOUT。
int pthread_cond_signal(pthread_cond_t *cond);  // 激活一个等待的线程,不会产生“惊群现象”,但是如果做signal的时候没有线程在阻塞等待,它也会正确返回
int pthread_cond_broadcast(pthread_cond_t *cond);  // 激活所有等待的线程

流程

  1. mutex加锁
  2. cond条件锁挂起(调用 pthread_cond_wait)之后, mutex解锁
  3. 条件锁收到signal信号释放, mutex加锁
  4. 条件变量外面经常搭配一个while循环,其逻辑是,在第一次进入条件锁的时候,逻辑为true,但是其他线程发出signal激活条件变量的时候,while循环为false
  5. 注意,这个地方只能用while,而不能用if(), 原因是spurious wakeup (《muduo》P41)
    1. 就算没有任何线程发signal激活因条件变量而睡眠的线程,这个条件变量线程仍然可能醒来。
    2. wiki: “This means that when you wait on a condition variable, the wait may (occasionally) return when no thread specifically broadcast or signaled that condition variable. Spurious wakeups may sound strange, but on some multiprocessor systems, making condition wakeup completely predictable might substantially slow all condition variable operations. The race conditions that cause spurious wakeups should be considered rare.”

读写锁 reader-writer lock

单读多写的场景

  1. 读写锁
  2. 双缓存
    1. https://www.cnblogs.com/openlib/p/5361888.html
    2. https://baike.baidu.com/item/%E5%8D%95%E5%86%99%E5%A4%9A%E8%AF%BB/22692499?fr=aladdin

自旋锁 spin lock

  • 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间。
  • 持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁。(在内核编程中,如果持有自旋锁的代码sleep了就可能导致整个系统挂起(hang)
int pthread_spin_destroy(pthread_spinlock_t *);
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 *);

初始化自旋锁

pthread_spin_init用来申请使用自旋锁所需要的资源并且将它初始化为非锁定状态。pshared的取值及其含义:
PTHREAD_PROCESS_SHARED:该自旋锁可以在多个进程中的线程之间共享。
PTHREAD_PROCESS_PRIVATE:仅初始化本自旋锁的线程所在的进程内的线程才能够使用该自旋锁。

获得一个自旋锁

pthread_spin_lock用来获取(锁定)指定的自旋锁. 如果该自旋锁当前没有被其它线程所持有,则调用该函数的线程获得该自旋锁.否则该函数在获得自旋锁之前不会返回。如果调用该函数的线程在调用该函数时已经持有了该自旋锁,则结果是不确定的。

尝试获取一个自旋锁

pthread_spin_trylock会尝试获取指定的自旋锁,如果无法获取则理解返回失败。

释放(解锁)一个自旋锁

pthread_spin_unlock用于释放指定的自旋锁。

销毁一个自旋锁

pthread_spin_destroy 用来销毁指定的自旋锁并释放所有相关联的资源(所谓的所有指的是由pthread_spin_init自动申请的资源)在调用该函数之后如果没有调用 pthread_spin_init重新初始化自旋锁,则任何尝试使用该锁的调用的结果都是未定义的。如果调用该函数时自旋锁正在被使用或者自旋锁未被初 始化则结果是未定义的。

信号量 semphore

unique_lock

  • std::unique_lock是一个模板类就是lock guard的一种形式。和std::lock_guard一样,声明的时候直接加锁。如果加不上锁就阻塞。
  • C++11多线程 unique_lock详解

成员函数

lock()
unlock()
try_lock()
release()  // 返回它所管理的mutex对象指针,并释放所有权;也就是说,这个unique_lock和mutex不再有关系。

thread local

  1. thread_local是说,每一个线程都会拥有一份自己的thread_local对象,
  2. tcmalloc是在当前调用的线程生成一份threadcache.
  3. 只有如下三种类型被thread_local类修饰
    1. 命名空间下的全局变量
    2. 类的static成员变量
    3. 本地局部变量

__thread

  • __thread是GCC内置的线程局部存储设施,
  • __thread变量每一个线程有一份独立实体,各个线程的值互不干扰。可以用来修饰那些带有全局性且值可能变,但是各线程独立不干扰的变量;
  • 只能修饰POD类型(类似整型指针的标量),不能修饰class类型,因为无法自动调用构造函数和析构函数;
  • 可以用于修饰全局变量,函数内的静态变量,不能修饰函数的局部变量或者class的普通成员变量;
    且__thread变量值只能初始化为编译器常量。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值