Linux C/C++ 中锁的使用总结
本文总结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个成员函数:lock
、try_lock
和 unlock
。
std::recursive_timed_mutex
std::recursive_timed_mutex 是带有延时功能的可递归 mutex, 比 std::recursive_mutex 多了两个函数:try_lock_for
和 try_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++ 中锁的使用总结相关推荐
- Linux内核中锁机制之完成量、互斥量
在上一篇博文中笔者分析了关于信号量.读写信号量的使用及源码实现,接下来本篇博文将讨论有关完成量和互斥量的使用和一些经典问题. 八.完成量 下面讨论完成量的内容,首先需明确完成量表示为一个执行单元需要等 ...
- linux 信号量锁 内核,Linux内核中锁机制之信号量、读写信号量
在上一篇博文中笔者分析了关于内存屏障.读写自旋锁以及顺序锁的相关内容,本篇博文将着重讨论有关信号量.读写信号量的内容. 六.信号量 关于信号量的内容,实际上它是与自旋锁类似的概念,只有得到信号量的进程 ...
- 大话Linux内核中锁机制之原子操作、自旋锁【转】
转自:http://blog.sina.com.cn/s/blog_6d7fa49b01014q7p.html 多人会问这样的问题,Linux内核中提供了各式各样的同步锁机制到底有何作用?追根到底其实 ...
- linux中断函数中有锁,Linux下fcntl实现建议锁和强制锁
近日小温下APUE,发现Linux下的 fcntl 实现强制锁的功能好像都没试验过,简单做个测试. 首先用 fcntl 实现建议锁(Advisory locking),比较简单,贴个最简单的代码: # ...
- linux 内核连接跟踪,Linux内核连接跟踪锁的优化分析(1)
Linux内核连接跟踪锁的优化分析(1) 作者:gfree.wind@gmail.com 博客:linuxfocus.blog.chinaunix.net 微博:weibo.com/glinuxer ...
- linux设备驱动程序中的阻塞机制
阻塞与非阻塞是设备访问的两种方式.在写阻塞与非阻塞的驱动程序时,经常用到等待队列. 一.阻塞与非阻塞 阻塞调用是指调用结果返回之前,当前线程会被挂起,函数只有在得到结果之后才会返回. 非阻塞指不能立刻 ...
- Linux设备驱动中的并发控制总结
并发(concurrency)指的是多个执行单元同时.并行被执行.而并发的执行单元对共享资源(硬件资源和软件上的全局.静态变量)的访问则容易导致竞态(race conditions). SMP是一 ...
- linux 两个驱动 竞态,第7章 Linux设备驱动中的并发控制之一(并发与竞态)
本章导读 Linux设备驱动中必须解决的一个问题是多个进程对共享资源的并发访问,并发的访问会导致竞态(竞争状态). Linux提供了多种解决竞态问题的方式,这些方式适合不同的应用场景. 7.1讲解了并 ...
- linux 信号量锁 内核,Linux内核信号量互斥锁应用
主要介绍了Linux 内核关于信号量,互斥锁等的应用 内核同步机制-信号量/互斥锁/读-写信号量 sema ,mutex ,rwsem 信号量 通用信号量 用户类进程之间使用信号量(semaphore ...
最新文章
- 百题大冲关系列课程更新啦!这次是 Golang
- ajax如何做到异步交互,1.ajax简单实现异步交互
- 普通程序员,如何转型大数据相关方向?
- Golang 结构类型
- 请教高手,如何取得Target属性
- 基于java的员工绩效考核管理系统
- Data Center TCP (DCTCP)学习笔记
- 入门级微单反性能对比
- ENVI实现最小距离法、最大似然法、支持向量机遥感图像监督分类与分类后处理操作
- 数据科学风云之互联网金融
- 山大中心校区计算机课在哪,山东大学有几个校区,哪个校区最好及各校区介绍...
- Java中使用isAlphabetic()办法无法解决判断一个char是英文字母,该用别的方法解决
- Kotlin入门-带着问题,理解 对象表达式和对象声明
- 计算机做课程表教程,初学表格制作教程 初学者如何制作课程表
- 企业寄件分部门管理教程
- python3.0正式发布的年份是_来喽,来喽,Python 3.9正式版发布了~~~
- com.netflix.hystrix.exception.HystrixRuntimeException short-circuited and no fallback available
- Chrome关闭后无法打开
- 3dsmax动画四、形体模式。
- metalink登陆故障解决!
热门文章
- 【教程】Git在Eclipse中的安装和基本使用
- Gradle ExtenionContainer 创建和使用扩展参数(extensions)详解
- 关于CTF竞赛的了解
- 【html】设置图片编码格式
- mtd和mtdblock之间的关系
- Python pandas 计算行/列数据之和
- 计算机专业对口升学考试科目,对口升学信息技术(计算机)类2017年专业课考试大纲...
- Pytorch实现GAT(基于Message Passing消息传递机制实现)
- 智能 | 你真的了解自动化仓储系统吗?
- 在Excel中怎样快速对数据进行求和?分享4种求和方法