本文总结C和C++中各种锁以及使用方式,主要是C语言中的互斥锁 mutex 和读写锁 rwlock,以及C++中的互斥锁mutex以互斥锁管理。C++中的各种mutex其实是对C语言中的mutex的面相对象的封装,此外的mutex管理部分的类其实是用RAII的风格对mutex对象进行进一步包装。
此外线程间通信还有信号量,因为它不叫锁就不说了,还有个非常不常用的自旋锁也不说了,还有名字很罕见的闩 latch 与屏障 barrier 也不说了。感兴趣自己看 cppreference.com吧。

1. C 互斥锁 mutex

初始化与去初始化

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

pthread_mutex_init 使用指定的attr属性初始化一个互斥锁mutex 。如果 atrr 设为 NULL 或者使用一个默认的 pthread_mutexattr_t 类型都是使用默认属性进行初始化。
重复初始化一个已经初始化过的锁会导致未知行为。
pthread_mutex_destroy 可以销毁一个初始化过的锁。使用此函数销毁一个mutex,可以再次初始化。
如果尝试销毁一个锁定状态的mutex会导致未知行为。

除了使用 pthread_mutex_init 函数对 mutex 进行初始化,还可以使用特定的宏在声明 mutex 的时候直接赋值进行静态初始化。例如:

// 普通mutex
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;// 可递归mutex
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER;
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;// 有错误检查的mutex,同一线程重复加锁报错
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER;
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;

上面那个带不带NP后缀取决于系统,我用的Ubuntu18.04对应的宏为PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP。

加锁与解锁

// 普通加锁,重复加锁会阻塞进程
int pthread_mutex_lock (pthread_mutex_t *__mutex);
// 重复加锁不阻塞进程
int pthread_mutex_trylock (pthread_mutex_t *__mutex);
// 带有超时功能加锁
int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abs_timeout);
// 解锁
int pthread_mutex_unlock (pthread_mutex_t *__mutex);

pthread_mutex_lock对一个 mutex 加锁。如果一个线程试图锁定一个已经被另一个线程锁定的互斥锁,那么该线程将被挂起,直到拥有该互斥锁的线程先解锁该互斥锁。
默认的 mutex 在同一个线程里再次被加锁会导致未定义行为,如果定义 mutex 为 PTHREAD_MUTEX_RECURSIVE 类型,即可递归 mutex ,则这个锁可以在同一个线程内重复加锁,每次加锁计数器+1,每次解锁计数器-1,当计数器为0 的时候其他线程才可以获取这个锁。

pthread_mutex_trylock 功能与pthread_mutex_lock,只是当mutex已经是锁定的时候,pthread_mutex_trylock直接返回错误码EBUSY,而不是阻塞进程。

pthread_mutex_timedlock也是加锁,但是只阻塞指定的时间,时间一到还没能获取锁则返回错误码ETIMEDOUT。

pthread_mutex_unlock为解锁。如果互斥锁未被锁定,尝试解锁会导致未定义行为。

示例

让一个数从0加到10,然后再减到0。

#include <pthread.h>
#include <stdio.h>int gValue=0;
pthread_mutex_t gMutex = PTHREAD_MUTEX_INITIALIZER;void *add(void*){pthread_mutex_lock(&gMutex);    // 加锁for (int i = 0; i < 10; ++i) {printf("[1]%d ", ++gValue);}pthread_mutex_unlock(&gMutex);  // 解锁
}void *sub(void*){pthread_mutex_lock(&gMutex);  // 加锁for (int i = 0; i < 10; ++i) {printf("[2]%d ", --gValue);}pthread_mutex_unlock(&gMutex);  // 解锁
}int main() {pthread_t p1, p2;pthread_create(&p1, NULL, add, NULL);pthread_create(&p2, NULL, sub, NULL);pthread_join(p1, NULL);pthread_join(p2, NULL);return 0;
}

输出:
[1]1 [1]2 [1]3 [1]4 [1]5 [1]6 [1]7 [1]8 [1]9 [1]10 [2]9 [2]8 [2]7 [2]6 [2]5 [2]4 [2]3 [2]2 [2]1 [2]0
不加锁的话输出就比较乱了。

2. C 读写锁 rwlock

前面说过互斥锁要么是lock状态,要么是unlock状态,而且一次只能一个线程对其加锁。也就是说这个锁是排他性的,每次只能一个线程拥有。
读写锁,顾名思义用在读写的地方,读写的地方要求就是如果是写的话只能一个线程拥有,防止写错覆盖新的值。如果是读状态可以多个线程拥有,这样就提高了效率,读写锁用于对数据结构读的次数远大于写的情况。
读写锁可以设置为两种加锁状态,即读锁定和写锁定状态。

  • 当处于写锁定状态时,所有加锁操作都会被阻塞。
  • 当处于读锁定状态时,所有试图设置读锁定都会成功,所有试图设置写锁定都会被阻塞,并且还会阻塞后续所有的读锁定加锁操作,直到所有的读锁定都被解锁。

初始化与去初始化
与互斥锁使用方式类似,都需要初始化和去初始化操作。

#include <pthread.h>    int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER;

初始化的时候同样可以使用常量PTHREAD_RWLOCK_INITIALIZER来定义个默认的读写锁。

加锁与解锁

// 加 读 状态的锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 不阻塞版本,成功则返回0
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);// 加 写 状态的锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 不阻塞版本,成功则返回0
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

pthread_rwlock_rdlock 是读模式下锁,pthread_rwlock_wrlock 是写模式下锁定,这两种锁定模式都使用同一个函数pthread_rwlock_unlock进行解锁。

示例

写了个非常傻瓜式的小程序来验证这个读写锁的功能。有两个函数一个是往数组里面写字符,一个是读字符,里面都加了sleep模拟耗时的操作。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>char str[10];
size_t pos = 0;pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;// 每次写一个字符
void *writeData(void *name)
{pthread_rwlock_wrlock(&rwlock);  // 写 加锁sleep(1);str[pos] = 'a' + pos;pos++;printf("%s %ld write\n", (char *)name, time(NULL));pthread_rwlock_unlock(&rwlock);  // 通用解锁函数
}// 读数组中字符串
void *readData(void *name)
{pthread_rwlock_rdlock(&rwlock);  // 读 加锁sleep(1);printf("%s %ld read: str = %s\n", (char *)name, time(NULL), str);pthread_rwlock_unlock(&rwlock);  // 通用解锁函数
}int main()
{// 搞了6个线程干起来pthread_t p[6];pthread_create(&p[0], NULL, writeData, (void *)"p1"); // 读pthread_create(&p[1], NULL, readData, (void *)"p2");  // 写pthread_create(&p[2], NULL, writeData, (void *)"p3"); // 读pthread_create(&p[3], NULL, readData, (void *)"p4");  // 写pthread_create(&p[4], NULL, writeData, (void *)"p5"); // 读pthread_create(&p[5], NULL, readData, (void *)"p6");  // 写for (int i = 0; i < 6; ++i){pthread_join(p[i], NULL);}return 0;
}

如果没有锁的话,这几个操作应该都是随机的。如果读和写函数是用的互斥锁,那么这几个函数的输出也应该是随机的。
但是输出结果是这样的。

p1 1594130585 write
p4 1594130586 read: str = a
p6 1594130586 read: str = a
p2 1594130586 read: str = a
p3 1594130587 write
p5 1594130588 write

每次输出read的几个线程都是几乎同时输出的,因为当有人锁定write锁的时候,没人可以获取锁。当有人锁定read锁的时候,其他write的会阻塞,但是其他read不会被阻塞,所以read可以同时执行。有问题的话欢迎指出。

3. C++ mutex

C++中的锁 mutex 其实是对C语言 mutex 进行面向对象的封装,根据不同特定封装成不同的mutex类,并添加一些安全性检查之类的特性。可以认为每种mutex类内部都有一个C mutex成员变量。
常见的mutex有如下几种:

C++ mutex 功能解释
std::mutex 普通mutex
std::timed_mutex 带有有时限锁定功能的mutex
std::recursive_mutex 可被同一线程递归锁定的mutex
recursive_timed_mutex 可被同一线程递归锁定的,且带有时限的mutex
shared_mutex 共享mutex

std:mutex

std::mutex 是个普通的C mutex封装,大概是下面这个样子,当然还有一些安全性检查等。

class mutex
{public:void lock() {pthread_mutex_lock(&_M_mutex);}bool try_lock() {return !pthread_mutex_trylock(&_M_mutex);}void unlock() {pthread_mutex_unlock(&_M_mutex);}pthread_mutex_t* native_handle() { return &_M_mutex; }pthread_mutex_t _M_mutex = PTHREAD_MUTEX_INITIALIZER; // 我是最普通的那个mutex
};

使用方面和C mutex 差不多,例如

int gValue=0;
std::mutex gMutex;void add(){gMutex.lock();for (int i = 0; i < 10; ++i) {printf("[1]%d ", ++gValue);}gMutex.unlock();
}

std::timed_mutex

普通的mutex获取不到锁会一直阻塞,std::timed_mutex多了个可以只阻塞一段时间的加锁函数。

可以理解为给 std::mutex 增加了一个对C mutex int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abs_timeout);封装的函数,然后起名为std::timed_mutex,这个封装的函数为

template< class Rep, class Period >
bool try_lock_for( const std::chrono::duration<Rep,Period>& timeout_duration );template< class Clock, class Duration >
bool try_lock_until( const std::chrono::time_point<Clock,Duration>& timeout_time );

try_lock_for 为阻塞指定时长,没有获取到锁就返回false。参数是时间长度。
try_lock_until 为阻塞到指定时间,没有获取到锁就返回false。参数是时间点。

示例:

int gValue=0;
std::timed_mutex gMutex;void f(){// 一段时长,可以使用其他 std::chrono:: 时间单位std::chrono::milliseconds timeout(10); if (gMutex.try_lock_for(timeout)) {// TODO:获取到了mutex,开始干活...gMutex.unlock(); // 干完活了,解锁} else {// 时间到了,也没获取到mutex,干点别的吧}
}void g(){auto now=std::chrono::steady_clock::now();if (gMutex.try_lock_until(now + std::chrono::seconds(10)) {// TODO:获取到了mutex,开始干活...gMutex.unlock(); // 干完活了,解锁} else {// 时间到了,也没获取到mutex,干点别的吧}
}

std::recursive_mutex

std::recursive_mutex 是带有计数功能的,可以在同一个线程内递归lock的锁。和 C mutex 中的 PTHREAD_RECURSIVE_MUTEX 相同。
std::recursive_mutex 类提供3个成员函数:locktry_lockunlock

std::recursive_timed_mutex

std::recursive_timed_mutex 是带有延时功能的可递归 mutex, 比 std::recursive_mutex 多了两个函数:try_lock_fortry_lock_until

4. C++ mutex 管理类

std::lock_guard

lock_guard 是严格基于作用域的RAII风格的 mutex 所有权包装器。
每次使用 mutex 的时候都先调用lock()再调用unlock()lock_guard在构造函数中加锁,在析构函数中解锁,在栈上申请的内存,超过作用域自动析构就解锁了。使用起来比较方便。
示例:

int gValue=0;
std::mutex gMutex;void add(){// 栈内存实例化lock_guard,构造函数中有lock()std::lock_guard<std::mutex> guard(gMutex);    for (int i = 0; i < 10; ++i) {printf("[1]%d ", ++gValue);}// 超出作用域lock_guard执行析构,析构函数中调用unlock()
}

等效于

void add(){gMutex.lock();for (int i = 0; i < 10; ++i) {printf("[1]%d ", ++gValue);}gMutex.unlock();
}

std::unique_lock

std::unique_lock也可以提供自动加锁、解锁功能,比std::lock_guard更加灵活,功能强大。
详细描述找机会再总结。

std::shared_lock

std::scoped_lock

Linux C/C++ 中锁的使用总结相关推荐

  1. Linux内核中锁机制之完成量、互斥量

    在上一篇博文中笔者分析了关于信号量.读写信号量的使用及源码实现,接下来本篇博文将讨论有关完成量和互斥量的使用和一些经典问题. 八.完成量 下面讨论完成量的内容,首先需明确完成量表示为一个执行单元需要等 ...

  2. linux 信号量锁 内核,Linux内核中锁机制之信号量、读写信号量

    在上一篇博文中笔者分析了关于内存屏障.读写自旋锁以及顺序锁的相关内容,本篇博文将着重讨论有关信号量.读写信号量的内容. 六.信号量 关于信号量的内容,实际上它是与自旋锁类似的概念,只有得到信号量的进程 ...

  3. 大话Linux内核中锁机制之原子操作、自旋锁【转】

    转自:http://blog.sina.com.cn/s/blog_6d7fa49b01014q7p.html 多人会问这样的问题,Linux内核中提供了各式各样的同步锁机制到底有何作用?追根到底其实 ...

  4. linux中断函数中有锁,Linux下fcntl实现建议锁和强制锁

    近日小温下APUE,发现Linux下的 fcntl 实现强制锁的功能好像都没试验过,简单做个测试. 首先用 fcntl 实现建议锁(Advisory locking),比较简单,贴个最简单的代码: # ...

  5. linux 内核连接跟踪,Linux内核连接跟踪锁的优化分析(1)

    Linux内核连接跟踪锁的优化分析(1) 作者:gfree.wind@gmail.com 博客:linuxfocus.blog.chinaunix.net 微博:weibo.com/glinuxer ...

  6. linux设备驱动程序中的阻塞机制

    阻塞与非阻塞是设备访问的两种方式.在写阻塞与非阻塞的驱动程序时,经常用到等待队列. 一.阻塞与非阻塞 阻塞调用是指调用结果返回之前,当前线程会被挂起,函数只有在得到结果之后才会返回. 非阻塞指不能立刻 ...

  7. Linux设备驱动中的并发控制总结

    并发(concurrency)指的是多个执行单元同时.并行被执行.而并发的执行单元对共享资源(硬件资源和软件上的全局.静态变量)的访问则容易导致竞态(race conditions).   SMP是一 ...

  8. linux 两个驱动 竞态,第7章 Linux设备驱动中的并发控制之一(并发与竞态)

    本章导读 Linux设备驱动中必须解决的一个问题是多个进程对共享资源的并发访问,并发的访问会导致竞态(竞争状态). Linux提供了多种解决竞态问题的方式,这些方式适合不同的应用场景. 7.1讲解了并 ...

  9. linux 信号量锁 内核,Linux内核信号量互斥锁应用

    主要介绍了Linux 内核关于信号量,互斥锁等的应用 内核同步机制-信号量/互斥锁/读-写信号量 sema ,mutex ,rwsem 信号量 通用信号量 用户类进程之间使用信号量(semaphore ...

最新文章

  1. 百题大冲关系列课程更新啦!这次是 Golang
  2. ajax如何做到异步交互,1.ajax简单实现异步交互
  3. 普通程序员,如何转型大数据相关方向?
  4. Golang 结构类型
  5. 请教高手,如何取得Target属性
  6. 基于java的员工绩效考核管理系统
  7. Data Center TCP (DCTCP)学习笔记
  8. 入门级微单反性能对比
  9. ENVI实现最小距离法、最大似然法、支持向量机遥感图像监督分类与分类后处理操作
  10. 数据科学风云之互联网金融
  11. 山大中心校区计算机课在哪,山东大学有几个校区,哪个校区最好及各校区介绍...
  12. Java中使用isAlphabetic()办法无法解决判断一个char是英文字母,该用别的方法解决
  13. Kotlin入门-带着问题,理解 对象表达式和对象声明
  14. 计算机做课程表教程,初学表格制作教程 初学者如何制作课程表
  15. 企业寄件分部门管理教程
  16. python3.0正式发布的年份是_来喽,来喽,Python 3.9正式版发布了~~~
  17. com.netflix.hystrix.exception.HystrixRuntimeException short-circuited and no fallback available
  18. Chrome关闭后无法打开
  19. 3dsmax动画四、形体模式。
  20. metalink登陆故障解决!

热门文章

  1. 【教程】Git在Eclipse中的安装和基本使用
  2. Gradle ExtenionContainer 创建和使用扩展参数(extensions)详解
  3. 关于CTF竞赛的了解
  4. 【html】设置图片编码格式
  5. mtd和mtdblock之间的关系
  6. Python pandas 计算行/列数据之和
  7. 计算机专业对口升学考试科目,对口升学信息技术(计算机)类2017年专业课考试大纲...
  8. Pytorch实现GAT(基于Message Passing消息传递机制实现)
  9. 智能 | 你真的了解自动化仓储系统吗?
  10. 在Excel中怎样快速对数据进行求和?分享4种求和方法