互斥锁基本概念
互斥锁是一种使用频繁的同步手段,也被称为互斥量。对比信号量的使用,我们可以将互斥锁的使用理解为信号量初值仅为1的一种情况。
互斥锁是属于系统的内核级对象,它能够使线程拥有某个资源的绝对访问权,互斥锁主要包括使用数量、线程ID,递归计数器等,其中线程ID表示当前拥有互斥锁的线程,递归计数器表示线程拥有互斥锁的次数。
- 当互斥锁的线程ID为0时,表示互斥锁不被任何线程所拥有,此时系统会发出该互斥锁的通知信号,等待该互斥锁的其他线程中的某一个线程会拥有该互斥锁,同时,互斥锁的线程ID为当前拥有该互斥锁的线程的线程ID。
- 当互斥锁的线程ID不为0时,表示当前有线程拥有该互斥锁。系统不会发出互斥锁的通知信号。其他等待互斥锁的线程继续等待,直到拥有改互斥锁的线程释放互斥对象的拥有权。
互斥锁可以进行分类:
分类 | 函数定义 |
---|---|
静态分配互斥锁 | pthread_mutex_t mutex= PTHREAD_MUTEX_INITIALIZER; |
动态分配互斥锁 | pthread_mutex_init(&mutex, *mutexattr);pthread_mutex_destroy(&mutex); |
每一个互斥锁都有一个数据类型为pthread_mutex_t
的互斥变量,表格中的静态分配互斥锁中的PTHREAD_MUTEX_INITIALIZER
是一个结构常量,所以是可以直接创建一个互斥锁用该常量进行初始化。
或者就使用动态的方式初始化一个互斥锁,这种方式的互斥锁在使用结束后一定要进行销毁。其中初始化的pthread_mutex_init
的参数中需要传递一个mutexattr
的属性值,而这种锁的属性共有四种。
属性值 | 意义 |
---|---|
PTHREAD_MUTEX_TIMED_NP | 这是缺省值,相当于NULL。当一个线程加锁后,其他请求这个锁的线程会进入一个队列等待,锁释放后按队列优先级获得锁。具有公平性。 |
PTHREAD_MUTEX_RECURSIVE_NP | 嵌套锁,允许同一个线程多次获得同一个锁,并多次解锁 |
PTHREAD_MUTEX_ERRORCHECK_NP | 检错锁,如果同一个线程请求同一个锁,返回EDEADLK;负责和PTHREAD_MUTEX_TIMED_NP操作一样(进入等待队列) |
PTHREAD_MUTEX_ADAPTIVE_NP | 适应锁,如果被锁上了,等解锁后重新竞争,不存在等待队列,而是解锁后先到先得。 |
互斥锁相关函数
操作 | 函数 | 意义 |
---|---|---|
阻塞加锁 | int pthread_mutex_lock(pthread_t *mutex) | 锁空闲,立即加锁否则阻塞等待直至解锁 |
非阻塞加锁 | int pthread_mutex_trylock(pthread_t *mutex) | 锁空闲,立即加锁;否则立即返回 EBUSY而非等待 |
避免死锁的加锁 | int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timesec *restrict tsptr); | 可设置等待时间,若超时锁未释放,返回ETIMEDOUT,避免两个或多个线程加了锁,又彼此等待锁的释放而产生死锁。 |
解锁 | int pthread_mutex_unlock(pthread_t *mutex) | 有加锁必然有解锁 |
销毁锁 | int pthread_mutex_destroy(pthread_mutex *mutex); | 互斥锁也是一种资源,使用完毕后需要释放 |
互斥锁的使用
静态互斥锁的使用
代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
using namespace std;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
void* func(void* arg){
sem_t* p = (sem_t*)arg;
for(int i=0; i<10; ++i){
usleep(100);
pthread_mutex_lock(&m);
cout << pthread_self() << ":before" << endl;
usleep(100);
cout << pthread_self() << ":exit" << endl;
pthread_mutex_unlock(&m);
}
}
int main(){
pthread_t tid;
pthread_create(&tid, NULL, func, NULL);
usleep(500); // 等待子进程跑起来
func(NULL);
pthread_join(tid, NULL);
}
执行部分结果如下:
动态互斥锁的使用
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
using namespace std;
void* func(void* arg){
pthread_mutex_t* p = (pthread_mutex_t*)arg;
for(int i=0; i<10; ++i){
usleep(100);
pthread_mutex_lock(p);
cout << pthread_self() << ":before" << endl;
usleep(100);
cout << pthread_self() << ":exit" << endl;
pthread_mutex_unlock(p);
}
}
int main(){
pthread_mutex_t m;
pthread_mutex_init(&m, NULL);
pthread_t tid;
pthread_create(&tid, NULL, func, &m);
usleep(500); // 等待子进程跑起来
func(&m);
pthread_join(tid, NULL);
pthread_mutex_destroy(&m);
}
执行部分结果如下:
显然这两种方式的互斥锁都保证在临界区内只有一个线程运行,保证了线程的同步。
C++11标准库中的mutex
这里我们直接使用标准库初始化一个互斥锁,进行加锁和解锁,具体代码如下:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int main(){
mutex m;
auto func = [&m](){
// 互斥锁是一种资源,不能拷贝,只能移动
for(int i=0; i<5; i++){
m.lock(); // 加锁
cout << this_thread::get_id() << ":before" << endl;
this_thread::sleep_for(500ms);
cout << this_thread::get_id() << ":after" << endl;
m.unlock(); // 解锁
}
};
thread t(func);
func();
t.join();
}
除此以外,我们可以不使用函数lock()
和unlock()
函数,而是直接使用lock_guard
来实现加锁和解锁的函数,具体代码如下:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int main(){
mutex m;
auto func = [&m](){
for(int i=0; i<5; i++){
lock_guard<mutex> guard(m);
cout << this_thread::get_id() << ":before" << endl;
this_thread::sleep_for(500ms);
cout << this_thread::get_id() << ":after" << endl;
}
};
thread t(func);
func();
t.join();
}
其中的lock_guard<mutex>
是守护互斥锁,借助一个对象在离开其作用域时将会自发调用析构函数这个特点,以上的代码块当守护锁离开其所在的作用域时将会解锁。