C++11 互斥量

  • Post author:
  • Post category:其他




01 简介

互斥量是一种同步原语,是

一种线程同步的手段,用于保护多线程同时访问的共享数据

C++11 头文件 <

mutex

> 包含:


  • Mutex 类系列


    • std::mutex

      独占的互斥量,不能递归使用。

    • std::timed_mutex

      带超时的独占互斥量,不能递归使用。

    • std::recursive_mutex

      递归互斥量。

    • std::recursive_timed_mutex

      带超时的递归互斥里昂。

  • Lock 类


    • std::lock_guard

      与 Mutex RAII 相关,方便线程对互斥量上锁。

    • std::unique_lock

      与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。

  • 其它类型


    • std::once_flag

    • std::adopt_lock_t

    • std::defer_lock_t

    • std::try_to_lock_t

  • 函数


    • std::try_lock

      尝试同时对多个互斥量上锁。

    • std::lock

      可以同时对多个互斥量上锁。

    • std::call_once

      如果多个线程需要同时调用某个函数,call_once 可以保证多个线程对该函数只调用一次。



02 Mutex 类



2.1 std::mutex

互斥量是用于

保护共享数据免受从多个线程同时访问

的同步对象。

互斥量的

基本用法

如下:


  • lock

    该方法阻塞线程,直到获得互斥量的所有权位置。


  • unlock

    该方法用于在获得互斥量并执行完任务之后除对互斥量的占用。


  • try_lock

    该方法尝试锁定互斥量,如果成功返回 true, 失败返回 false, 它并不会阻塞线程。线程调用 try_lock 方法会出现如下 3 种情况:

    • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
    • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
    • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

    lock 和 unlock 方法必须成对使用。


  • 成员函数

    lock
    unlock
    try_lock
    

  • 注意事项

    • lock 和 unlock 必须成对使用。
    • lock 会

      阻塞线程

      直到获的互斥量;而 try_lock 返回 false 时不会线程

      不会被阻塞

    • 使用独占的互斥量 std::mutex 可能会出现死锁现象:如使用 lock 对已经上锁的互斥量上锁,因此

      不建议直接使用 lock, unlock

      后续会介绍更好控制上锁解锁行为的

      std::lock_guard

      ,

      std::unique_lock


  • 应用实例

    /***************************************************************
     * lock
     * unlock
     * ************************************************************/
    int count{ 0 };
    
    std::mutex mtx01;
    
    void printNum(int num)
    {
        mtx01.lock();
    
        // 临界区独占 std::cout 
        std::cout << count++ << ": "  << num << std::endl;
    
        mtx01.unlock();
    }
    
    std::thread th01(printNum, 10);
    std::thread th02(printNum, 15);
    
    th01.join();
    th02.join();
    /*
    // ---------------------------------------------------------
    // output
    // ---------------------------------------------------------
    0: 10
    1: 15
    */
    
    /***************************************************************
     * try_lock
     * ************************************************************/
    volatile int index{ 0 };
    std::vector<int> numVec{ 1, 2, 3, 4, 5 };
    
    std::mutex mtx01;
    
    void printNum(int thIndex, int numIndex, int sleepTime)
    {
        std::this_thread::sleep_for(std::chrono::seconds(sleepTime));
    
        if (mtx01.try_lock())
        {
            std::cout << index << ": thread0" << thIndex << " read num " << numVec[numIndex] << std::endl;
            ++index;
            mtx01.unlock();
        }
    }
    
    std::vector<std::thread> thVec(5);
    
    for (int i = 0; i < thVec.size(); ++i)
    {
        thVec[i] = std::thread(printNum, i + 1, i, 5 - i);
    }
    
    for (auto& th : thVec)
    {
        th.join();
    }
    /*
    // ---------------------------------------------------------
    // output
    // ---------------------------------------------------------
    0: thread05 read num 5
    1: thread04 read num 4
    2: thread03 read num 3
    3: thread02 read num 2
    4: thread01 read num 1
    */
    



2.2 std::recursive_mutex

std::recursive_mutex 允许

同一线程获得该互斥锁

,可以用于解决同一线程多次获取互斥量时的死锁问题。

std::recursive_mutex 在线程种使用 lock 和 unlock 的次数必须相同。

std::recursive_mutex recMtx01;

void printNum01(const int n) 
{
    recMtx01.lock();
    std::cout << n << std::endl;                // 10
    recMtx01.unlock();
}

void threadFunc01(int n)
{
    recMtx01.lock();
    printNum01(n);
    recMtx01.unlock();
}

std::thread th01(threadFunc01, 10);
th01.join();



2.3 std::timed_mutex & std::recursive_timed_mutex

程序在实际运行种有时不知道获取锁需要多久,为了不至于一直等待获取互斥量就设置一个等待超时时间。

带超时时间的互斥量增加了两个方法:

try_lock_for        // 尝试锁定互斥,若互斥在指定的时限中不可用则返回
try_lock_until      // 尝试锁定互斥,若直至抵达指定时间点互斥不可用则返回

应用实例如下:

/***************************************************************
 * try_lock_for
 * ************************************************************/
volatile int index{ 0 };
std::vector<int> numVec{ 1, 2, 3, 4, 5 };

std::timed_mutex tmdMtx01;

void printNum(int thIndex, int numIndex)
{
    // 循环尝试锁定互斥量,每次尝试的超时时间为 0.5s
    while (!tmdMtx01.try_lock_for(std::chrono::milliseconds(500))) 
    {
        // 等待 1s 之后重新尝试
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }

    // 互斥量上锁成功之后执行临界区的操作
    std::cout << index++ << ": thread0" << thIndex << " read num " << numVec[numIndex] << std::endl;

    // 任务执行完成之后记得解锁互斥量
    tmdMtx01.unlock();
}

std::vector<std::thread> thVec(5);

for (int i = 0; i < thVec.size(); ++i)
{
    thVec[i] = std::thread(printNum, i + 1, i);
}

for (auto& th : thVec)
{
    th.join();
}
/*
// ---------------------------------------------------------
// output
// ---------------------------------------------------------
0: thread03 read num 3
1: thread01 read num 1
2: thread02 read num 2
3: thread04 read num 4
4: thread05 read num 5
*/

/***************************************************************
 * try_lock_until [参见 std::this_thread::sleep_until]
 * ************************************************************/



03 Lock 类



3.1 lock_guard

lock_guard 是互斥体包装器: 创建 lock_guard 对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard 对象的作用域时,销毁 lock_guard 并释放互斥(RAII)。

lock_guard 特点如下:

  • 创建即加锁,作用域结束之后自动析构并解锁,不需要手动上锁解锁。
  • 在 lock_guard 对象作用域内不能对互斥量解锁。
  • 不能进行赋值相关的操作,但是可以进行移动相关的操作。
int count{ 0 };

std::mutex mtx01;

void printNum(int num)
{
    // lock_guard 构造的时获取互斥量
    std::lock_guard<std::mutex> glckMtx01(mtx01);

    // 临界区独占 std::cout 
    std::cout << count++ << ": "  << num << std::endl;

    // lock_guard 离开作用域析构时解锁互斥量
}

std::thread th01(printNum, 10);
std::thread th02(printNum, 15);

th01.join();
th02.join();
/*
// ---------------------------------------------------------
// output
// ---------------------------------------------------------
0: 10
1: 15
*/



3.2 unique_lock

unique_lock 是一个通用的互斥量锁定包装器:它允许

延迟锁定



限时深度锁定



递归锁定



锁定所有权的转移

以及

与条件变量一起使用

unique_lock 是 lock_guard 的增强版,它具有 lock_guard 的所有功能,同时又支持上锁、解锁操作。

unique_lock 特点如下:

  • 创建时可以通过指定第二个参数实现之创建而不锁定互斥量。

  • 可以随时执行上锁、解锁操作。

  • unique_lock 对象析构时和 lock_guard 操作相似,也会自动解锁。

  • 不能进行赋值相关的操作,但是可以进行移动相关的操作。

  • 条件变量需要 unique_lock 类型的锁作为参数。


  • 成员函数

    (constructor)           
    (destructor)
    lock
    try_lock
    try_lock_for
    try_lock_until
    unlock
    operator=
    swap                    // 与另外一个 std::unique_lock 交换状态
    release                 // 释放关联互斥量但是不解锁它
    owns_lock               // 检查锁是否占有其相关互斥
    operator bool           // 检查锁是否占有其相关互斥
    mutex                   // 返回指向互斥量的指针
    

  • 应用实例


    • 构造函数


      • default

        unique_lock() noexcept;
        

        新创建的 unique_lock 对象不管理任何 mutex 对象。


      • locking

        explicit unique_lock(mutex_type& m);
        

        新创建的 unique_lock 对象管理 mutex 对象 m, 并尝试调用 m.lock() 对 mutex 对象进行上锁。如果此时某一个 unique_lock 已经管理了该 mutex 对象的 m 则当前线程将会被阻塞。


      • try locking

        unique_lock(mutex_type& m, try_to_lock_t tag);
        

        新创建的 unique_lock 对象管理 mutex 对象 m, 并尝试调用 m.try_lock() 对 mutex 对象进行上锁。如果上锁不成功将不会阻塞当前线程。


      • deferred

        unique_lock(mutex_type& m, defer_lock_t tag) noexcept;
        

        新创建的 unique_lock 对象管理 mutex 对象 m, 但是在初始化时并不对 mutex 对象上锁, m 应该是一个没有被当前线程锁住的 mutex 对象。


      • adopting

        unique_lock(mutex_type& m, adopt_lock_t tag);
        

        新创建的 unique_lock 对象管理 mutex 对象 m, m 应该是一个已经被当前线程锁住的 mutex 对象。


      • locking for

        template <class Rep, class Period>
        unique_lock(mutex_type& m, const chrono::duration<Rep,Period>& rel_time);
        

        新创建的 unique_lock 对象管理 mutex 对象 m, 并试图通过调用 m.try_lock_for(rel_time) 来锁住 mutex 对象 rel_time 长度的时间。


      • locking until

        template <class Clock, class Duration>
        unique_lock(mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time);
        

        新创建的 unique_lock 对象管理 mutex 对象 m, 并试图通过调用 m.try_lock_until(abs_time) 在时间点 abs_time 之前锁住 mutex 对象。


      • copy [deleted]


      • move (9)

        unique_lock(unique_lock&& x);
        

        新创建的 unique_lock 对象获得了由 x 所管理的 mutex 对象的所有权(包括当前 mutex 对象的状态)。调用 move 构造之后, x 对象如同通过默认构造函数所创建的,就不再管理任何 mutex 对象了。

      /***************************************************************
       * (constructor) 
       * lock
       * unlock
       * try_lock
       * try_lock_for
       * operator=
       * ************************************************************/
      
      std::mutex mtx01, mtx02, mtx03, mtx04;
      std::timed_mutex tmtx01, tmtx02;
      
      void func01() 
      {
          // std::unique_lock<std::mutex> ulck01;
          std::unique_lock<std::mutex> ulck02(mtx01);
          // std::unique_lock<std::mutex> ulck03(ulck02);             // error: 拷贝构造被禁止
          // std::unique_lock<std::mutex> ulck04 = ulck02;            // error: 赋值操作被禁止
          std::unique_lock<std::mutex> ulck06(std::move(ulck02));     // ok: 移动构造
          std::unique_lock<std::mutex> ulck07;                
          ulck07 = std::move(ulck02);                                 // ok: 移动赋值
      
          // 构造时尝试调用 mtx02.try_lock() 对互斥量 mtx02 上锁,如果上锁失败不会阻塞线程
          std::unique_lock<std::mutex> ulck08(mtx02, std::try_to_lock); 
      
          // 构造时尝试不对互斥量 mtx03 上锁
          std::unique_lock<std::mutex> ulck09(mtx03, std::defer_lock);
      
          mtx04.lock();               // 使用 std::adopt_lock 时应该提前上锁互斥量      
      
          // 构造时尝试不对互斥量 mtx04 上锁,但是在此之前当前线程应该已经对互斥量 mtx04 成功上锁
          std::unique_lock<std::mutex> ulck10(mtx04, std::adopt_lock);
      
          ulck09.try_lock();           // 使用 std::defer_lock 时应该在后续语句中对互斥量上锁
      
          // try_lock_for
          std::unique_lock<std::timed_mutex> ulck11(tmtx01, std::defer_lock);
          // 循环尝试对互斥量 mtx05 上锁每次尝试超时时间为 0.5s 直到上锁成功
          while (!ulck11.try_lock_for(std::chrono::milliseconds(500))) 
          {
              // 等待 1s 之后重新尝试
              std::this_thread::sleep_for(std::chrono::milliseconds(1000));
          }
      }
      
      std::thread th01(func01);
      
      th01.join();
      

    • 其它成员

      // 暂时没有好的代码作为示例
      


C++11 多线程并发编程目录