C++并发编程 互斥和同步

C++并发编程 异步任务(async)

线程基本的互斥和同步工具类, 主要包括:
  std::mutex 类
  std::recursive_mutex 类
  std::timed_mutex 类
  std::recursive_timed_mutex 类
  std::lock_guard 类型模板
  std::unique_lock 类型模板
  std::lock 函数模板
  std::once_flag 类
  std::call_once 函数模板

 

std::mutex 类

  std::mutex 上锁须要调用 lock() 或 try_lock(), 当有一个线程获取了锁, 其它线程想要取得此对象的锁时, 会被阻塞(lock)或失败(try_lock). 当线程完成共享数据的保护后, 需要调用 unlock 进行释放锁.
  std::mutex 不支嵌套, 如果两次调用 lock, 会产生未定义行为.

std::recursive_mutex 类

使用方法同 std::mutex, 但 std::recursive_mutex 支持一个线程获取同一个互斥量多次,而没有对其进行一次释放. 但是同一个线程内, lock 与 unlock 次数要相等, 否则其它线程将不能取得任何机会.
其原理是, 调用 lock 时, 当调用线程已持有锁时, 计数加1; 调用 try_lock 时, 尝试取得锁, 失败时不会阻塞, 成功时计数加1; 调用 unlock 时, 计数减1, 如果是最后一个锁时, 释放锁.
需要注意的是: 调用 try_lock时, 如果当前线程未取得锁, 即使没有别的线程取得锁, 也有可能失败.

std::timed_mutex 类

std::timed_mutex 在 std::mutex 的基础上支持让锁超时. 上锁时可以调用 try_lock_for, try_lock_until 设置超时值.
try_lock_for 的参数是需要等待的时间, 当参数小于等于0时会立即返回, 效果和使用 try_lock 一样.
try_lock_until 传入的参数不能小于当前时间, 否则会立即返回, 效果和使用 try_lock 一样. 实际上 try_lock_for 内部也是调用 try_lock_until 实现的.
tm.try_lock_for(std::chrono::milliseconds(1000)) 与 tm.try_lock_until(std::chrono::steady_clock::now() + std::chrono::milliseconds(1000)) 等价, 都是等待1s.

std::recursive_timed_mutex 类

std::recursive_timed_mutex 在 std::recursive_mutex 的基础上, 让锁支持超时.
用法同 std::timed_mutex, 超时原理同 std::recursive_mutex.

std::lock_guard 类型模板

std::lock_guard 类型模板为基础锁包装所有权. 指定的互斥量在构造函数中上锁, 在析构函数中解锁.
这就为互斥量锁部分代码提供了一个简单的方式: 当程序运行完成时, 阻塞解除, 互斥量解锁(无论是执行到最后, 还是通过控制流语句break或return, 亦或是抛出异常).
std::lock_guard 不支持拷贝构造, 拷贝赋值和移动构造.

std::unique_lock 类型模板

std::unique_lock 类型模板比 std::loc_guard 提供了更通用的所有权包装器.
std::unique_lock 可以调用 unlock 释放锁, 而后当再次需要对共享数据进行访问时再调用 lock(), 但是必须注意一次 lock 对应一次 unlock, 不能连续多次调用同一个 lock 或 unlock.
std::unique_lock 不支持拷贝构造和拷贝赋值, 但是支持移动构造和移动赋值.
std::unique_lock 比 std::loc_guard 还增加了另外几种构造方式:
unique_lock(_Mutex& _Mtx, adopt_lock_t) 构建持有锁实例, 其不会调用 lock 或 try_lock, 但析构时默认会调用 unlock.
unique_lock(_Mutex& _Mtx, defer_lock_t) 构建非持有锁实例, 其不会调用 lock 或 try_lock, 如果没有使用 std::lock 等函数修改标志, 析构时也不会调用 unlock.
unique_lock(_Mutex& _Mtx, try_to_lock_t) 尝试从互斥量上获取锁, 通过调用 try_lock
unique_lock(_Mutex& _Mtx, const chrono::duration<_Rep, _Period>& _Rel_time) 在给定时间长度内尝试获取锁
unique_lock(_Mutex& _Mtx, const chrono::time_point<_Clock, _Duration>& _Abs_time) 在给定时间点内尝试获取锁
bool owns_lock() const 检查是否拥有一个互斥量上的锁

std::lock 函数模板

std::lock 函数模板提供同时锁住多个互斥量的功能, 且不会有因改变锁的一致性而导致的死锁. 其声明如下:
template<typename LockableType1,typename... LockableType2> void lock(LockableType1& m1,LockableType2& m2...);

    // 使用互斥量保护代码
    typedef std::lock_guard<std::mutex>         MutexLockGuard;
    typedef std::unique_lock<std::mutex>        UniqueLockGuard;

    class Func
    {
        int i;
        std::mutex& m;
    public:
        Func(int i_, std::mutex& m_) : i(i_), m(m_) {}
        void operator() ()
        {
            //MutexLockGuard lk(m);
            UniqueLockGuard lk(m);
            for (unsigned j = 0; j < 10; ++j)
            {
                std::cout << i << " ";
            }
            std::cout << std::endl;
        }
    };

    std::mutex m;
    std::vector<std::thread> threads;

    for (int i = 1; i < 10; i++)
    {
        Func f(i, m);
        threads.push_back(std::thread(f));
    }
    std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join)); // 对每个线程调用join()   
    // 同时对多个 mutex 上锁
    std::mutex m1;
    std::mutex m2;
    //std::unique_lock<std::mutex> lock_a(m1, std::defer_lock);
    //std::unique_lock<std::mutex> lock_b(m2, std::defer_lock); // std::def_lock 留下未上锁的互斥量
    //std::lock(lock_a, lock_b); // 互斥量在这里上锁, 并修改对象的上锁标志
    std::lock(m1, m2); // 锁住两个互斥量
    std::lock_guard<std::mutex> lock_a(m1, std::adopt_lock); // std::adopt_lock 参数表示对象已经上锁,因此不会调用 lock 函数
    std::lock_guard<std::mutex> lock_b(m2, std::adopt_lock); 

std::call_once 函数模板

如果多个线程需要同时调用某个函数,std::call_once 可以保证多个线程对该函数只调用一次, 并且是线程安全的.

线程安全的延迟初始化

-- 使用 std::call_once 和 std::once_flag 

考虑下面的代码, 每个线程必须等待互斥量,以便确定数据源已经初始化, 这导致了线程资源产生不必要的序列化问题.

        std::shared_ptr<some_resource> resource_ptr;
        std::mutex resource_mutex;
        void foo()
        {
            std::unique_lock<std::mutex> lk(resource_mutex); // 所有线程在此序列化
            if (!resource_ptr)
            {
                resource_ptr.reset(new some_resource); // 只有初始化过程需要保护
            }
            lk.unlock();
            resource_ptr->do_something();
        }

使用双重检查锁优化上述代码, 指针第一次读取数据不需要获取锁, 并且只有在指针为NULL时才需要获取锁; 然后, 当获取锁之后, 指针会被再次检查一遍(这就是双重检查的部分), 避免另一的线程在第一次检查后再做初始化, 并且让当前线程获取锁.
这样同样存在问题, 即潜在的条件竞争, 因为外部的读取锁①时没有与内部的写入锁进行同步③, 因此就会产生条件竞争,这个条件竞争不仅覆盖指针本身, 还会影响到其指向的对象:
即使一个线程知道另一个线程完成对指针进行写入, 它可能没有看到新创建的some_resource实例, 然后调用do_something()④后, 得到不正确的结果. 这在C++标准中被指定为“未定义行为”.

        void undefined_behaviour_with_double_checked_locking()
        {
            if (!resource_ptr) // 1
            {
                std::lock_guard<std::mutex> lk(resource_mutex);
                if (!resource_ptr) // 2
                {
                    resource_ptr.reset(new some_resource); // 3
                }
            }
            resource_ptr->do_something(); // 4
        }

  C++的解决方法:

        std::shared_ptr<some_resource> resource_ptr;
        std::once_flag resource_flag; // 1
        
        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) // 1
            {
                std::call_once(connection_init_flag, &X::open_connection, this); // 2
                connection.send_data(data);
            }
            data_packet receive_data() // 3
            {
                std::call_once(connection_init_flag, &X::open_connection, this); // 2
                return connection.receive_data();
            }
        };

 

boost::shared_lock

读者-写者锁 boost::shared_lock, 允许两中不同的使用方式:一个“作者”线程独占访问和共享访问, 让多个“读者”线程并发访问. (C++11标准不支持)
其性能依赖与参与其中的处理器数量, 也与读者和写者线程的负载有关. 一种典型的应用:

    #include <map>
    #include <string>
    #include <mutex>
    #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); // 1
            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); // 2
            entries[domain] = dns_details;
        }
    };
    

 

 

 

posted @ 2016-10-07 23:13  阿Hai  阅读(2148)  评论(0编辑  收藏  举报