概念
互斥(Mutex) :互斥是一种保证多线程或多进程在访问共享资源时不会发生冲突的技术。
同步(Synchronization) :同步通常涉及多个线程或进程之间的协调,以保证它们以特定的顺序执行某些操作。
啰嗦一句,初学计算机时,我一直不太明白“资源”是什么意思?是指内存?硬盘?——是的,内存、硬盘都是资源,但是在程序中,可以把资源理解为“变量”。一块内存就是用一个变量来表示,例如C中的指针;一个文件也是用一个变量(文件描述符)来表示。变量可以表示购票软件中的火车票,支付宝中的余额等等,而这些都可以称之为资源。
从概念上看两者是不一样的,但互斥可以被看作是一种特定的同步形式,即在同一时间只允许一个线程访问特定的代码段(临界区)。所以不必刻意从概念上进行区分,而是从实际情况中进行区分。即每次只允许一个线程/进程进行操作的就是互斥,其余都是同步。
互斥锁、条件变量、信号量的联系与区别
互斥锁(Mutex)、条件变量(Condition Variables)和信号量(Semaphores)都是在并发编程中用于同步和协调多线程或多进程的技术。虽然Mutex的名字是互斥锁,但有时也可以用于同步。再次印证前面所说,不必刻意区分。
先看一下概念:
-
互斥锁(Mutex) :互斥锁主要用于保护共享资源,避免同时被多个线程或进程修改。当一个线程获得互斥锁后,其他线程必须等待直到该线程释放锁。互斥锁是最简单的一种同步机制,主要用于实现"临界区"的概念。
-
条件变量(Condition Variables) :条件变量主要用于线程间的同步。它们通常与互斥锁一起使用(后面会解释)。线程在某些条件下等待,并在条件满足时被唤醒。例如,一个线程可能在等待队列中有元素可处理时才继续运行。如果条件不满足,线程将会被阻塞,并释放已持有的互斥锁,当条件满足(通常由其他线程触发)时,线程将被唤醒并重新获取互斥锁。
-
信号量(Semaphores) :信号量是一种更复杂的同步机制,可以被视为能够持有多个 "许可" 的互斥锁。信号量有两个主要的操作:
wait(或称P操作)和post(或称V操作)。wait操作会获取一个许可,如果许可不可用,则线程将阻塞直到许可可用。post操作会释放一个许可。信号量可以用来控制同时访问某个资源的线程数。
互斥硕可以看做是一种特殊的信号量,即只有0/1两种状态,只有为1时才能加锁成功。
一个例子
可通过力扣题目来练习信号量、互斥锁、条件变量的使用1115. 交替打印 FooBar (考研的同学一定要练习一下)
c++中的互斥锁、条件变量和信号量
具体可参考c++并发支持库
//互斥锁
std::mutex
//条件变量
std::condition_variable
//计数信号量
std::counting_semaphore(c++20)
//二值信号量——相当于互斥锁
std::binary_semaphore(c++20)
//常用互斥锁管理类
std::lock_guard //实现严格基于作用域的互斥体所有权包装器
std::unique_lock //实现可移动的互斥体所有权包装器
通常不直接使用 std::mutex。而是通过std::unique_lock 、 std::lock_guard 的方式管理互斥锁。
std::unique_lock,std::lock_guard,以及std::scoped_lock(C++17以后)都是互斥锁(mutex)的RAII封装。RAII(Resource Acquisition Is Initialization)是一种在 C++ 中管理资源的编程技术,其目标是将资源的生命周期与对象的生命周期捆绑在一起,以确保在任何情况下(包括错误和异常)资源的正确释放。
相比于直接使用 std::mutex,使用这些封装有以下好处:
- 异常安全:如果在互斥锁锁定之后、解锁之前的代码中抛出异常,那么互斥锁就可能永远不会被解锁,导致死锁。然而,如果我们使用的是
std::unique_lock或者std::lock_guard,当它们的对象超出作用域时,它们的析构函数将自动解锁互斥锁,从而避免死锁。(关键) - 代码简洁:无需在所有可能退出函数的地方明确调用
unlock()。当std::unique_lock或者std::lock_guard的对象超出作用域时,它们的析构函数将自动解锁互斥锁。
所以std::condition_variable提供的接口void wait( std::unique_lock<std::mutex>& lock );要求传入一个std::unique_lock而不是std::mutex。如此保证了异常安全。
Linux平台下的互斥锁、条件变量和信号量
//信号量接口定义
#include <semaphore.h>
//初始化信号量
//pshared:该信号量在线程间共享还是进程间共享,0表示线程共享,1表示进程共享
//value:信号量的初始值
int sem_init(sem_t *sem, int pshared, unsigned int value);
//销毁信号量
int sem_destroy(sem_t *sem);
//P操作,如果sem<=0则阻塞
int sem_wait(sem_t *sem);
//V操作,如果sem加一
int sem_post(sem_t *sem);
//封装
//封装的目的:- 采用RAII思想,对象离开作用域时,自动销毁 - pthreads没有内置的异常处理机制。要手动处理
class sem
{
public:
sem(){
if (sem_init(&m_sem, 0, 0) != 0){
throw std::exception();
}
}
sem(int num){
if (sem_init(&m_sem, 0, num) != 0){
throw std::exception();
}
}
~sem(){
sem_destroy(&m_sem);
}
bool wait(){
return sem_wait(&m_sem) == 0;
}
bool post(){
return sem_post(&m_sem) == 0;
}
private:
sem_t m_sem;
};
//互斥锁接口
#include <pthread.h>
//初始化锁
//mutexattr:锁属性,默认为NULL
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//封装
class locker
{
public:
locker(){
if (pthread_mutex_init(&m_mutex, NULL) != 0){
throw std::exception();
}
}
~locker(){
pthread_mutex_destroy(&m_mutex);
}
bool lock(){
return pthread_mutex_lock(&m_mutex) == 0;
}
bool unlock(){
return pthread_mutex_unlock(&m_mutex) == 0;
}
pthread_mutex_t *get(){
return &m_mutex;
}
private:
pthread_mutex_t m_mutex;
};
//条件变量接口
#include <pthread.h>
//初始化条件变量
//cond_attr:条件变量属性,默认为NULL
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
//阻塞当前线程,直到条件变量被唤醒
//mutex:与该条件变量配套的互斥锁
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
//通知一个等待的线程
int pthread_cond_signal(pthread_cond_t *cond);
//通知所有等待的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
//销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
//封装
class cond
{
public:
cond(){
if (pthread_cond_init(&m_cond, NULL) != 0){
throw std::exception();
}
}
~cond(){
pthread_cond_destroy(&m_cond);
}
bool wait(pthread_mutex_t *m_mutex){
return pthread_cond_wait(&m_cond, m_mutex) == 0;
}
bool timewait(pthread_mutex_t *m_mutex, struct timespec t){
return pthread_cond_timedwait(&m_cond, m_mutex, &t) == 0;
}
bool signal(){
return pthread_cond_signal(&m_cond) == 0;
}
bool broadcast(){
return pthread_cond_broadcast(&m_cond) == 0;
}
private:
pthread_cond_t m_cond;
};
C++提供的同步机制与pthread线程库中的有何不同?如何选择
在Linux下进行网络编程,既可以选择c++线程库也可以采用pthread线程库,两者有何不同?如何选择?
- 编程语言:首先,最明显的区别是Pthreads是用C编写的,而C++线程库则是C++编写的,这意味着C++线程库可以更好地与C++特性(如类、对象和异常)一起工作。
- 简洁性和易用性:C++线程库通常更易于使用和理解。例如,它提供了类似std::lock_guard和std::unique_lock这样的RAII风格的工具,可以自动管理互斥锁的生命周期,从而避免程序员忘记解锁。而在Pthreads中,你需要手动调用pthread_mutex_lock和pthread_mutex_unlock函数,或者二次封装。
- 异常安全:C++线程库的设计考虑到了异常安全性。例如,当一个线程抛出异常时,C++的std::thread可以捕获这个异常,并在主线程中重新抛出。相比之下,Pthreads没有内置的异常处理机制。所以前面对他们进行了封装。
- 可移植性:虽然Pthreads设计为POSIX标准的一部分,具有很好的跨Unix类系统的可移植性,但在非POSIX系统(如Windows)上可能需要特殊处理或者替代方案。相比之下,C++的线程库是标准库的一部分,因此在任何支持C++11或更高版本的编译器上都应该可用。
总的来说,如果是编写 C++ 代码,并且目标平台支持 C++11 或更高版本,那么使用 C++ 的线程库可能是一个更好的选择。然而,如果需要处理特定于 POSIX 的特性,或者正在编写 C 代码,那么 pthreads 可能是一个更好的选择。
最后一个问题:为什么condition_variable要配合mutex使用?
-
保护共享资源:当多个线程需要访问共享资源(例如,修改共享数据或检查某些条件)时,你通常需要一个
std::mutex来确保这些操作的原子性,防止数据竞争(race conditions)和不一致。 -
正确地等待条件:以c++为例:
std::condition_variable的wait()方法接受一个std::unique_lock(通常由std::mutex构造而来)和一个可选的谓词(predicate)。wait()方法会自动释放锁,使线程进入睡眠状态,并在以下情况之一发生时醒来:其他线程调用了notify_one()或notify_all(),或者谓词函数返回true。然后,wait()方法在返回前会重新获取锁。这个过程是原子性的,防止了以下情况的发生:当线程检查条件并准备调用wait()时,其他线程可能已经更改了条件并调用了notify_one()或notify_all(),这可能会导致通知被错过,使线程无法正确地醒来。