• c++多线程基础3(mutex)


    整理自:zh.cppreference.com/w/cpp/thread

    互斥锁

    互斥算法避免多个线程同时访问共享资源。这会避免数据竞争,并提供线程间的同步支持。定义于头文件 <mutex>

    互斥锁有可重入、不可重入之分。C++标准库中用 mutex 表示不可重入的互斥锁,用 recursive_mutex 表示可重入的互斥锁。为这两个类增加根据时间来阻塞线程的能力,就又有了两个新的互斥锁:timed_mutex(不可重入的锁)、recursive_timed_mutex(可重入的锁)

    C++标准库的所有mutex都是不可拷贝的,也不可移动

    std::mutex:

    mutex 类是能用于保护共享数据免受从多个线程同时访问的同步原语。mutex 提供排他性非递归所有权语义。操作:

    lock:如果 mutex 未上锁,则将其上锁。否则如果已经其它线程 lock,则阻塞当前线程

    try_lock:如果 mutex 未上锁,则将其上锁。否则返回 false,并不阻塞当前线程

    unlock:如果 mutex 被当前线程锁住,则将其解锁。否则,是未定义的行为

    native_handle:返回底层实现定义的线程句柄

    注意:std::mutex 既不可复制亦不可移动

    例1:

     1 #include <iostream>
     2 #include <chrono>
     3 #include <thread>
     4 #include <mutex>
     5 using namespace std;
     6 
     7 int g_num = 0;//为 g_num_mutex 所保护
     8 std::mutex g_num_mutex;
     9 
    10 void slow_increment(int id) {
    11     for(int i = 0; i < 3; ++i) {
    12         g_num_mutex.lock();
    13         ++g_num;
    14         cout << id << " => " << g_num << endl;
    15         g_num_mutex.unlock();
    16 
    17         std::this_thread::sleep_for(std::chrono::seconds(1));
    18     }
    19 }
    20 
    21 int main(void) {
    22     std::thread t1(slow_increment, 0);
    23     std::thread t2(slow_increment, 1);
    24     t1.join();
    25     t2.join();
    26 
    27 // 输出:
    28 // 0 => 1
    29 // 1 => 2
    30 // 0 => 3
    31 // 1 => 4
    32 // 0 => 5
    33 // 1 => 6
    34 
    35     return 0;
    36 }
    View Code

    例2:

     1 #include <iostream>
     2 #include <chrono>
     3 #include <mutex>
     4 #include <thread>
     5 using  namespace std;
     6 
     7 std::chrono::milliseconds interval(100);
     8 std::mutex mtex;
     9 int job_shared = 0;//两个线程都能修改,mtex将保护此变量
    10 int job_exclusive = 0;//只有一个线程能修改
    11 
    12 //此线程能修改 jon_shared 和 job_exclusive
    13 void job_1() {
    14     std::this_thread::sleep_for(interval);//令job_2持锁
    15 
    16     while(true) {
    17         //尝试锁定 mtex 以修改 job_shared
    18         if(mtex.try_lock()) {
    19             cout << "job shared (" << job_shared << ")
    ";
    20             mtex.unlock();
    21             return;
    22         } else {
    23             //不能修改 job_shared
    24             ++job_exclusive;
    25             cout << "job exclusive (" << job_exclusive << ")
    ";
    26             std::this_thread::sleep_for(interval);
    27         }
    28     }
    29 }
    30 
    31 // 此线程只能修改 job_shared
    32 void job_2() {
    33     mtex.lock();
    34     std::this_thread::sleep_for(5 * interval);
    35     ++job_shared;
    36     mtex.unlock();
    37 }
    38 
    39 int main(void) {
    40     std::thread t1(job_1);
    41     std::thread t2(job_2);
    42     t1.join();
    43     t2.join();
    44 
    45 // 输出:
    46 // job exclusive (1)
    47 // job exclusive (2)
    48 // job exclusive (3)
    49 // job exclusive (4)
    50 // job shared (1)
    51 
    52     return 0;
    53 }
    View Code

    std::timed_mutex:

    timed_mutex 类是能用于保护数据免受多个线程同时访问的同步原语。

    以类似 mutex 的行为, timed_mutex 提供排他性非递归所有权语义。另外,timed_mutex 在 mutex 的基础上增加了以下两个操作:

    try_lock_for():

    函数原型:templateclass Rep, class Period >
    bool try_lock_forconst std::chrono::duration<Rep,Period>& timeout_duration );

    尝试锁互斥。阻塞直到经过指定的 timeout_duration 或得到锁,取决于何者先到来。成功获得锁时返回 true , 否则返回 false 。若 timeout_duration 小于或等于 timeout_duration.zero() ,则函数表现同 try_lock() 。由于调度或资源争议延迟,此函数可能阻塞长于 timeout_duration 。

    标准推荐用 steady_clock 度量时长。若实现用 system_clock 代替,则等待时间亦可能对时钟调整敏感。

    同 try_lock() ,允许此函数虚假地失败并返回 false ,即使在 timeout_duration 中某点互斥不为任何线程所锁定。

    若此操作返回 true ,则同一互斥上先前的 unlock() 调用同步于(定义于 std::memory_order )它。若已占有 mutex 的线程调用 try_lock_for ,则行为未定义。

    try_lock_until(time_point):

    函数原型:templateclass Clock, class Duration >
    bool try_lock_untilconst std::chrono::time_point<Clock,Duration>& timeout_time );

    尝试所互斥。阻塞直至抵达指定的 timeout_time 或得到锁,取决于何者先到来。成功获得锁时返回 true ,否则返回 false 。若已经过 timeout_time ,则此函数表现同 try_lock() 。

    使用倾向于 timeout_time 的时钟,这表示时钟调节有影响。从而阻塞的最大时长可能小于但不会大于在调用时的 timeout_time - Clock::now() ,依赖于调整的方向。由于调度或资源争议延迟,函数亦可能阻塞长于抵达 timeout_time 之后。同 try_lock() ,允许此函数虚假地失败并返回 false ,即使在 timeout_time 前的某点任何线程都不锁定互斥。

    若此操作返回 true ,则同一互斥上先前的 unlock() 调用同步于(定义于 std::memory_order )它。

    若已占有 mutex 的线程调用 try_lock_until ,则行为未定义。

    try_lock_for / until可以检测到死锁的出现:

    1 if(!try_lock_for(chrono::hours(1)))
    2 {
    3   throw "出现死锁!";  
    4 }
    View Code

    例1:

     1 #include <iostream>
     2 #include <mutex>
     3 #include <thread>
     4 #include <vector>
     5 #include <sstream>
     6  
     7 std::mutex cout_mutex; // 控制到 std::cout 的访问
     8 std::timed_mutex mutex;
     9  
    10 void job(int id) 
    11 {
    12     using Ms = std::chrono::milliseconds;
    13     std::ostringstream stream;
    14  
    15     for (int i = 0; i < 3; ++i) {
    16         if (mutex.try_lock_for(Ms(100))) {
    17             stream << "success ";
    18             std::this_thread::sleep_for(Ms(100));
    19             mutex.unlock();
    20         } else {
    21             stream << "failed ";
    22         }
    23         std::this_thread::sleep_for(Ms(100));
    24     }
    25  
    26     std::lock_guard<std::mutex> lock(cout_mutex);
    27     std::cout << "[" << id << "] " << stream.str() << "
    ";
    28 }
    29  
    30 int main() 
    31 {
    32     std::vector<std::thread> threads;
    33     for (int i = 0; i < 4; ++i) {
    34         threads.emplace_back(job, i);
    35     }
    36  
    37     for (auto& i: threads) {
    38         i.join();
    39     }
    40 
    41 // 输出:
    42 // [0] failed failed failed 
    43 // [3] failed failed success 
    44 // [2] failed success failed 
    45 // [1] success failed success
    46 
    47     return 0;
    48 }
    View Code

    例2:

     1 #include <thread>
     2 #include <iostream>
     3 #include <chrono>
     4 #include <mutex>
     5  
     6 std::timed_mutex test_mutex;
     7  
     8 void f()
     9 {
    10     auto now=std::chrono::steady_clock::now();
    11     test_mutex.try_lock_until(now + std::chrono::seconds(10));
    12     std::cout << "hello world
    ";
    13 }
    14  
    15 int main()
    16 {
    17     std::lock_guard<std::timed_mutex> l(test_mutex);
    18     std::thread t(f);
    19     t.join();
    20 
    21     return 0;
    22 }
    View Code

    递归锁:

    在同一个线程中连续 lock 两次 mutex 会产生死锁:

    一般情况下,如果同一个线程先后两次调用 lock,在第二次调⽤用时,由于锁已经被占用,该线程会挂起等待占用锁的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此 就永远处于挂起等待状态了,于是就形成了死锁(Deadlock):

     1 #include<iostream> //std::cout
     2 #include<thread>   //std::thread
     3 #include<mutex>    //std::mutex
     4 using namespace std;
     5 mutex g_mutex;
     6 
     7 void threadfun1()
     8 {
     9     cout << "enter threadfun1" << endl;
    10     // lock_guard<mutex> lock(g_mutex);
    11     g_mutex.lock();
    12     cout << "execute threadfun1" << endl;
    13     g_mutex.unlock();
    14 }
    15 
    16 void threadfun2()
    17 {
    18     cout << "enter threadfun2" << endl;
    19     // lock_guard<mutex> lock(g_mutex);
    20     g_mutex.lock();
    21     threadfun1();
    22     cout << "execute threadfun2" << endl;
    23     g_mutex.unlock();
    24 }
    25 
    26 int main()
    27 {
    28     threadfun2(); //死锁
    29     return 0;
    30 }
    31  
    32 // 运行结果:
    33 // enter threadfun2
    34 // enter threadfun1
    35 //就会产生死锁
    View Code

    此时就需要使用递归式互斥量 recursive_mutex 来避免这个问题。recursive_mutex 不会产生上述的死锁问题,只是是增加锁的计数,但必须确保你 unlock 和 lock 的次数相同,其他线程才可能锁这个 mutex:

     1 #include<iostream> //std::cout
     2 #include<thread>   //std::thread
     3 #include<mutex>    //std::mutex
     4 using namespace std;
     5 
     6 recursive_mutex g_rec_mutex;
     7 
     8 void threadfun1()
     9 {
    10     cout << "enter threadfun1" << endl;
    11     lock_guard<recursive_mutex> lock(g_rec_mutex);
    12     cout << "execute threadfun1" << endl;
    13 }
    14 
    15 void threadfun2()
    16 {
    17     cout << "enter threadfun2" << endl;
    18     lock_guard<recursive_mutex> lock(g_rec_mutex);
    19     threadfun1();
    20     cout << "execute threadfun2" << endl;
    21 }
    22 
    23 int main()
    24 {
    25     threadfun2(); //利用递归式互斥量来避免这个问题
    26     return 0;
    27 }
    28 // 运行结果:
    29 // enter threadfun2
    30 // enter threadfun1
    31 // execute threadfun1
    32 // execute threadfun2
    View Code

    recursive_mutex、recursive_timed_mutex 与对应的 mutex、timed_mutex 操作一致。不同点在于,非递归锁在 lock 或 try_lock 一个已经被当前线程 lock 的锁时会导致死锁,而递归锁不会

    共享锁:

    std::shared_timed_mutex(c++14起)

    shared_mutex 类是能用于保护数据免受多个线程同时访问的同步原语。与其他促进排他性访问的互斥类型相反, shared_mutex 拥有二个层次的访问:

    • 共享 - 多个线程能共享同一互斥的所有权。
    • 排他性 - 仅一个线程能占有互斥。

    共享互斥通常用于多个读线程能同时访问同一资源而不导致数据竞争,但只有一个写线程能访问的情形:

     1 #include <iostream>
     2 #include <mutex>  // 对于 std::unique_lock
     3 #include <shared_mutex>
     4 #include <thread>
     5  
     6 class ThreadSafeCounter {
     7  public:
     8   ThreadSafeCounter() = default;
     9  
    10   // 多个线程/读者能同时读计数器的值。
    11   unsigned int get() const {
    12     std::shared_lock<std::shared_timed_mutex> lock(mutex_);//shared_lock 作用类似于 lock_guard
    13     return value_;
    14   }
    15  
    16   // 只有一个线程/写者能增加/写线程的值。
    17   void increment() {
    18     std::unique_lock<std::shared_timed_mutex> lock(mutex_);
    19     value_++;
    20   }
    21  
    22   // 只有一个线程/写者能重置/写线程的值。
    23   void reset() {
    24     std::unique_lock<std::shared_timed_mutex> lock(mutex_);
    25     value_ = 0;
    26   }
    27  
    28  private:
    29   mutable std::shared_timed_mutex mutex_;
    30   unsigned int value_ = 0;
    31 };
    32  
    33 int main() {
    34   ThreadSafeCounter counter;
    35  
    36   auto increment_and_print = [&counter]() {
    37     for (int i = 0; i < 3; i++) {
    38       counter.increment();
    39       std::cout << std::this_thread::get_id() << ' ' << counter.get() << '
    ';
    40  
    41       // 注意:写入 std::cout 实际上也要由另一互斥同步。省略它以保持示例简洁。
    42     }
    43   };
    44  
    45   std::thread thread1(increment_and_print);
    46   std::thread thread2(increment_and_print);
    47  
    48   thread1.join();
    49   thread2.join();
    50 
    51 // 输出:
    52 // 2 1
    53 // 3 2
    54 // 2 3
    55 // 3 4
    56 // 2 5
    57 // 3 6
    58 
    59   return 0;
    60 }
    View Code

    std::shared_mutex(c++17起)

    以类似 timed_mutex 的行为, shared_timed_mutex 提供通过 try_lock_for() 、 try_lock_until() 、 try_lock_shared_for() 、 try_lock_shared_until() 方法,试图带时限地要求 shared_timed_mutex 所有权的能力。std::shared_mutex 则恰好相反

    通用互斥管理:

    定义于头文件 <mutex>

    std::lock_guard:

    类 lock_guard 是互斥封装器,为在作用域块期间占有互斥提供便利 RAII 风格机制。

    创建 lock_guard 对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard 对象的作用域时,销毁 lock_guard 并释放互斥。

    lock_guard 类不可复制

    要锁定的互斥,类型必须满足基础可锁要求

    代码:

     1 #include <thread>
     2 #include <mutex>
     3 #include <iostream>
     4  
     5 int g_i = 0;
     6 std::mutex g_i_mutex;  // 保护 g_i
     7  
     8 void safe_increment()
     9 {
    10     std::lock_guard<std::mutex> lock(g_i_mutex);
    11     ++g_i;
    12  
    13     std::cout << std::this_thread::get_id() << ": " << g_i << '
    ';
    14  
    15     // g_i_mutex 在锁离开作用域时自动释放
    16 }
    17  
    18 int main()
    19 {
    20     std::cout << "main: " << g_i << '
    ';
    21  
    22     std::thread t1(safe_increment);
    23     std::thread t2(safe_increment);
    24  
    25     t1.join();
    26     t2.join();
    27  
    28     std::cout << "main: " << g_i << '
    ';
    29 
    30 // 输出:
    31 // main: 0
    32 // 2: 1
    33 // 3: 2
    34 // main: 2
    35 
    36     return 0;
    37 }
    View Code

    std::scoped_lock(c++17起):

    类 scoped_lock 是提供便利 RAII 风格机制的互斥包装器,它在作用域块的存在期间占有一或多个互斥。

    创建 scoped_lock 对象时,它试图取得给定互斥的所有权。控制离开创建 scoped_lock 对象的作用域时,析构 scoped_lock 并以逆序释放互斥。若给出数个互斥,则使用免死锁算法,如同以 std::lock 。

    scoped_lock 类不可复制

    要锁的互斥类型必须满足可锁要求

    构造函数:

    explicit scoped_lock( MutexTypes&... );

    scoped_lockstd::adopt_lock_t, MutexTypes&... );

    scoped_lockconst scoped_lock= delete;

    取得给定互斥 m 的所有权。

    1) 若 sizeof...(MutexTypes== 0 则不做任何事。否则若 sizeof...(MutexTypes== 1 ,则等效地调用 m.lock() 。否则,等效地调用 std::lock(m...) 。若 MutexTypes 之一不是递归互斥,且当前线程已占有 m... 中对应的参数,则行为未定义。
    2) 取得互斥 m... 的所有权而试图不锁定任何互斥。若当前线程已占有 m... 中所有互斥,则行为未定义。
    3) 复制构造函数被删除。

    若 m 在 scoped_lock 对象之前被销毁,则行为未定义

    要获得其所有权的互斥

    代码:

     1 #include <mutex>
     2 #include <thread>
     3 #include <iostream>
     4 #include <vector>
     5 #include <functional>
     6 #include <chrono>
     7 #include <string>
     8  
     9 struct Employee {
    10     Employee(std::string id) : id(id) {}
    11     std::string id;
    12     std::vector<std::string> lunch_partners;
    13     std::mutex m;
    14     std::string output() const
    15     {
    16         std::string ret = "Employee " + id + " has lunch partners: ";
    17         for( const auto& partner : lunch_partners )
    18             ret += partner + " ";
    19         return ret;
    20     }
    21 };
    22  
    23 void send_mail(Employee &, Employee &)
    24 {
    25     // 模拟耗时的发信操作
    26     std::this_thread::sleep_for(std::chrono::seconds(1));
    27 }
    28  
    29 void assign_lunch_partner(Employee &e1, Employee &e2)
    30 {
    31     static std::mutex io_mutex;
    32     {
    33         std::lock_guard<std::mutex> lk(io_mutex);
    34         std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
    35     }
    36  
    37     {
    38         // 用 std::scoped_lock 取得二个锁,而无需担心
    39         // 其他对 assign_lunch_partner 的调用死锁我们
    40         // 而且它亦提供便利的 RAII 风格机制
    41  
    42         std::scoped_lock lock(e1.m, e2.m);
    43  
    44         // 等价代码 1 (用 std::lock 和 std::lock_guard )
    45         // std::lock(e1.m, e2.m);
    46         // std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
    47         // std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);
    48  
    49         // 等价代码 2 (若需要 unique_lock ,例如对于条件变量)
    50         // std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
    51         // std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
    52         // std::lock(lk1, lk2);
    53         {
    54             std::lock_guard<std::mutex> lk(io_mutex);
    55             std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
    56         }
    57         e1.lunch_partners.push_back(e2.id);
    58         e2.lunch_partners.push_back(e1.id);
    59     }
    60  
    61     send_mail(e1, e2);
    62     send_mail(e2, e1);
    63 }
    64  
    65 int main()
    66 {
    67     Employee alice("alice"), bob("bob"), christina("christina"), dave("dave");
    68  
    69     // 在并行线程中指派,因为就午餐指派发邮件消耗很长时间
    70     std::vector<std::thread> threads;
    71     threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
    72     threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(bob));
    73     threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(alice));
    74     threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));
    75  
    76     for (auto &thread : threads) thread.join();
    77     std::cout << alice.output() << '
    '  << bob.output() << '
    '
    78               << christina.output() << '
    ' << dave.output() << '
    ';
    79 }
    View Code

    blog.csdn.net/zouxinfox/article/details/5848519

    unique_lock

    类 unique_lock 可移动,但不可复制——它满足可移动构造 (MoveConstructible) 和可移动赋值 (MoveAssignable) 但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 。类 unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。

    类 unique_lock 满足基础可锁 (BasicLockable) 要求。若 Mutex 满足可锁 (Lockable) 要求,则 unique_lock 亦满足可锁 (Lockable) 要求(例如:能用于 std::lock ) ;若 Mutex 满足定时可锁 (TimedLockable) 要求,则 unique_lock 亦满足定时可锁 (TimedLockable) 要求

    要锁定的互斥类型。类型必须满足基础可锁 (BasicLockable) 要求

    代码:

     1 #include <mutex>
     2 #include <thread>
     3 #include <chrono>
     4  
     5 struct Box {
     6     explicit Box(int num) : num_things{num} {}
     7  
     8     int num_things;
     9     std::mutex m;
    10 };
    11  
    12 void transfer(Box &from, Box &to, int num)
    13 {
    14     // 仍未实际取锁
    15     std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
    16     std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
    17  
    18     // 锁两个 unique_lock 而不死锁
    19     std::lock(lock1, lock2);
    20  
    21     from.num_things -= num;
    22     to.num_things += num;
    23  
    24     // 'from.m' 与 'to.m' 互斥解锁于 'unique_lock' 析构函数
    25 }
    26  
    27 int main()
    28 {
    29     Box acc1(100);
    30     Box acc2(50);
    31  
    32     std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
    33     std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);
    34  
    35     t1.join();
    36     t2.join();
    37 }
    View Code

    关于 lock_guard 和 unique_lock:

    http://lib.csdn.net/article/cplusplus/29099

    http://blog.csdn.net/daaikuaichuan/article/details/71022824

  • 相关阅读:
    BigDecimal
    android sdk manager 无法更新,解决连不上dl.google.com的问题
    程序卡在 while(SPI_I2S_GetFlagStatus(W5500_SPI, SPI_I2S_FLAG_TXE) == RESET) 处
    获取本设备IP地址
    Xamarin Android 监听音量键(下)
    xamarin Android 监听音量键(上)
    最大子序列和
    2.找出单独出现的数字
    编程题常见输入格式处理方法
    二进制比较:破解 LuaJIT 加密脚本的一种新思路。直接修改,无需反编译
  • 原文地址:https://www.cnblogs.com/geloutingyu/p/8538963.html
Copyright © 2020-2023  润新知