第3章 线程共享数据
[TOC]
本章内容
- 线程间访问共享数据的问题
- 利用互斥保护共享数据
- 其他方法保护共享数据
1. 线程间共享数据的问题
不变量
对数据的断言
类似于循环不变式, 数列通项
问题
当代码段 A 对共享数据 D 以不变量 V 为假设处理时
- 若不变量 V 满足, 就正确处理
- 若不变量 V 并未满足, 就出问题
// A:
{
process(D); // 以不变量 V 满足为前提处理
}
例:
不变量: 双向链表中, 若 , 则
操作: 删除双向链表的一个结点
void delete(Node n)
{
// 不变量满足
n->pre->next = n->next; // 不变量不满足, 若以不变量满足为前提访问该双向链表, 会出问题
n->next->pre = n->pre; // 不变量再次满足
}
条件竞争
多个线程, 各自的操作序列混杂为一个操作序列, 若结果取决于相对次序, 称为条件竞争
场景
线程 A 有操作序列: a1 a2
线程 B 有操作序列: b1 b2
混杂为一个操作序列有 6 种情况:
- a1 a2 b1 b2
- a1 b1 a2 b2
- a1 b1 b2 a2
- b1 a1 a2 b2
- b1 a1 b2 a2
- b1 b2 a1 a2
问题
线程 A 的 a1 执行之后, a2 开始之前, 不变量 V 被破坏
线程 B 的 b1 b2 都以不变量 V 满足为前提
则第 2, 3, 5 三种情况都在 V 不满足时访问 V
第 1, 4, 6 三种情况在 V 满足时访问 V
1, 4, 6, 相当于加了锁
有 50% 的情况出问题, 50% 的情况不出问题
2 a1 b1 a2 b2
3 a1 b1 b2 a2
5 b1 a1 b2 a2
防止恶性条件竞争
恶性条件竞争: 出问题时的条件竞争
- 用互斥保护共享数据: 有锁编程
- 修改数据结构的设计: 无锁编程
- 用事物来处理: 操作序列视为原子操作
2. mutex 源码
class mutex : public _Mutex_base
{
public:
mutex(): _Mutex_base() {}
mutex(const mutex&) = delete;
mutex& operator=(const mutex&) = delete;
};
class mutex_base
{ // base class for all mutex types
public:
mutex_base(int flag = 0)
{
_Mtx_init_in_situ(mymtx(), flag | mtx_try);
}
~mutex_base()
{
_Mtx_destroy_in_situ(mymtx());
}
mutex_base(const mutex_base&) = delete;
mutex_base& operator=(const mutex_base&) = delete;
void lock()
{
_Check_C_return(_Mtx_lock(mymtx()));
}
bool try_lock()
{
const auto res = _Mtx_trylock(mymtx());
switch (res)
{
case _Thrd_success:
return true;
case _Thrd_busy:
return false;
default:
_Throw_C_error(res);
}
}
void unlock()
{
_Mtx_unlock(mymtx());
}
private:
struct mtx_storage;
void* mymtx()
{
return reinterpret_cast<void*>(&mtx_storage);
}
};
3. mutex 操作
互斥量 mutex 表示资源的个数
- 1 表示互斥未被占有
- 0 表示互斥被某个线程占有
lock() 阻塞
n 个线程, 1 个互斥 m, 线程 A 调用 m.lock()
- m = 1, 成功
- m = 0, A 被阻塞, 直至 m = 1
void lock()
{
while(m == 0){}
}
mutex lock() 两次会死锁
mutex m;
m.lock();
m.lock();
...
m.unlock();
m.unlock();
分析
线程对互斥量 m 有 lock\unlock
操作对, 操作对的连线表示临界区, 任意两个操作对的连线不能相交
l 指 lock 操作, u 指 unlock 操作, l 和 u 的连线表示临界区
则任意 l 和 u 的连线不能相交, 包括同一线程和不同线程间的 l 和 u 连线
线程 A 对 m 有 l1 u1, 线程 B 对 m 有 l2 u2, 根据任意两个操作对的连线不能相交的限制:
- l1 u1 l2 u2
- l2 u2 l1 u1
则会排除上述混杂操作序列的出问题的三种情况
线程 A 有操作序列: a1 a2
线程 B 有操作序列: b1 b2
混杂为一个操作序列有 6 种情况:
- a1 a2 b1 b2
- a1 b1 a2 b2
- a1 b1 b2 a2
- b1 a1 a2 b2
- b1 a1 b2 a2
- b1 b2 a1 a2
三种情况出问题, 三种情况不出问题
新的操作序列:
线程 A : l1 a1 a2 u1
线程 B : l2 b1 b2 u2
则对 6 种混杂操作序列的分析:
-
a1 a2 b1 b2 正确
l1 a1 a2 u1 l2 b1 b2 u2
-
a1 b1 a2 b2 排除
-
a1 b1 b2 a2 排除
-
b1 a1 a2 b2 mutex 排除, recursive_mutex 在同一线程时正确, 不同线程时排除
-
b1 a1 b2 a2 排除
-
b1 b2 a1 a2 正确
unlock()
线程调用
- 若 mutex 为 0, 则置为 1
- 若 mutex 为 1, 则不操作
void unlock()
{
if(m == 0)
m = 1;
}
unlock() 两次会让本应阻塞的线程不被阻塞
线程 A unlock
两次对线程 A 没有问题
但对线程 B, 有可能因为多一次 unlock
而进入临界区, 所以第二次 unlock 不会成功
a1 b1 a2 c1 b2 c2 : B, C 在非正常情况下进入临界区
A:
{
m.lock();
...
m.unlock(); a1
m.unlock(); a2
}
B:
{
m.lock(); b1
...
m.unlock(); b2
}
C:
{
m.lock(); c1
...
m.unlock(); c2
}
第二次 unlock() 会无效
mutex m;
m.lock();
...
m.unlock();
m.unlock(); // 相当于没有操作
try_lock() 非阻塞
n 个线程, 1 个互斥 m, 线程 A 调用 m.try_lock()
- m = 1, 成功, 令 m = 0 后返回 true
- m = 0, 失败, 返回 false, 不被阻塞
void try_lock()
{
if(m == 1)
{
m = 0;
return true;
}
return false;
}
try_lock() 两次不会死锁
mutex m;
m.try_lock(); // 尝试加锁成功
m.try_lock(); // 尝试加锁失败
...
m.unlock();
4. 加锁方式
1. mutex
{
mutex m;
m.lock();
...
m.unlock();
}
2. lock_guard<>
{
mutex m;
lock_guard<mutex> l(m);
...
}
lock_guard 源码
template <class mutex>
class lock_guard
{
public:
lock_guard(mutex& mtx) : m(mtx)
{
m.lock();
}
lock_guard(mutex& mtx, adopt_lock_t) : m(mtx) {}
~lock_guard()
{
m.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
mutex& m;
};
3. unique_lock<>
std::mutex m;
std::unique_lock<std::mutex> l(m); // lock
std::unique_lock<std::mutex> l(m, std::adopt_lock); // 假定已 lock
std::unique_lock<std::mutex> l(m, std::defer_lock); // 不 lock
std::unique_lock<std::mutex> l(m, std::try_to_lock);// try_lock
// unique_lock 可手动加锁解锁
l.lock();
l.unlock();
unique_lock<> 支持移动操作
mutex m;
unique_lock<mutex> u1(m);
unique_lock<mutex> u2 = move(u1);
unique_lock<> 源码
class unique_lock
{
public:
unique_lock(): pmtx(nullptr), own(false) {}
unique_lock(_Mutex& mtx): pmtx(std::addressof(mtx)), own(false)
{
pmtx->lock();
own = true;
}
unique_lock(_Mutex& mtx, adopt_lock_t): pmtx(std::addressof(mtx)), own(true) {}
unique_lock(_Mutex& mtx, defer_lock_t): pmtx(std::addressof(mtx)), own(false) {}
unique_lock(_Mutex& mtx, try_to_lock_t): pmtx(std::addressof(mtx)), own(pmtx->try_lock()) {}
// 时间段和时间点
template <class _Rep, class _Period>
unique_lock(_Mutex& mtx, const chrono::duration<_Rep, _Period>& rel_time)
: pmtx(std::addressof(mtx)), own(pmtx->try_lock_for(rel_time)) {}
template <class _Clock, class _Duration>
unique_lock(_Mutex& mtx, const chrono::time_point<_Clock, _Duration>& abs_time)
: pmtx(std::addressof(mtx)), own(pmtx->try_lock_until(abs_time)) {}
// 移动操作
unique_lock(unique_lock&& other): pmtx(other.pmtx), own(other.own)
{
other.pmtx = nullptr;
other.own = false;
}
unique_lock& operator=(unique_lock&& other)
{
if (this != std::addressof(other))
{
if (own)
{
pmtx->unlock();
}
pmtx = other.pmtx;
own = other.own;
other.pmtx = nullptr;
other.own = false;
}
return *this;
}
~unique_lock()
{
if (own)
{
pmtx->unlock();
}
}
unique_lock(const unique_lock&) = delete;
unique_lock& operator=(const unique_lock&) = delete;
void lock()
{
check();
pmtx->lock();
own = true;
}
bool try_lock()
{
check();
own = pmtx->try_lock();
return own;
}
template <class _Rep, class _Period>
bool try_lock_for(const chrono::duration<_Rep, _Period>& rel_time)
{
check();
own = pmtx->try_lock_for(rel_time);
return own;
}
template <class _Clock, class _Duration>
bool try_lock_until(const chrono::time_point<_Clock, _Duration>& abs_time)
{
check();
own = pmtx->try_lock_until(abs_time);
return own;
}
void unlock()
{
if (!pmtx || !own)
{
_Throw_system_error(errc::operation_not_permitted);
}
pmtx->unlock();
own = false;
}
void swap(unique_lock& other)
{
std::swap(pmtx, other.pmtx);
std::swap(own, other.own);
}
_Mutex* release()
{
_Mutex* res = pmtx;
pmtx = nullptr;
own = false;
return res;
}
bool owns_lock() const
{
return own;
}
operator bool() const
{
return own;
}
_Mutex* mutex() const
{
return pmtx;
}
private:
_Mutex* pmtx; // 指向管理的 mutex
bool own; // 是否拥有互斥
void check() const
{ // 检测能否 lock
if (!pmtx)
{
_Throw_system_error(errc::operation_not_permitted);
}
if (own)
{
_Throw_system_error(errc::resource_deadlock_would_occur);
}
}
};
4. lock()
一次性锁住多个互斥, 要么全部加锁, 要么全部未加锁
- 不发生死锁
- 仍需自己解锁
void fun(T &l, T &r)
{
lock(l.m, r.m); // 一次性锁住 l 和 r 的互斥量
lock_guard<mutex> lock_l(l.m, adopt_lock);
lock_guard<mutex> lock_r(r.m, adopt_lock);
...
}
lock() 源码
template <class lock0, class lock1, class... lockN>
void lock(lock0& lk0, lock1& lk1, lockN&... lkN)
{
_Lock_nonmember1(lk0, lk1, lkN...);
}
5. scoped_lock<>
std::lock()
与 std::lock_guard<>
的结合版
一次性锁住所有互斥量, 并析构时解锁
void fun(T &l, T &r)
{
scoped_lock guard(l.m, r.m);
...
}
scoped_lock<> 源码
template <class... _Mutexes>
class scoped_lock
{
public:
scoped_lock(_Mutexes&... mtxes) : my_mtxes(mtxes...)
{
lock(mtxes...);
}
scoped_lock(adopt_lock_t, _Mutexes&... mtxes) : my_mtxes(mtxes...) {}
~scoped_lock()
{
apply([](_Mutexes&... mtxes) { (..., (void) mtxes.unlock()); }, my_mtxes);
}
scoped_lock(const scoped_lock&) = delete;
scoped_lock& operator=(const scoped_lock&) = delete;
private:
tuple<_Mutexes&...> my_mtxes;
};
5. 死锁
死锁的四个必要条件
- (资源)互斥占有
- (资源)不可剥夺
- (线程)请求并保持
- (线程)循环等待
线程 A 需要 a 和 b, 线程 B 需要 a 和 b
- 互斥占有: a 和 b 只能被一个占有
- 不可剥夺: a 和 b 被占有后不能被剥夺
- 请求并保持: 线程 A 占有 a 请求 b, 线程 B 占有 b 请求 a
- 循环等待: 线程 A 等待 b, 线程 B 等待 a
死锁的原因
-
推进顺序不当
推进 A 获取 a 后, 推进 B 获取 b, 造成死锁
-
资源分配不当
推进 A 获取 a 后, 不应该给 B 分配 b
死锁实例
互相等待
1. lock
互相阻塞
把互斥看作资源
-
A 需要互斥 a 和 b, B 需要互斥 a 和 b
-
A 拥有 a, B 拥有 b
-
1 等待 2, 2 等待 1
A:
{
a.lock();
b.lock(); 1
...
a.unlock();
b.unlock();
}
B:
{
b.lock();
a.lock(); 2
...
b.unlock();
a.unlock();
}
2. lock 两次
自己阻塞自己
1 等待 2, 2 等待 1
A:
{
m.lock();
m.lock(); 1
...
m.unlock(); 2
m.unlock();
}
3. join
把 join 看作资源
1 等待 2, 2 等待 1
A:
{
B.join(); 1
}
B:
{
A.join(); 2
}
4. join 自己
1 等待 2, 2 等待 1
A:
{
A.join(); 1
} 2
防范死锁
- 避免嵌套锁: 若已持有锁, 就不要获取第二个锁
- 避免调用用户接口: 用户可能试图获取第二个锁
- 按顺序加锁
- 按层级加锁
6. 其他方式保护共享数据
初始化
只初始化一次, 且只有在第一次使用时进行初始化
普通做法
缺点: 初始化部分强制所有线程串行执行
A:
flag 表示是否初始化
{
m.lock();
if(!flag)
flag = true;
m.unlock();
}
双重检验锁
A:
{
if(!flag)
{
m.lock();
if(!flag)
flag = true;
m.unlock();
}
...
}
std::call_once()
搭配 std::once_flag
类对象进行延迟初始化, 一个 std::once_flag
类对象 对应一个初始化
std::once_flag flag;
void init();
A:
{
std::call_once(flag, init); // 只执行一次 init
...
}
排他写和共享读
排他锁: lock_guard<shared_mutex>
共享锁: shared_lock<shared_mutex>
- 排他锁与排他锁和共享锁互斥
- 共享锁与共享锁不互斥, 与排他锁互斥
void read(int x) const
{
std::shared_lock<std::shared_mutex> l(m); // 共享读
...
}
void write(int x)
{
std::lock_guard<std::shared_mutex> l(m); // 排他写
...
}
mutex 类型
-
mutex 普通锁
-
recursive_mutex 递归锁
-
time_mutex 延迟锁
-
recursive_timed_mutex 递归延迟锁
总结
-
不变量与条件竞争
-
mutex 的类型
-
加锁的方式
- mutex
- lock_guard
- unique_lock
- lock
- scoped_lock
-
死锁