线程之间的锁有:互斥锁、读写锁、递归锁、自旋锁。一般而言,锁的功能越强大,性能就会越低。
std::mutex-互斥锁
互斥锁用于控制多个线程对他们之间共享资源互斥访问的一个信号量。也就是说是为了避免多个线程在某一时刻同时操作一个共享资源。例如线程池中的有多个空闲线程和一个任务队列。任何是一个线程都要使用互斥锁互斥访问任务队列,以避免多个线程同时访问任务队列以发生错乱。
在某一时刻,只有一个线程可以获取互斥锁,在释放互斥锁之前其他线程都不能获取该互斥锁。如果其他线程想要获取这个互斥锁,那么这个线程只能以阻塞方式进行等待。
另有一种形式std::timed_mutex:超时机制的互斥锁
std::shared_mutex-读写锁
访问者一般有两种:读者和写者,写者是一种排他的访问方式,即独占资源,读者可以是共享的,就是说可以有多个线程同时去访问某个资源,所以,读写锁也可以叫做共享-独占锁。读写锁比起mutex具有更高的适用性、更高的并行性,可以有多个线程同时占用读模式的读写锁,但是只能有一个线程占用写模式的读写锁。读写锁最适用于对数据结构的读操作次数多于写操作的场合,因为,读模式锁定时可以共享,而写模式锁定时只能某个线程独占资源。
另有一种形式std::shared_timed_mutex
std::recursive_mutex-递归锁
所谓递归锁,就是在同一线程上该锁是可重入的,对于不同线程则相当于普通的互斥锁。
另有一种形式std::recursive_timed_mutex
自旋锁
自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
实现自旋锁:
class SpinMutex
{
std::atomic_flag flag;
public:
sSpinMutex()
{
flag=ATOMIC_FLAG_INIT;
}
void lock()
{
while(flag.test_and_set(std::memory_order_acquire));
}
void unlock()
{
flag.clear(std::memory_order_release);
}
};
std::lock_guard
严格基于作用域的锁管理类模板,构造时是否加锁是可选的(不加锁时假定当前线程已经获得锁的所有权),析构时自动释放锁,所有权不可转移,对象生存期内不允许手动加锁和释放锁。
std::unique_lock
更加灵活的锁管理类模板,构造时是否加锁是可选的,在对象析构时如果持有锁会自动释放锁,所有权可以转移。对象生命期内允许手动加锁和释放锁。
std::shared_lock
用于管理可转移和共享所有权的互斥对象。
std::scoped_lock
类 scoped_lock提供便利RAII风格 机制的互斥包装器,它在作用域块的存在期间占有一或多个互斥。创建scoped_lock 对象时,它试图取得给定互斥的所有权。控制离开创建 scoped_lock对象的作用域时,析构scoped_lock并以逆序释放互斥。若给出数个互斥,则使用免死锁算法,如同以std::lock。scoped_lock类不可复制。在处理多个互斥量时,特别简单。
官方例子:
#include
#include
#include
#include
#include
#include
#include
struct Employee {
Employee(std::string id) : id(id) {}
std::string id;
std::vector<:string> lunch_partners;
std::mutex m;
std::string output() const
{
std::string ret = "Employee " + id + " has lunch partners: ";
for( const auto& partner : lunch_partners )
ret += partner + " ";
return ret;
}
};
void send_mail(Employee &, Employee &)
{
// simulate a time-consuming messaging operation
std::this_thread::sleep_for(std::chrono::seconds(1));
}
void assign_lunch_partner(Employee &e1, Employee &e2)
{
static std::mutex io_mutex;
{
std::lock_guard<:mutex> lk(io_mutex);
std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
}
{
// use std::scoped_lock to acquire two locks without worrying about
// other calls to assign_lunch_partner deadlocking us
// and it also provides a convenient RAII-style mechanism
std::scoped_lock lock(e1.m, e2.m);
// Equivalent code 1 (using std::lock and std::lock_guard)
// std::lock(e1.m, e2.m);
// std::lock_guard<:mutex> lk1(e1.m, std::adopt_lock);
// std::lock_guard<:mutex> lk2(e2.m, std::adopt_lock);
// Equivalent code 2 (if unique_locks are needed, e.g. for condition variables)
// std::unique_lock<:mutex> lk1(e1.m, std::defer_lock);
// std::unique_lock<:mutex> lk2(e2.m, std::defer_lock);
// std::lock(lk1, lk2);
{
std::lock_guard<:mutex> lk(io_mutex);
std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
}
e1.lunch_partners.push_back(e2.id);
e2.lunch_partners.push_back(e1.id);
}
send_mail(e1, e2);
send_mail(e2, e1);
}
int main()
{
Employee alice("alice"), bob("bob"), christina("christina"), dave("dave");
// assign in parallel threads because mailing users about lunch assignments
// takes a long time
std::vector<:thread> threads;
threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(bob));
threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(alice));
threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));
for (auto &thread : threads) thread.join();
std::cout << alice.output() << '\n' << bob.output() << '\n'
<< christina.output() << '\n' << dave.output() << '\n';
}