C++ 并发编程实战: 第3章 线程共享数据

149 阅读9分钟

第3章 线程共享数据

[TOC]

本章内容

  • 线程间访问共享数据的问题
  • 利用互斥保护共享数据
  • 其他方法保护共享数据

1. 线程间共享数据的问题

不变量

对数据的断言

类似于循环不变式, 数列通项

问题

代码段 A共享数据 D不变量 V假设处理时

  • 若不变量 V 满足, 就正确处理
  • 若不变量 V 并未满足, 就出问题
// A:
{
    process(D);		// 以不变量 V 满足为前提处理
}

例:

不变量: 双向链表中, 若 ABA \rightarrow B, 则 BAB \rightarrow A

操作: 删除双向链表的一个结点

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()

线程调用

  1. 若 mutex 为 0, 则置为 1
  2. 若 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
  • 死锁