序
数据同步是多线程编程中不可避免的话题,下面来探讨一下数据同步相关的知识点。
基础知识
互斥量:互斥量是一种可被锁定的变量,互斥量一般有排他互斥量、共享互斥量以及循环互斥量。排他互斥量是一种只能被一个线程访问(锁定)的互斥量,当一个线程获得排他互斥量的锁后,其他线程只有在该互斥的锁释放后才能获得这个互斥量的锁;共享互斥量一般实现为读共享、写排他的互斥量,换句话说就是该互斥量的读锁可以被多个线程获取(因为读并不会改变数据),而写锁只能被一个线程获取; 循环互斥量本质也是一种排他互斥量,所不同的是,循环互斥量被一个线程获得其锁后,该线程可以多次锁定该互斥量,当然,最后在释放该互斥量时,有多少次锁定,就应该释放多少次。C++11标准库中提供了排他互斥量和循环互斥量以及其他特定功能的互斥量,基础的排他互斥量和循环互斥量分别是std::mutex和std::recursive_mutex,可惜的是,C++标准委员会在经过商讨后,决定不在C++11中提供共享互斥量。但这个互斥量在boost库中是受到支持的。
锁:出于方便使用和安全的目的,对互斥量进行了封装,这就是锁。C++11提供了简单的锁std::lock_guard和灵活的锁std::unique_lock。
mutex
mutex,使用时非常简单,例程如下:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex wrMutex;
int Total = 0;
void test(bool flag)
{
int factor = flag == true ? 3 : -3;
for (int i = 0; i < 100000; ++i)
{
wrMutex.lock();
Total += factor;
wrMutex.unlock();
}
}
int main()
{
std::thread t1(test, true);
std::thread t2(test, false);
t1.join();
t2.join();
std::cout << "Total is :" << Total << std::endl;
std::cin.get();
return 0;
}
lock_guard
单纯地使用std::mutex,这时可能会有潜在的安全隐患——当忘记调用unlock()或者unlock()调用与lock()调用不对称时,这时就可能发生死锁或者未定义的错误。由于这种安全隐患的存在,再加上RAII的设计思想,lock_guard就相应地出现了。lock_guard的使用例程如下:
#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list;
std::mutex some_mutex;
void add_to_list(int new_value)
{ std::lock_guard<std::mutex> guard(some_mutex);
some_list.push_back(new_value);
}
bool list_contains(int value_to_find)
{
std::lock_guard<std::mutex> guard(some_mutex);
return std::find(some_list.begin(), some_list.end(), value_to_find)
!= some_list.end();
}
lock_guard是一个模板类,它模板参数是互斥量类型,同时还要传进一个相应的互斥量实例。lock_guard在构造函数中锁定互斥量,在析构函数中释放互斥量,因此使用lock_guard后,互斥量不需要显式地锁定或者释放。
unique_lock
unique_lock是一种相当灵活的锁,它也是一个模板类,模板参数为互斥量类型,初始化时也需要传入一个互斥量的实例。为了详细地了解unique_lock,我们首先从其构造函数开始。
(1)
unique_lock() noexcept;
(2)
explicit unique_lock (mutex_type& m);
(3)
unique_lock (mutex_type& m, try_to_lock_t tag);
(4)
unique_lock (mutex_type& m, defer_lock_t tag) noexcept;
(5)
unique_lock (mutex_type& m, adopt_lock_t tag);
template <class Rep, class Period>
unique_lock (mutex_type& m, const chrono::duration<Rep,Period>& rel_time);
template <class Clock, class Duration>
unique_lock (mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time);
copy [deleted]
unique_lock (const unique_lock&) = delete;
move
unique_lock (unique_lock&& x);
我们主要关注第(2)(3)(4)(5)构造函数。
(2)构造函数需要传入一个互斥量实例,这时使用unique_lock和lock_guard的效果是一样的;
(3)(4)(5)构造函数的不同,主要表现在传入的tag参数的值不同。
tag的可选值如下:
try_to_lock:尝试获得互斥量的锁
defer_lock:延迟锁定(在以后手动锁定)
adopt_lock:接收一个互斥量(这个互斥量已经在外部被加锁)
无论使用何种tag,还是传入的是排他互斥量或者循环互斥量,只要构造时是正确的,unique_lock对象就能够在析构时正常释放其锁。
unique_lock主要成员函数:
这些成员函数简单易懂,无需多说。
recursive_mutex
循环互斥量是一种可以连续锁定的互斥量,当数据的同步需要多重验证时,这时循环互斥量就会相当有用。简单的例程如下:
void test2()
{
std::recursive_mutex Mutex;
Mutex.lock();
Mutex.lock();
Turn++;
Mutex.unlock();
Mutex.unlock();
}
timed_mutex
std::timed_mutex类似于std::mutex,所不同的是,timed_mutex有了时间控制功能,其增加的两个函数是:
try_lock_for(chrono::duration<Rep,Period>& rel_time)
try_lock_until(const const chrono::time_point<Clock,Duration>& abs_timerel_time)
try_lock_for函数将等待最长rel_time的时间,在这个时间段内尝试获得锁,否则获取锁失败,不继续等待,线程继续运行。
trye_lock_for函数将等待最迟到abs_timerel_time这个时间点,在这个时间点前尝试获得锁,否则获取锁失败,不继续等待,线程继续运行。
recursive_timed_mutex
std::recursive_timed_mutex是timed_mutex相应的加强版,在此不再赘述。