C++ 线程锁 mutex 理解 附代码
1. 概述
对C++
里面的mutex
相关知识进行梳理,理解线程锁
。
2. 理解
mutex英文含义如下,表示一种不相容的性质。
mutex
n. 互斥;互斥元,互斥体;互斥量
- C++里面的mutex类是用来进行
线程同步
,保护数据
的,防止不同线程对同一数据同时进行处理
。如果不使用线程锁会使程序不稳定,达不到预期的效果。 - 它实际上是用来
对线程进行处理的,不是直接处理数据
。不同的线程只有拿到了线程锁才可以继续运行
,否则需要等待其他线程解除线程锁。 - 线程进行同步时需要进行线程锁的
加锁
、解锁
的操作。如果加锁之后没有及时解锁
可能会导致死锁
现象。因为其他线程会等待其他线程解除线程锁,如果一直等不到,可能会一直处于阻塞
的状态。
boost中的锁有
class mutex
typedef mutex try_mutex;
class timed_mutex
typedef unique_lock<mutex> scoped_lock;
typedef detail::try_lock_wrapper<mutex> scoped_try_lock;
typedef unique_lock<timed_mutex> scoped_timed_lock;
typedef detail::try_lock_wrapper<timed_mutex> scoped_try_lock;
typedef scoped_timed_lock scoped_lock;
3. boost::mutex
C++使用线程锁时最简单的就是使用boost::mutex。
3.1 无锁
如果不使用线程锁
,代码示例如下
使用命令
g++ nomutextest.cpp -o nomutex -lboost_system -lboost_thread
#include <boost/thread/thread.hpp>
#include <iostream>
#include <iomanip>
#include <unistd.h>
/*
* g++ nomutextest.cpp -o nomutex -lboost_system -lboost_thread
*/
int num = 5;
void helloA()
{
std::cout << "****I'm thread A ! " << boost::this_thread::get_id() << " --- Start " << std::endl;
std::cout << "****I'm thread A the num is "<<num <<std::endl;
num++;
std::cout << "****I'm thread A the num+1 is "<<num <<std::endl;
num++;
std::cout << "****I'm thread A the num+1+1 is "<<num <<std::endl;
sleep(1);
std::cout << "****I'm thread A ! --- OVER " << std::endl;
}
void helloB()
{
std::cout << "I'm thread B ! " << boost::this_thread::get_id() << " --- Start " << std::endl;
std::cout << "I'm thread B the num is "<<num <<std::endl;
num--;
std::cout << "I'm thread B the num-1 is "<<num <<std::endl;
num--;
std::cout << "I'm thread B the num-1-1 is "<<num <<std::endl;
std::cout << "I'm thread B ! --- OVER " << std::endl;
}
int main(int argc, char* argv[])
{
// 建立并执行两个线程
boost::thread thrdA(&helloA);
boost::thread thrdB(&helloB);
thrdA.join(); // 等待子线程完成后再继续执行主进程;
thrdB.join();
// 等待两个 join 后才会继续执行
std::cout<< " ==== over ==== "<<std::endl;
return 0;
}
由于线程实际运行时对num处理的先后顺序不一样导致结果不一样。
可能thrdA先对num进行加处理,
可以允许
也有可能thrdB先对num进行减处理,
可以允许
也有可能thrdA、thrdB对num同时进行处理
。禁止这种情况
输出结果可能为下面情况
线程A先对num进行处理
A5 -> A6 -> A7->B7 -> B6 -> B5
****I'm thread A ! 7f01551c6700 --- Start I'm thread B !
****I'm thread A the num is 5
****I'm thread A the num+1 is 6
****I'm thread A the num+1+1 is 7
7f01549c5700 --- Start
I'm thread B the num is 7
I'm thread B the num-1 is 6
I'm thread B the num-1-1 is 5
I'm thread B ! --- OVER
****I'm thread A ! --- OVER
==== over ====
或者
线程B先对num进行处理,
B5 -> B4 -> B3 -> A3 -> A4 -> A5
I'm thread B ! 7ff008235700 --- Start
I'm thread B the num is 5
I'm thread B the num-1 is 4
I'm thread B the num-1-1 is 3
I'm thread B ! --- OVER
****I'm thread A ! 7ff008a36700 --- Start
****I'm thread A the num is 3
****I'm thread A the num+1 is 4
****I'm thread A the num+1+1 is 5
****I'm thread A ! --- OVER
==== over ====
或者
线程A、B同时对num进行处理。
A5 -> A6 -> B6 -> B5 -> B4 -> A5
****I'm thread A ! I'm thread B ! 7f1c044567007f1c04c57700 --- Start --- Start
****I'm thread A the num is 5
****I'm thread A the num+1 is
I'm thread B the num is 6
I'm thread B the num-1 is 5
6I'm thread B the num-1-1 is 4
I'm thread B ! --- OVER
****I'm thread A the num+1+1 is 5
****I'm thread A ! --- OVER
==== over ====
以及其他情况,就不一一列举了。
A5 -> B5 -> B4 -> B3 -> A4 -> A5
****I'm thread A ! 7fed8d265700 --- Start
I'm thread B ! 7fed8ca64700****I'm thread A the num is --- Start
I'm thread B the num is 5
5I'm thread B the num-1 is 4
I'm thread B the num-1-1 is 3
I'm thread B ! --- OVER
****I'm thread A the num+1 is 4
****I'm thread A the num+1+1 is 5
****I'm thread A ! --- OVER
==== over ====
3.2 有锁
如果使用线程锁
,示例代码如下
#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp> //定义锁
#include <iostream>
#include <unistd.h>
/*
* g++ mutextest.cpp -o mutextest -lboost_system -lboost_thread
*/
boost::mutex lock; //互斥锁
using namespace std;
int num = 5;
void helloA()
{
std::cout << "****I'm thread A ! " << boost::this_thread::get_id() << " --- Start " << std::endl;
lock.lock(); // 锁住变量 num, 另一处调用将在此处运行完后再继续运行
std::cout << "****I'm thread A the num is "<<num <<std::endl;
num++;
std::cout << "****I'm thread A the num+1 is "<<num <<std::endl;
num++;
std::cout << "****I'm thread A the num+1+1 is "<<num <<std::endl;
sleep(1);
lock.unlock();
std::cout << "****I'm thread A ! --- OVER " << std::endl;
}
void helloB()
{
std::cout << "I'm thread B ! " << boost::this_thread::get_id() << " --- Start " << std::endl;
lock.lock();
std::cout << "I'm thread B the num is "<<num <<std::endl;
num--;
std::cout << "I'm thread B the num-1 is "<<num <<std::endl;
num--;
std::cout << "I'm thread B the num-1-1 is "<<num <<std::endl;
sleep(1);
lock.unlock();
std::cout << "I'm thread B ! --- OVER " << std::endl;
}
int main(int argc, char* argv[])
{
// 建立并执行两个线程
boost::thread thrdA(&helloA);
boost::thread thrdB(&helloB);
thrdB.join();
thrdA.join(); // 等待子线程完成后再继续执行主进程;
// 等待两个 join 后才会继续执行
std::cout<< " ==== over ==== "<<std::endl;
return 0;
}
输出结果只可能为下面情况,
每次num的处理都是在同一个线程里,如果thrdA处理num,那么thrdB就不能处理num,反之同理。
因此不会出现线程A、线程B同时对num进行处理的情况
。
线程A先对num进行处理。
A5 -> A6 -> A7 -> B7 -> B6 -> B5
****I'm thread A ! 7f9335fd1700 --- Start
I'm thread B ! 7f93357d0700 --- Start
****I'm thread A the num is 5
****I'm thread A the num+1 is 6
****I'm thread A the num+1+1 is 7
****I'm thread A ! --- OVER
I'm thread B the num is 7
I'm thread B the num-1 is 6
I'm thread B the num-1-1 is 5
I'm thread B ! --- OVER
==== over ====
线程B先对num进行处理。
B5 -> B4 -> B3 -> A3 -> A4 -> A5
****I'm thread A ! I'm thread B ! 7f30f291f700 --- Start
7f30f3120700I'm thread B the num is --- Start
5
I'm thread B the num-1 is 4
I'm thread B the num-1-1 is 3
I'm thread B ! --- OVER
****I'm thread A the num is 3
****I'm thread A the num+1 is 4
****I'm thread A the num+1+1 is 5
****I'm thread A ! --- OVER
==== over ====
3.3 mutex类
查看源代码,可以看到mutex本身还是使用pthread_mutex_t
实现线程锁。主要有lock()
,unlock()
,try_lock()
三个成员函数。
class mutex
{
private:
pthread_mutex_t m;
public:
BOOST_THREAD_NO_COPYABLE(mutex)
mutex()
{
//初始化mutex
int const res=pthread_mutex_init(&m,NULL);
if(res)
{
boost::throw_exception(thread_resource_error(res, "boost:: mutex constructor failed in pthread_mutex_init"));
}
}
~mutex()
{
//释放mutex
int const res = posix::pthread_mutex_destroy(&m);
boost::ignore_unused(res);
BOOST_ASSERT(!res);
}
void lock()
{
//加锁mutex
int res = posix::pthread_mutex_lock(&m);
if (res)
{
boost::throw_exception(lock_error(res,"boost: mutex lock failed in pthread_mutex_lock"));
}
}
void unlock()
{
//解锁mutex
int res = posix::pthread_mutex_unlock(&m);
(void)res;
BOOST_ASSERT(res == 0);
// if (res)
// {
// boost::throw_exception(lock_error(res,"boost: mutex unlock failed in pthread_mutex_unlock"));
// }
}
bool try_lock()
{
int res;
do
{
//尝试加锁mutex
res = pthread_mutex_trylock(&m);
} while (res == EINTR);
if (res==EBUSY)
{
return false;
}
return !res;
}
对于pthread_mutex_t
的使用主要有下面几条:
函数:
pthread_mutex_init
(pthread_mutex_t * mutex, const phtread_mutexattr_t * mutexattr); //动态方式创建锁,相当于new动态创建一个对象pthread_mutex_t
mutex = PTHREAD_MUTEX_INITIALIZER; //以静态方式创建锁pthread_mutex_destory
(pthread_mutex_t *mutex) //释放互斥锁,相当于deletepthread_mutex_lock
(pthread_mutex_t *mutex)pthread_mutex_unlock
(pthread_mutex_t *mutex)- int
pthread_mutex_trylock
(pthread_mutex_t * mutex); //会尝试对mutex加锁。如果mutex之前已经被锁定,返回非0,;如果mutex没有被锁定,则函数返回并锁定mutex;
pthread_mutex
初始化时,需要传入参数pthread_mutexattr_t
,有下列值可选
PTHREAD_MUTEX_TIMED_NP
,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。PTHREAD_MUTEX_RECURSIVE_NP
,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。PTHREAD_MUTEX_ERRORCHECK_NP
,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。PTHREAD_MUTEX_ADAPTIVE_NP
,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
这些都在mutex
类中有使用。
typedef mutex try_mutex;
4. boost::timed_mutex
时间锁timed_mutex
,与mutex
相比,可以设置加锁时等待的绝对时间、相对时间
,超时自动退出,不再加锁。
- 时间锁使用:在
指定时间之内
进行加锁,如果不能加锁,自动退出
。
列举重要的函数如下
- bool timed_lock(TimeDuration const & relative_time)相对时间锁
- bool timed_lock(boost::xtime const & absolute_time)绝对时间锁
- bool timed_lock(system_time const & abs_time)绝对时间锁
class timed_mutex
{
private:
pthread_mutex_t m;
、、、
public:
template<typename TimeDuration>
bool timed_lock(TimeDuration const & relative_time)
{
return timed_lock(get_system_time()+relative_time);
}
bool timed_lock(boost::xtime const & absolute_time)
{
return timed_lock(system_time(absolute_time));
}
bool timed_lock(system_time const & abs_time)
{
struct timespec const ts=boost::detail::to_timespec(abs_time);
return do_try_lock_until(ts);
}
private:
bool do_try_lock_until(struct timespec const &timeout)
{
int const res=pthread_mutex_timedlock(&m,&timeout);
BOOST_ASSERT(!res || res==ETIMEDOUT);
return !res;
}
5. boost::mutex::scoped_lock
可以自动加锁,自动解锁,不用手动进行这些操作,作用范围是my_lock的作用域。构造时自动上锁,析构时自动解锁
。
使用unique_lock
来实现这个功能。如下
typedef unique_lock<mutex> scoped_lock;
unique_lock
的内部结构包含了一个唯一的mutex类型
,这个可以是时间锁、普通锁等。将里面重要的信息列举如下
- bool owns_lock()是否有锁
- lock()加锁,
构造函数自动加锁
- unlock()解锁,
析构函数自动解锁
- try_lock()尝试加锁
- swap()交换锁以及锁的状态
template <typename Mutex>
class unique_lock
{
private:
Mutex* m;
bool is_locked;
private:
explicit unique_lock(upgrade_lock<Mutex>&);
unique_lock& operator=(upgrade_lock<Mutex>& other);
public:
explicit unique_lock(Mutex& m_) :
m(&m_), is_locked(false)
{
lock();
}
bool owns_lock() const BOOST_NOEXCEPT
{
return is_locked;
}
void lock()
{
if (m == 0)
{
boost::throw_exception(
boost::lock_error(static_cast<int>(system::errc::operation_not_permitted), "boost unique_lock has no mutex"));
}
if (owns_lock())
{
boost::throw_exception(
boost::lock_error(static_cast<int>(system::errc::resource_deadlock_would_occur), "boost unique_lock owns already the mutex"));
}
m->lock();
is_locked = true;
}
bool try_lock()
{
if (m == 0)
{
boost::throw_exception(
boost::lock_error(static_cast<int>(system::errc::operation_not_permitted), "boost unique_lock has no mutex"));
}
if (owns_lock())
{
boost::throw_exception(
boost::lock_error(static_cast<int>(system::errc::resource_deadlock_would_occur), "boost unique_lock owns already the mutex"));
}
is_locked = m->try_lock();
return is_locked;
}
~unique_lock()
{
if (owns_lock())
{
m->unlock();
}
}
void swap(unique_lock& other)BOOST_NOEXCEPT
{
std::swap(m,other.m);
std::swap(is_locked,other.is_locked);
}
5.1 示例
boost::mutex::scoped_lock my_lock (lock);
等价与之前的
lock.lock();
、、、
lock.unlock();
6. std::mutex
除了boost库
,C++11
中也包含了mutex类
。
C++11中新增了<mutex>
头文件,实现了C++11标准中的一些互斥访问的类与方法等。其中std::mutex可以进行加锁lock、解锁unlock。也可以自动加锁解锁,std::lock_guard与std::mutex配合使用
,把锁放到lock_guard中时,mutex自动上锁
,lock_guard析构时,同时把mutex解锁
。
-
lock_guard
:更加灵活的锁管理类模板,构造时是否加锁是可选
的,在对象析构时如果持有锁会自动释放锁
,所有权可以转移
。对象生命期内允许手动加锁和释放锁
。 -
scope_lock
:严格基于作用域(scope-based)的锁管理类模板,构造时是否加锁是可选
的(不加锁时假定当前线程已经获得锁的所有权),析构时自动释放锁
,所有权不可转移
,对象生存期内不允许手动加锁和释放锁
。 -
share_lock
:用于管理可转移和共享所有权
的互斥对象。
std::lock_guard
、std::unique_lock
和std::shared_lock
类模板在构造时是否加锁是可选的,可选参数有
参数 | 功能 |
---|---|
(默认) | 请求锁 ,阻塞 当前线程直到成功获得锁 |
std::defer_lock | 不请求锁 |
std::try_to_lock | 尝试请求 锁,但不阻塞 线程,锁不可用时也会立即返回。 |
std::adopt_lock | 假定 当前线程已经获得互斥对象的所有权,所以不再请求锁 。 |
7. 死锁
7.1 示例
一个死锁示例如下
std::mutex mt1, mt2;
// thread 1
{
std::lock_guard<std::mutex> lck1(mt1);
std::lock_guard<std::mutex> lck2(mt2);
// do something
}
// thread 2
{
std::lock_guard<std::mutex> lck2(mt2);
std::lock_guard<std::mutex> lck1(mt1);
// do something
}
可能会出现
- thread 1持有mt1等待mt2,
- thread 2持有mt2等待mt1,
死锁出现,为了避免这种情况,对于任意两个互斥对象,在多个线程中进行加锁时应保证其先后顺序是一致
std::mutex mt1, mt2;
// thread 1
{
std::lock_guard<std::mutex> lck1(mt1);
std::lock_guard<std::mutex> lck2(mt2);
// do something
}
// thread 2
{
std::lock_guard<std::mutex> lck1(mt1);
std::lock_guard<std::mutex> lck2(mt2);
// do something
}
也可以使用std::lock
和std::try_lock
函数来对多个Lockable对象加锁。
std::try_lock
不会阻塞线程当有对象不可用时会释放已经加锁的其他对象并立即返回。- 当待加锁的对象中有不可用对象时
std::lock
会阻塞当前线程知道所有对象都可用。该函数使用未指定的调用序列来锁定对象的成员lock、try_lock和unlock,以确保在返回时锁定所有参数
(不会产生任何死锁
)。如果函数不能锁定所有对象
(例如,因为它的一个内部调用抛出了一个异常),函数在失败之前首先解锁它成功锁定的所有对象
(如果有的话)。
7.2 示例
防止死锁的代码如下
使用命令
g++ nodeadlock.cpp -o nodeadlock -std=c++11 -lpthread
#include <thread>
#include <mutex>
#include <iostream>
#include <unistd.h>
int g_i = 5;
std::mutex mt1; //
std::mutex mt2; //
//g++ nodeadlock.cpp -o nodeadlock -std=c++11 -lpthread
void testA()
{
std::lock(mt1, mt2);//去掉将会死锁
std::cout<<"****test A g_i "<<g_i<<std::endl;
std::lock_guard<std::mutex> lck1(mt1,std::adopt_lock);//这个时候已经获得了锁,因此使用adopt_lock
g_i++;
sleep(2);
std::cout<<"****test A g_i+1 "<<g_i<<std::endl;
std::lock_guard<std::mutex> lck2(mt2,std::adopt_lock);
g_i++;
std::cout<<"****test A g_i+1+1 "<<g_i<<std::endl;
}
void testB()
{
std::lock(mt1, mt2);
std::cout<<"test B g_i "<<g_i<<std::endl;
std::lock_guard<std::mutex> lck2(mt2,std::adopt_lock);
g_i--;
sleep(2);
std::cout<<"test B g_i-1 "<<g_i<<std::endl;
std::lock_guard<std::mutex> lck1(mt1,std::adopt_lock);
g_i--;
std::cout<<"test B g_i-1-1 "<<g_i<<std::endl;
}
int main()
{
std::thread t1(testA);
std::thread t2(testB);
t1.join();
t2.join();
std::cout<<"=====over====="<<std::endl;
}
输出结果为,已经没有了死锁
****test A g_i 5
****test A g_i+1 6
****test A g_i+1+1 7
test B g_i 7
test B g_i-1 6
test B g_i-1-1 5
=====over=====
7.3 防止死锁
三种用于避免死锁的技术:
加锁顺序(线程按照一定的顺序加锁)
按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁并对这些锁做适当的排序,但总有些时候是无法预知的。
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。
死锁检测