《C++ Concurrency in Action》读书笔记二 线程之间共享资源
发布日期:2021-05-07 23:34:52 浏览次数:19 分类:原创文章

本文共 9107 字,大约阅读时间需要 30 分钟。

线程之间共享资源对于线程的安全性很重要,不正确的共享资源会导致多线程程序产生许多不可意料的bug


CP3


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


读取数据的情况不会发生问题。如果多个线程同时需要对数据进行修改不制定相应的规则会发生奇怪的后果。书中举例了双向链表的读写删除操作时候多线程可能引发的问题。最基本的std::list 就是一个双向链表


1)并发冲突,资源竞争(Race condition)


benign race condition(良性的资源竞争) 不用太care


problematic race condition (有问题的资源竞争)重点 通常发生于一个操作需要分多次修改数据,而当操作只完成某一步时别的线程需要对数据进行访问。这就会产生不良的后果


2)避免有问题的资源竞争(problematic race condition)


a.对数据进行适当的保护 (mutex 互斥量)


b.修改数据结构 lock-free 编程


c.使用类似数据库的transaction机制 


  软件事务内存(software transactional memory STM)




2. 使用mutex保护共享数据


在访问共享资源之前先lock mutex,访问结束以后unlock mutex。在这个过程中所有尝试lock mutex的线程会等待mutex被unlock以后才执行对他的lock


1)在C++中使用mutex



#include <iostream>#include <thread>#include <tr1/memory>#include <list>#include <mutex>#include <algorithm>class mutex_sample{public:    void add_to_list(int new_value);    bool list_contains(int value_to_find);private:    std::list<int> some_list;    std::mutex some_mutex;};void mutex_sample::add_to_list(int new_value){    std::lock_guard<std::mutex> guard(some_mutex);    some_list.push_back(new_value);    std::cout<<new_value<<std::endl;}bool mutex_sample::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();}int main(){    mutex_sample mtx;    std::vector<std::thread> threads;    for(int i=0; i<5; ++i)    {        threads.push_back(std::thread([=,&mtx]{                                      mtx.add_to_list(i);                                      }));    }    std::for_each(threads.begin(), threads.end(),std::mem_fn(&std::thread::join));    if(mtx.list_contains(3))        std::cout<<"Find the value"<<std::endl;    //do something else.    return 0;}
一个简单的基于面向对象的使用mutex保护共享资源的例子,但是该例子有隐患。任何试图将保护资源通过引用或者指针放回给外界都会绕开mutex的保护从而造成资源竞争。


2)加强保护共享资源的代码


一条规则,不将需要保护的数据以指针或者引用的方式传递给lock锁保护的范围


3)在接口上确定资源竞争


比如std::stack由于不同线程调用pop的时机不同会导致资源竞争


解决方案


a. 在pop的时候使用引用


b. 使用no-throw的copy构造函数或者move构造函数


c. 将pop对象用指针返回


d. 使用a条款加上b或c


一个线程安全的stack的例子



#include <exception>#include <memory>#include <iostream>#include <mutex>struct empty_stack: std::exception{    const char* what() const throw();};template<typename T>class threadsafe_stack{private:    std::stack<T> data;    mutable std::mutex m;public:    threadsafe_stack(){}    threadsafe_stack(const threadsafe_stack& other)    {        std::lock_guard<std::mutex> lock(other.m);        data = other.data;    }    threadsafe_stack& operator=(const threadsafe_stack&)=delete;    void push(T new_value)    {        std::lock_guard<std::mutex> lock(m);        data.push(new_value);    }    std::shared_ptr<T> pop()    {        std::lock_guard<std::mutex> lock(m);        if(data.empty()) throw empty_stack();        std::shared_ptr<T> const res(std::make_shared<T>(data.top()));        data.pop();        return res;    }    void pop(T& value)    {        std::lock_guard<std::mutex> lock(m);        if(data.empty()) throw empty_stack();        value = data.top();        data.pop();    }    bool empty() conost    {        std::lock_guard<std::mutex> lock(m);        return data.empty();    }};






4. 死锁和解决方法


多线程程序中需要锁定多个mutex的时候,当前线程锁定了某些mutex等待另外一些线程解锁mutex,而另外一些线程也在等待当前线程解锁mutex于是变成了无线等待。通常发生在需要锁定多个mutex的时候。


解决方法是,在锁定mutex的时候永远按照固定顺序来执行,或者同时锁定多个mutex


在一个交换操作中使用std::lock() 和std::lock_guard()



std::lock(m1, m2); //同时锁定互斥量m1, m2std::lock_guard<mutex> lock_a(m1, std::adopt_lock); //将已经锁定的m1转移给lock_a对象管理,方便退出作用域时候由资源管理器自动解锁mutexstd::lock_guard<mutex> lock_b(m2, std::adopt_lock); //同上



5. 一些避免死锁的建议


死锁不仅仅发生在锁定mutex上,比如两个线程中的一个中调用了join来等待另外一个线程正常结束,也会导致死锁。



a. 避免使用多重锁 


    一个线程只有一个锁的情况下不会发生死锁。如果一定要使用多重锁用std::lock()同时锁定mutex


b. 在锁定一个mutex的时候避免使用用户定义的代码


    比如你已经锁定了一个mutex 可是在你的代码中调用的其代码仍然也要lock同一个mutex就会造成死锁。


一个死锁的例子



#include <exception>#include <memory>#include <iostream>#include <mutex>#include <thread>class dead_lock{private:    std::mutex m;public:    void do_something();    void do_something_else();};void dead_lock::do_something(){    std::lock_guard<std::mutex> lock(m);    std::cout<<"do something!"<<std::endl;    do_something_else();}void dead_lock::do_something_else(){    std::lock_guard<std::mutex> lock(m);    std::cout<<"do something else"<<std::endl;}int main(){    dead_lock d;    std::thread t(&dead_lock::do_something, &d); // use the member function as the thread function.    t.join();    return 0;}


c. 在锁定mutex时候使用固定的顺序


如果需要锁定多个资源,但是又不能使用std::lock同时锁定他们的时候,一定要按照固定的顺序来锁定他们。


d.  使用分层锁


强制lock的顺序,否则会报告excption 实现代码如下



#include <exception>#include <memory>#include <iostream>#include <mutex>#include <thread>class hierarchical_mutex //分层锁{private:    std::mutex internal_mutex;    unsigned long const hierarchy_value;    unsigned long previous_hierarchy_value;    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()    {        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 hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX); //initial the static member variable which is shared by thread itself;



6. 使用std::unique_lock更加弹性的控制锁


std::unique_lock<std::mutex> lock_a(m1, std::deter_lock);std::unique_lock<std::mutex> lock_b(m2, std::deter_lock);std::lock(lock_a, lock_b);



7. 在块之间传递mutex的所有权


函数返回std::unique_lock<std::mutex> 时候可以不使用std::move()


std::unique_lock 可以move不可拷贝



std::unique_lock<std::mutex> get_lock(){    extern std::mutex some_mutex;    std::unique_lock<std::mutex> lk(some_mutex);    prepare_data();    return lk;}void process_data(){    std::unique_lock<std::mutex> lk(get_lock());    do_something();}






8. 根据适当的粒度来使用锁


使用


std::unique_ptr<std::mutex> lk(some_mutex);


...


lk.unlock();


...


lk.lock();


...


锁必须尽可能的最小时间来锁定需要被处理的共享资源。



class Y{private:    int some_detail;    mutable std::mutex m;    int get_detail() const    {        std::lock_guard<std::mutex> lock_a(m);        return some_detail;    }public:    Y(int sd):some_detail(sd){}    friend bool operator==(const Y& lhs, const Y& rhs)    {        if(&lhs == &rhs)            return true;        int const lhs_value = lhs.get_detail();        int const rhs_value = rhs.get_detail();        return lhs_value == rhs_value;    }};


如果你不能在整个操作过程中使用锁,你就会进入资源竞争的情况。




3 保护共享资源的非主流方法


1)在初始化的时候保护共享资源



std::shared_prt<some_resource> resource_ptr;std::mutex resource_mutex;void foo(){        std::unique_lock<some_resource> lk(resource_mutex);    if(!resource_ptr)    {        resource_ptr.reset(new some_resource); //only initialization needs protection.    }    lk.unlock();    resource_ptr->do_something();}
可以使用std::once_flag 或者std::call_once 来只在资源初始化的时候执行一次



std::shared_ptr<some_resource> resource_ptr;std::once_flag resource_flag;void init_resource(){    resource_ptr.reset(new some_resource);    }void foo(){    std::call_once(resource_flag, init_resource);    resource_ptr->do_something();}


一个线程安全的初始化共享资源的例子



class X{private:    connection_info connection_details;    connection_handle connection;    std::once_flag connection_init_flag;    void open_connection()    {        connection = connection_manager.open(connection_details);    }public:    X(connection_info  const& connection_details_):        connection_details(connection_details_)    {        }    void send_data(data_packet const& data)    {        std::call_once(connection_init_flag, &X::open_connection, this);        connection.send_data(data);    }    data_packet receive_data()    {        std::call_once(connection_init_flag, &X::open_connection, this);        return connection.receive_data();    }};
2)保护较少次数更新的共享资源


#include <map>#include <memory>#include <iostream>#include <mutex>#include <thread>#include <boost/thread/shared_mutex.hpp>class dns_entry;class dns_cache{    std::map<std::string, dns_entry> entries;    mutable boost::shared_mutex entry_mutex;public:    dns_entry find_entry(std::string const& domain) const    {        boost::shared_lock<boost::shared_mutex> lk(entry_mutex);        std::map<std::string, dns_entry>::const_iterator const it =            entries.find(domain);        return (it == entries.end())? dns_entry():it->second;    }    void update_or_add_entry(std::string const& domain, dns_entry const& dns_details)    {        std::lock_guard<boost::shared_mutex> lk(entry_mutex);        entries[domain] = dns_details;    }};
3) 递归锁


std::recursive_mutex 递归互斥量,支持多次锁,但是同时也要解锁多次。


std::lock_guard<std::recursive_mutex> 


std::unique_lock<std::recursive_mutex>


上一篇:《C++ Concurrency in Action》读书笔记三 同步并发操作
下一篇:《C++ Concurrency in Action》读书笔记一 多线程与线程管理

发表评论

最新留言

表示我来过!
[***.240.166.169]2025年04月04日 15时51分50秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章