【C++11多线程】线程锁保护共享数据

1 互斥锁

Race Conditions/条件竞争是当两个或多个线程读写某些共享数据,而最后的结果取决于进程运行的精确时序。
想要成功的解决race condition问题,使用互斥锁来保护多线程环境中的共享数据并避免竞争情况。互斥锁又会产生死锁问题。

避免死锁的建议:

  • 一是一个线程已经获取一个锁时就不要获取第二个。如果每个线程只有一个锁,锁上就不会产生死锁(但除了互斥锁,其他方面也可能造成死锁,比如即使无锁,线程间相互等待也可能造成死锁)
  • 二是持有锁时避免调用用户提供的代码。用户提供的代码可能做任何时,包括获取锁,如果持有锁时调用用户代码获取锁,就会违反第一个建议,并造成死锁。但有时调用用户代码是无法避免的
  • 三是按固定顺序获取锁。如果必须获取多个锁且不能用std::lock同时获取,最好在每个线程上用固定顺序获取。上面的例子虽然是按固定顺序获取锁,但如果不同时加锁就会出现死锁,对于这种情况的建议是额外规定固定的调用顺序
  • 四是使用层次锁,如果一个锁被低层持有,就不允许再上锁。

2 luck_guard

try_lock、defer_lock、try_lock_for
try_lock()尝试给互斥量加锁,如果拿不到锁,返回false,如果拿到了锁,返回true,这个函数是不阻塞的。
用std::defer_lock的前提是,你不能自己先lock,否则会报异常

std::defer_lock的意思就是并没有给mutex加锁:初始化了一个没有加锁的mutex。

为了仍旧能够使用class std::lock_guard,需要额外加一个参数std::adopt_lock
    std::mutex m;
    while (m.try_lock() == false) {
        doSomeOtherStuff();
    }
    std::lock_guard<std::mutex> lg(m,std::adopt_lock);
//实例:
    std::timed_mutex m;
    if (m.try_lock_for(std::chrono::seconds(1))) { 
        std::lock_guard<std::timed_mutex> lg(m,std::adopt_lock); 
        ...
    }
    else {
        couldNotGetTheLock();
    }
    注意:如果改变系统时间,时间段和时间点的行为有所不同

luck_guard加adopt参数(提供死锁检查)

std::mutex m1; std::mutex m2; ...
    {
        std::lock (m1, m2);
        std::lock_guard<std::mutex> lockM1(m1,std::adopt_lock); 
        std::lock_guard<std::mutex> lockM2(m2,std::adopt_lock); 
        ...
    }

1.全局函数lock(),会锁住它收到的所有锁(直到全部锁住,或者抛出异常),如果抛出异常,已经上锁的锁,也会全部解锁
2.锁上后,应该使用class std::lock_guard,并带上一个额外的参数std::adopt_lock,表示这个互斥量已经被lock了(你必须要把互斥量提前lock了 ,否者会报异常);
std::adopt_lock标记的效果就是假设调用一方已经拥有了互斥量的所有权(已经lock成功了);通知lock_guard不需要再构造函数中lock这个互斥量了。用std::adopt_lock的前提是,自己需要先把mutex lock上。
注意:lock()提供了死锁的回避机制,也就代表,锁住的顺序不明确

3 unique_lock

调用成员函数owns_lock(),可以检查当前unique_lock东西内的锁,是否锁住(同样,如果锁住unique_lock的析构函数会自动释放锁,否则析构函数什么也不做)

1.构造函数中传递try_to_lock,表示企图锁定,但不希望阻塞
    std::unique_lock<std::mutex> lock(mutex,std::try_to_lock); ...
    if (lock) { 
        ...
    }
2.可以传递一个时间段或时间点给构造函数(尝试在一个时间周期内锁定)
    std::unique_lock<std::timed_mutex> lock(mutex,
    std::chrono::seconds(1));
    ...
3.可以传递一个defer_lock,表示初始化这一个锁,但是并未打算锁住
    std::unique_lock<std::mutex> lock(mutex,std::defer_lock); 
    ...
    lock.lock(); // or (timed) try_lock()
    (可以用来建立一个或多个锁,并且在稍后再锁住他们)
    (提供release成员函数来释放锁)
1.lock_guard保证锁在离开作用域的时候,会自动释放
    mutex可以由lock_guard的构造函数申请,也可以绑定已经锁上的mutex
    (lock_guard生命周期内,锁总是保持锁定状态)
    lock_guard的操作函数见:表18.9 class lock_guard的操作函数 P1000

2.unique_lock和lock_guard类似,区别是unique_lock的生命周期内,并不是始终保持锁定状态
    (你可以明确的控制,unique_lock所控制的锁,是占有锁定状态,还是解锁状态)
    如果在调用析构函数的时候,是锁定状态,那析构函数会自动解锁,否则,析构函数什么也不干
    lock()可能抛出system_error异常,夹带的差错码和mutex相同
    unlock()可能抛出system_error异常,并夹带差错码operation_not_permitted(这个锁并未被锁定)

4 std::lock()同时上锁

避免死锁通常建议让两个mutex以相同顺序上锁,总是先锁A再锁B,但这并不适用所有情况
std::lock解决了此问题,它可以一次性锁住多个mutex,并且没有死锁风险
C++17支持的同时上锁std::scoped_lock

class A {
public:
    explicit A(int x) : i(x) {}
    int i;
    std::mutex m;
};

void f(A& from, A& to, int n)
{
    std::lock(from.m, to.m);//同时加锁
    //std::scoped_lock l(from.m, to.m);
    // 下面按固定顺序加锁,看似不会有死锁的问题
    // 但如果没有std::lock同时上锁
    // 另一线程中执行f(to, from, n)
    // 两个锁的顺序就反了过来,从而可能导致死锁
    std::lock_guard<std::mutex> lock1(from.m, std::adopt_lock); // std::adopt_lock表示获取m的所有权
    std::lock_guard<std::mutex> lock2(to.m, std::adopt_lock);
    from.i -= n;
    to.i += n;
}

int main()
{
    A x(70);
    A y(30);

    std::thread t1(f, std::ref(x), std::ref(y), 5);
    std::thread t2(f, std::ref(y), std::ref(x), 10);

    t1.join();
    t2.join();
}

也可以使用std::unique_lock,它比std::lock_guard灵活,可以传入std::adopt_lock管理mutex,也可以传入std::defer_lock表示mutex应保持解锁状态,以使mutex能被std::unique_lock::lock获取,或可以把std::unique_lock传给std::lock

std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
// std::defer_lock表示不获取m的所有权,因此m还未上锁
std::lock(lock1, lock2); // 此处上锁

std::unique_lock比std::lock_guard占用的空间多,会稍慢一点,如果不需要更灵活的锁,依然可以使用std::lock_guard

5 转移锁的所有权

std::unique_lock<std::mutex> getLock()
{
    extern std::mutex m;
    std::unique_lock<std::mutex> l(m);
    prepareData();
    return l; // 不需要std::move(编译器负责调用移动构造函数)
}

void f()
{
    std::unique_lock<std::mutex> l(getLock());
    doSomething();
}

6 费时操作先解锁

对一些费时的操作(如文件IO)上锁可能造成很多操作被阻塞,可以在面对这些操作时先解锁

void f()
{
    std::unique_lock<std::mutex> l(m);
    auto data = getData();
    l.unlock(); // 费时操作没有必要持有锁,先解锁
    auto res = process(data);
    l.lock(); // 为了写入数据再次上锁
    writeResult(data, res);
}

7 层次锁

class hierarchical_mutex {
    std::mutex internal_mutex;
    const unsigned long hierarchy_value; // 当前层级值
    unsigned long previous_hierarchy_value; // 前一线程的层级值
    // 所在线程的层级值,thread_local表示值存在于线程存储期
    static thread_local unsigned long this_thread_hierarchy_value;
    void check_for_hierarchy_violation() // 检查是否违反层级结构
    {
        if (this_thread_hierarchy_value <= hierarchy_value)
        {
            throw std::logic_error("mutex hierarchy violated");
        }
    }
    void update_hierarchy_value()
    {
        // 先存储当前线程的层级值(用于解锁时恢复)
        previous_hierarchy_value = this_thread_hierarchy_value;
        // 再把其设为锁的层级值
        this_thread_hierarchy_value = hierarchy_value;
    }
public:
    explicit hierarchical_mutex(unsigned long value) :
        hierarchy_value(value), previous_hierarchy_value(0)
    {}
    void lock()
    {
        check_for_hierarchy_violation(); // 要求线程层级值大于锁的层级值
        internal_mutex.lock(); // 内部锁被锁住
        update_hierarchy_value(); // 更新层级值
    }
    void unlock()
    {
        if (this_thread_hierarchy_value != hierarchy_value)
        {
            throw std::logic_error("mutex hierarchy violated");
        }
        // 恢复前一线程的层级值
        this_thread_hierarchy_value = previous_hierarchy_value;
        internal_mutex.unlock();
    }
    bool try_lock()
    {
        check_for_hierarchy_violation();
        if (!internal_mutex.try_lock()) return false;
        update_hierarchy_value();
        return true;
    }
};

thread_local unsigned long // 初始化为ULONG_MAX以使构造锁时能通过检查
hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX);

简化一下

class A {
    std::mutex m; // 内部锁
    int val; // 当前层级值
    int pre; // 用于保存前一线程层级值
    static thread_local int tVal; // tVal存活于一个线程周期
public:
    explicit A(int x) : val(x), pre(0) {}
    void lock()
    {
        if (tVal > val)
        { // 存储线程层级值tVal到pre后将其更新为锁的层级值val
            m.lock();
            pre = tVal;
            tVal = val;
        }
        else
        {
            throw std::logic_error("mutex hierarchy violated");
        }
    }
    void unlock()
    { // 恢复线程层级值为pre
        if (tVal != val)
        {
            throw std::logic_error("mutex hierarchy violated");
        }
        tVal = pre;
        m.unlock();
    }
    bool try_lock()
    {
        if (tVal > val)
        {
            if (!m.try_lock()) return false;
            pre = tVal;
            tVal = val;
            return true;
        }
        else
        {
            throw std::logic_error("mutex hierarchy violated");
        }
    }
};

thread_local int A::tVal(INT_MAX); // 保证初始构造std::scoped_lock正常

实例

hierarchical_mutex high(10000);
hierarchical_mutex mid(6000);
hierarchical_mutex low(5000); // 构造一个层级锁
// 初始化时锁的层级值hierarchy_value为5000
// previous_hierarchy_value为0

void lf()
{
    std::scoped_lock l(low);
    // 用层级锁构造std::scoped_lock时会调用low.lock
    // lock先检查,this_thread_hierarchy_value初始化为ULONG_MAX
    // 因此this_thread_hierarchy_value大于hierarchy_value
    // 通过检查,内部锁上锁
    // 更新值,把previous_hierarchy_value更新为线程层级值ULONG_MAX
    // 把this_thread_hierarchy_value更新为low的层级值5000
} // 调用low.unlock,检查this_thread_hierarchy_value,值为5000
// 与hierarchy_value相等,通过检查
// 接着把this_thread_hierarchy_value恢复为pre保存的ULONG_MAX
// 最后解锁

void hf()
{
    std::scoped_lock l(high);
    // this_thread_hierarchy_value更新为high的层级值10000
    lf(); // 调用lf时,lf里的this_thread_hierarchy_value值为10000
    // 过程只是把lf中的注释里this_thread_hierarchy_value初始值改为10000
    // 同样能通过检查,其余过程一致,最后解锁lf会恢复到这里的线程层级值10000
}

void mf()
{
    std::scoped_lock l(mid);
    // this_thread_hierarchy_value更新为mid的层级值6000
    hf(); // 调用hf时,hf里的this_thread_hierarchy_value值为6000
    // 构造hf里的l时,调用high.lock
    // 检查this_thread_hierarchy_value,小于high.hierarchy_value
    // 于是throw std::logic_error("mutex hierarchy violated")
}

8 call_once保护共享数据的初始化

一个极端但常见的情况是,共享数据在并发访问和初始化都需要保护,但之后要隐式同步。数据初始化后上锁只是为了保护初始化过程,但这会不必要地影响性能

std::shared_ptr<A> P;
void f()
{
	if (!p)
	{
		// 1. 为A对象分配一片内存
	    // 2. 在分配的内存上调用A的构造函数,构造一个A对象
	    // 3. 返回该内存的指针,让p指向该内存
	    // 编译器不一定按23顺序执行,可能32
		p.reset(new A); // 在多线程中这里需要保护,直接上锁会导致不必要的线程资源阻塞
	}
	p->doSomething();
}

为了处理race condition,C++标准库提供了std::once_flag和std::call_once
每个线程只要使用std::call_once,在std::call_once结束时就能安全地知道指针已被其他线程初始化,而且这比使用mutex的开销更小

#include <iostream>
#include <thread>
#include <mutex>

std::once_flag flag;

void f()
{
    std::call_once(flag, [] { std::cout << 1; });
    std::cout << 2;
}

int main()
{
    std::thread t1(f);
    std::thread t2(f);
    std::thread t3(f);
    t1.join();
    t2.join();
    t3.join();
}

// output
1222

std::call_once也可以用在类中

class A {
private:
    std::once_flag flag;
    void init() { ... }
public:
    void f()
    {
        std::call_once(flag, &A::init, this);
        ...
    }
};

C++11规定static变量的初始化只完全发生在一个线程中,直到初始化完成前其他线程都不会做处理,从而避免了race condition。只有一个全局实例时可以不使用std::call_once而直接用static

class A {
public:
    static A& getInstance();
    A(const A&) = delete;
    A& operator(const A&) = delete;
private:
    A() = default;
    ~A() = default;
};

A& A::getInstance()
{
    static A instance; // 线程安全的初始化
    return instance;
}

9 读写锁

有些数据(比如缓存中存放的DNS入口表)需要经常访问但更新频率很低,如果用std::mutex保护数据有些过度(大量读的操作也会因锁而影响性能),这就需要用上读写锁(reader-writer mutex),它允许多个线程并发读但仅一个线程写
C++17提供了std::shared_mutex和std::shared_timed_mutex(C++14),后者比前者提供了更多操作,但前者性能更高。C++11没有提供读写锁,为此可使用boost::shared_mutex
C++14提供了std::shared_lock,用法和std::unique_lock相同,此外std::shared_lock还允许多线程同时获取共享锁,因此一般用std::shared_lock锁定读,std::unique_lock锁定写

class A {
private:
    mutable std::shared_mutex m;
    int n = 0;
public:
    int read()
    {
        std::shared_lock<std::shared_mutex> l(m);
        return n;
    }
    void write()
    {
        std::unique_lock<std::shared_mutex> l(m);
        ++n;
    }
};

10 递归锁std::recursive_mutex()

一个线程已经获取std::mutex(即已上锁)后再次上锁就会产生未定义行为。为了允许这种情况,C++提供了std::recursive_mutex,它可以在一个线程上多次获取锁,但在其他线程获取锁之前必须释放所有的锁

std::mutex m;

void f()
{
    m.lock();
    m.unlock();
}

void g()
{
    m.lock();
    f();
    m.unlock();
}

int main()
{
    std::thread t(g);
    t.join(); // 产生未定义行为
}

参考

1.https://downdemo.gitbook.io/cpp-concurrency-in-action-2ed
2.https://wangyi.blog.csdn.net/article/details/104428264
3.https://www.jianshu.com/p/2e82636885cc

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值