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;
        }
    };
    
时间: 2024-10-13 18:15:30

C++并发编程 互斥和同步的相关文章

Java并发编程之多线程同步

线程安全就是防止某个对象或者值在多个线程中被修改而导致的数据不一致问题,因此我们就需要通过同步机制保证在同一时刻只有一个线程能够访问到该对象或数据,修改数据完毕之后,再将最新数据同步到主存中,使得其他线程都能够得到这个最新数据.下面我们就来了解Java一些基本的同步机制. Java提供了一种稍弱的同步机制即volatile变量,用来确保将变量的更新操作通知到其他线程.当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的.然而,在访问volatile变量时不会执行加锁操作

【Java】线程并发、互斥与同步

网络上对于线程的解析总是天花龙凤的,给你灌输一大堆概念,考研.本科的操作系统必修课尤甚,让你就算仔细看完一大堆文章都不知道干什么. 下面不取网站复制粘贴,在讲解自己的Java线程并发.互斥与同步之前先给大家解构操作系统书中那些给出书者为了出书者而写的废话到底是什么意思. 大神们如果只想看程序,可以自行跳过,反正我的文章从来新手向,不喜勿看. 一.线程的基本概念 其实线程的概念非常简单,多一个线程,就多了一个做事情的人. 比如搬东西,搬10箱货物,这就是所谓的一个进程,一个人,就是所谓的一个线程,

JAVA并发编程4_线程同步之volatile关键字

上一篇博客JAVA并发编程3_线程同步之synchronized关键字中讲解了JAVA中保证线程同步的关键字synchronized,其实JAVA里面还有个较弱的同步机制volatile.volatile关键字是JAVA中的轻量级的同步机制,用来将变量的更新操作同步到其他线程.从内存可见性的角度来说,写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块. 旧的内存模型:保证读写volatile都直接发生在main memory中. 在新的内存模型下(1.5)

&lt;&lt;操作系统精髓与设计原理&gt;&gt;读书笔记(一) 并发性:互斥与同步(1)

<<操作系统精髓与设计原理>>读书笔记(一) 并发性:互斥与同步 并发问题是所有问题的基础,也是操作系统设计的基础.并发包括很多设计问题,其中有进程间通信,资源共享与竞争,多个进程活动的同步以及分配给进程的处理器时间的. 和并发相关的关键术语:原子操作: 一个或多个指令的序列,对外是不可分的:即没有其他进程可以看到其中间状态或者中断此操作. 并发中,为了确保并发下的数据完整性,我们有一系列的同步方法,其实这些就是为了实现互斥性!对临界区程序的互斥性.有三种方法: 1.软件方法,但是

Java并发编程-非阻塞同步方式原子类(Atomic)的使用

非阻塞同步 在大多数情况下,我们为了实现线程安全都会使用Synchronized或lock来加锁进行线程的互斥同步,但互斥同步的最主要的问题就是进行线程的阻塞和唤醒所带来的性能问题,因此这种阻塞也称作阻塞同步.从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都会进行加锁.用户态核心态转换.维护锁的计数器和检查是否有被阻塞的线程需要被唤醒等操作. 随着硬件指令集的发展,我们有了另一个选择:基于冲突检测的乐

JAVA并发编程3_线程同步之synchronized关键字

在上一篇博客里讲解了JAVA的线程的内存模型,见:JAVA并发编程2_线程安全&内存模型,接着上一篇提到的问题解决多线程共享资源的情况下的线程安全问题. 不安全线程分析 public class Test implements Runnable { private int i = 0; private int getNext() { return i++; } @Override public void run() { // synchronized while (true) { synchro

【Java并发编程二】同步容器和并发容器

一.同步容器 在Java中,同步容器包括两个部分,一个是vector和HashTable,查看vector.HashTable的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字synchornized. 另一个是Collections类中提供的静态工厂方法创建的同步包装类. 同步容器都是线程安全的.但是对于复合操作(迭代.缺少即加入.导航:根据一定的顺序寻找下一个元素),有时可能需要使用额外的客户端加锁进行保护.在一个同步容器中,复合操作是安全

iOS并发编程指南之同步

1.gcd fmdb使用了gcd,它是通过 建立系列化的G-C-D队列 从多线程同时调用调用方法,GCD也会按它接收的块的顺序来执行. fmdb使用的是dispatch_sync,多线程调用a serialized queue,gcd会在接收块的线程执行,并阻塞其他线程. 使用FMDatabaseQueue 及线程安全 在多个 线程中同时使用一个FMDatabase实例是不明智的.现在你可以为每个线程创建一个FMDatabase对象. 不要让多个线程分享同一个实例,它无法在多个线程中同时使用.

JAVA 并发编程-传统线程同步通信技术(四)

首先介绍几个概念: wait()方法 wait()方法使得当前线程必须要等待,等到另外一个线程调用notify()或者notifyAll()方法. 当前的线程必须拥有当前对象的monitor,也即lock,就是锁. 线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行. 要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或s