如果在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞,Core0会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其它的任务而不必进行等待。一般情况下锁是一个二元变量,其状态为开锁(允许0)和上锁(禁止1),将某个共享资源(要加锁的代码块)与锁在逻辑上绑定,即:要申请该资源必须先获取锁,从而解决多线程情况下对共享资源访问存在的线程安全问题
常见锁的介绍
1、互斥锁(mutex)
同一时间只能有一个线程对共享资源进行加锁,在A线程加锁未释放的情况下,B线程再试图加锁会导致B线程阻塞,因为是一种排他性质的锁,所以在高并发访问共享资源的时候性能较低
Mutex是一个结构体,里面包含了一个等待队列头,一个原子变量,一个自旋锁,工作机制可以参考linux源码。
2、读写锁
也叫写独占,读共享。当A线程加了写锁后,在未解锁的情况下B线程和C线程不能再对共享资源加锁,包括读锁和写锁。当A线程加了读锁后,B线程和C线程可以加读锁,但不能加写锁。性能比互斥锁好,但是比较适用于读取操作频繁的情况下。
3、递归锁
也叫可重入锁,在当前线程中可以对资源进行重复加锁,可以在一定程度上避免死锁问题
4、自旋锁
使用原子操作模拟互斥锁的行为就是自旋锁,互斥锁状态是由操作系统控制的,自旋锁的状态是程序员自己控制的。前面几种锁,有一个共同的特点,就是在加锁不成功时当前线程会阻塞,线程加入阻塞队列,让出cpu执行权。但自旋锁不会引起调用线程阻塞,它会一致占用CPU请求这个自旋锁使得CPU不能去做其他的事情,直到获取这个锁为止,适用于在短时间就可以获取到锁的情况下,否则会使CPU效率降低。
如果持有锁的线程能在短时间内释放锁资源,那么等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,只需要等一等(自旋),等到持有锁的线程释放锁后即可获取,避免用户进程和内核切换的消耗。
在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。
因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下,若持锁时间太长,性能降低。
C/C++ 中锁的使用
1、C/C++ 11之前
语言层面原生不提供锁的操作。使用锁,需要和平台相关。linux相关API:
-
互斥锁
#include <pthread.h>
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
pthread_mutex_tylock(&mutex);
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex); -
递归锁
Linux下的pthread_mutex_t 锁默认是非递归的。可以显示的设置PTHREAD_MUTEX_RECURSIVE属性,将pthread_mutex_t设为递归锁
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&Mutex, &attr); -
读写锁
#include <pthread.h>
pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_unlock(pthread_rwlock_t *rwlock); -
自旋锁
#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
2、C++11及以后
Mutex 类
std::mutex,最基本的 Mutex 类。
std::recursive_mutex,递归 Mutex 类。
std::time_mutex,定时 Mutex 类。
std::recursive_timed_mutex,定时递归 Mutex 类。
std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。
std::mutex 的成员函数
1、构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。
2、lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
3、unlock(), 解锁,释放对互斥量的所有权。
4、try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
std::recursive_mutex
与 std::mutex 一样,也是一种可以被上锁的对象,但是和 std::mutex 不同的是,std::recursive_mutex 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。
std::time_mutex
比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until()。
try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
try_lock_until 函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
std::shared_mutex
c++14 引入用来实现读写锁
std::unique_lock与std::lock_guard(锁管理器)
采用RAII机制管理锁对象指其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源,c++ 中智能指针,stl 都采用这个机制。在锁管理器中可以避免因为在对mutex进行lock之后忘记unlock造成死锁。
unique_lock比lock_guard使用更加灵活,用法更多,功能更加强大。但使用unique_lock相对需要付出额外的空间和时间成本
可参考cppreference: 两种管理器的区别
常规用法示例
互斥锁
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
int g_num = 0; // 为 g_num_mutex 所保护
std::mutex g_num_mutex;
void slow_increment(int id)
{
for (int i = 0; i < 3; ++i) {
g_num_mutex.lock();
++g_num;
std::cout << id << " => " << g_num << '\n';
g_num_mutex.unlock();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main()
{
std::thread t1(slow_increment, 0);
std::thread t2(slow_increment, 1);
t1.join();
t2.join();
}
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <sstream>
std::mutex cout_mutex; // 控制到 std::cout 的访问
std::timed_mutex mutex;
void job(int id)
{
using Ms = std::chrono::milliseconds;
std::ostringstream stream;
for (int i = 0; i < 3; ++i) {
if (mutex.try_lock_for(Ms(100))) {
stream << "success ";
std::this_thread::sleep_for(Ms(100));
mutex.unlock();
} else {
stream << "failed ";
}
std::this_thread::sleep_for(Ms(100));
}
std::lock_guard<std::mutex> lock(cout_mutex);
std::cout << "[" << id << "] " << stream.str() << "\n";
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(job, i);
}
for (auto& i: threads) {
i.join();
}
}
递归锁
#include <iostream>
#include <thread>
#include <mutex>
class X {
std::recursive_mutex m;
std::string shared;
public:
void fun1() {
std::lock_guard<std::recursive_mutex> lk(m);
shared = "fun1";
std::cout << "in fun1, shared variable is now " << shared << '\n';
}
void fun2() {
std::lock_guard<std::recursive_mutex> lk(m);
shared = "fun2";
std::cout << "in fun2, shared variable is now " << shared << '\n';
fun1(); // ① 递归锁在此处变得有用
std::cout << "back in fun2, shared variable is " << shared << '\n';
};
};
int main()
{
X x;
std::thread t1(&X::fun1, &x);
std::thread t2(&X::fun2, &x);
t1.join();
t2.join();
}
读写锁
#include <iostream>
//std::unique_lock
#include <mutex>
#include <shared_mutex>
#include <thread>
class ThreadSafeCounter {
public:
ThreadSafeCounter() = default;
// 多个线程/读者能同时读计数器的值。
unsigned int get() const {
std::shared_lock<std::shared_mutex> lock(mutex_);
return value_;
}
// 只有一个线程/写者能增加/写线程的值。
void increment() {
std::unique_lock<std::shared_mutex> lock(mutex_);
value_++;
}
// 只有一个线程/写者能重置/写线程的值。
void reset() {
std::unique_lock<std::shared_mutex> lock(mutex_);
value_ = 0;
}
private:
mutable std::shared_mutex mutex_;
unsigned int value_ = 0;
};
int main() {
ThreadSafeCounter counter;
auto increment_and_print = [&counter]() {
for (int i = 0; i < 3; i++) {
counter.increment();
std::cout << std::this_thread::get_id() << '\t' << counter.get() << std::endl;
}
};
std::thread thread1(increment_and_print);
std::thread thread2(increment_and_print);
thread1.join();
thread2.join();
system("pause");
return 0;
}
std::unique_lock与std::lock_guard共同使用
template <typename T>
class ThreadSafeQueue{
public:
void Insert(T value);
void Popup(T &value);
bool Empety();
private:
mutable std::mutex mut_;
std::queue<T> que_;
std::condition_variable cond_;
};
template <typename T>
void ThreadSafeQueue::Insert(T value){
std::lock_guard<std::mutex> lk(mut_);
que_.push_back(value);
cond_.notify_one();
}
template <typename T>
void ThreadSafeQueue::Popup(T &value){
std::unique_lock<std::mutex> lk(mut_);
cond_.wait(lk, [this]{return !que_.empety();});
value = que_.front();
que_.pop();
}
template <typename T>
bool ThreadSafeQueue::Empty() const{
std::lock_guard<std::mutex> lk(mut_);
return que_.empty();
}
std::unique_lock相对std::lock_guard更灵活的地方在于在等待中的线程如果在等待期间需要解锁mutex,并在之后重新将其锁定。而std::lock_guard却不具备这样的功能。当lambda表达式返回true时(即queue不为空),wait函数会锁定mutex。当lambda表达式返回false时,wait函数会解锁mutex同时会将当前线程置于阻塞状态。
此外boost库也提供了大量丰富的锁操作。
Golang中锁的使用
golang中sync包实现了两种锁Mutex (互斥锁)和RWMutex(读写锁),其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能。
type Mutex
func (m *Mutex) Lock()
func (m *Mutex) Unlock()
type RWMutex
func (rw *RWMutex) Lock()
func (rw *RWMutex) RLock()
func (rw *RWMutex) RLocker() Locker
func (rw *RWMutex) RUnlock()
func (rw *RWMutex) Unlock()
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var (
lock sync.Mutex
m = make(map[int]int)
)
for i := 0; i < 10; i++ {
i := i
go func() {
lock.Lock()
m[i] = i
lock.Unlock()
}()
}
time.Sleep(time.Second)
for k, v := range m {
fmt.Println(k, v)
}
}
死锁的产生
案例1:
使用mutex进行lock()后未调用unlock释放(包括异常退出导致未调用unlock)
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
int g_num = 0; // 为 g_num_mutex 所保护
std::mutex g_num_mutex;
void slow_increment(int id)
{
for (int i = 0; i < 3; ++i) {
g_num_mutex.lock();
++g_num;
if (i%2 == 1) {
return //直接返回未解锁,导致死锁
}
std::cout << id << " => " << g_num << '\n';
g_num_mutex.unlock();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main()
{
std::thread t1(slow_increment, 0);
std::thread t2(slow_increment, 1);
t1.join();
t2.join();
}
案例2:
使用非递归锁进行重复加锁
#include <iostream>
#include <thread>
#include <mutex>
class X {
std::mutex m;
std::string shared;
public:
void fun1() {
std::lock_guard<std::mutex> lk(m);
shared = "fun1";
std::cout << "in fun1, shared variable is now " << shared << '\n';
}
void fun2() {
std::lock_guard<std::mutex> lk(m);
shared = "fun2";
std::cout << "in fun2, shared variable is now " << shared << '\n';
fun1(); // ① 递归锁在此处变得有用
std::cout << "back in fun2, shared variable is " << shared << '\n';
};
};
int main()
{
X x;
std::thread t1(&X::fun1, &x);
std::thread t2(&X::fun2, &x);
t1.join();
t2.join();
}
案例3
使用两个mutex在多线程中互相加锁
#include <iostream>
#include <thread>
#include <mutex>
#include <deque>
class A{
private:
std::deque<int> my_deque;
std::mutex my_mutex_1;
std::mutex my_mutex_2;
public:
void Write(){
for (int i = 0; i < 1000; ++i){
my_mutex_1.lock();
my_mutex_2.lock();
std::cout << "向队列中添加一个元素" << std::endl;
my_deque.push_back(i);
my_mutex_1.unlock();
my_mutex_2.unlock();
}
}
void Read(){
for (int i = 0; i < 1000; ++i){
my_mutex_2.lock();
my_mutex_1.lock();
if (!my_deque.empty()){
std::cout << "读出队列的第一个元素: " << my_deque.front() << std::endl;
my_deque.pop_front();
}
my_mutex_2.unlock();
my_mutex_1.unlock();
}
}
};
int main(){
A a;
std::thread my_thread_1(&A::WriteFunction, std::ref(a));
std::thread my_thread_2(&A::ReadFunction, std::ref(a));
my_thread_1.join();
my_thread_2.join();
std::cout << "Hello World!\n";
}
避免死锁
1、使用RAII规则
使用std::unique_lock与std::lock_guard锁管理器,尽量避免直接使用mutex进行lock()和unlock()。防止因为线程的提前推出或者忘记解锁造成死锁问题
2、c++ 中使用std::lock()或者std::scoped_lock()
std::lock(my_mutex_1, my_mutex_2);的作用就是,同时去锁my_mutex_1, my_mutex_2,必须同时锁成功,一旦有一个互斥量不能被锁,线程就会卡在这里,直至两个锁可以被同时锁成功,这样就避免了死锁的现象。
#include <iostream>
#include <thread>
#include <mutex>
#include <deque>
class A{
private:
std::deque<int> my_deque;
std::mutex my_mutex_1;
std::mutex my_mutex_2;
public:
void WriteFunction(){
for (int i = 0; i < 1000; ++i){
std::lock(my_mutex_1, my_mutex_2);
std::cout << "向队列中添加一个元素" << std::endl;
my_deque.push_back(i);
my_mutex_1.unlock();
my_mutex_2.unlock();
}
}
void ReadFunction(){
for (int i = 0; i < 1000; ++i){
std::lock(my_mutex_1, my_mutex_2);
if (!my_deque.empty()){
std::cout << "读出队列的第一个元素: " << my_deque.front() << std::endl;
my_deque.pop_front();
}
my_mutex_2.unlock();
my_mutex_1.unlock();
}
}
};
int main()
{
A a;
std::thread my_thread_1(&A::WriteFunction, std::ref(a));
std::thread my_thread_2(&A::ReadFunction, std::ref(a));
my_thread_1.join();
my_thread_2.join();
std::cout << "Hello World!\n";
}
3、在一定要对多个mutex加锁的情况下,按固定顺序加锁
如果确实需要锁定多个mutex,而这些mutex可以被同时锁定,使用前面std::lock方法来保证锁定mutex的顺序而避免死锁。如果这些mutex只能分别被锁定,则需要在实现时保证锁定mutex的顺序是固定的,比如总是在锁定B之前锁定A,在锁定C之前锁定B。
4、使用递归锁
可参考std::recursive_mutex的案例
5、尽可能减小锁的粒度
在需要加锁的函数中,尽可能缩小加锁的代码块范围,即能提高整体性能也能一定程度上减少出错产生死锁的概率