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

互斥锁

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

例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 << ")\n";
20             mtex.unlock();
21             return;
22         } else {
23             //不能修改 job_shared
24             ++job_exclusive;
25             cout << "job exclusive (" << job_exclusive << ")\n";
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 }

std::timed_mutex:

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

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

try_lock_for():

函数原型:template< class Rep, class Period >
bool try_lock_for( const 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):

函数原型:template< class Clock, class Duration >
bool try_lock_until( const 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 }

例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() << "\n";
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 }

例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\n";
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 }

递归锁:

在同一个线程中连续 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 //就会产生死锁

此时就需要使用递归式互斥量 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

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() << ‘\n‘;
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 }

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 << ‘\n‘;
14
15     // g_i_mutex 在锁离开作用域时自动释放
16 }
17
18 int main()
19 {
20     std::cout << "main: " << g_i << ‘\n‘;
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 << ‘\n‘;
29
30 // 输出:
31 // main: 0
32 // 2: 1
33 // 3: 2
34 // main: 2
35
36     return 0;
37 }

原文地址:https://www.cnblogs.com/geloutingyu/p/8538963.html

时间: 2024-08-30 10:55:31

c++多线程基础3(mutex)的相关文章

Java多线程基础

1. 前言 这篇文章,是对Java多线程编程的基础性介绍. 文章将介绍Java语言为支持多线程编程提供的一些特性.通过这篇文章,您将了解到如何通过Java语言创建一个线程,如何通过内置的锁来实现线程间的同步,如何在线程间进行通信以及线程的中断机制. 2. 什么是线程 线程是操作系统调度的最小单位,在一个进程中,一般至少有一个线程在运行.一个进程中包含的多个线程,在多核处理器中,操作系统可以将多个线程调度到不同的CPU核心上运行,多个线程可以并行运行. 在同一个进程中的多个线程,共享同一个进程空间

JAVASE02-Unit010: 多线程基础 、 TCP通信

多线程基础 . TCP通信 * 当一个方法被synchronized修饰后,那么 * 该方法称为同步方法,即:多个线程不能同时 * 进入到方法内部执行. package day10; /** * 当多线程并发操作同一资源时,由于线程切换的不确定 * 性,可能导致执行顺序的混乱,严重时可能导致系统 * 瘫痪. * @author adminitartor * */ public class SyncDemo1 { public static void main(String[] args) { f

多线程 基础

进程 什么是进程 进程是指在系统中正在运行的一个应用程序 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内 比如同时打开QQ.Xcode,系统就会分别启动2个进程 通过"活动监视器"可以查看Mac系统中所开启的进程 线程 什么是线程 1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程) 线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行 比如使用酷狗播放音乐.使用迅雷下载电影,都需要在线程中执行 线程的串行 1个线程中任务的执行是串行的 如果

[转]Java多线程干货系列—(一)Java多线程基础

Java多线程干货系列—(一)Java多线程基础 字数7618 阅读1875 评论21 喜欢86 前言 多线程并发编程是Java编程中重要的一块内容,也是面试重点覆盖区域,所以学好多线程并发编程对我们来说极其重要,下面跟我一起开启本次的学习之旅吧. 正文 线程与进程 1 线程:进程中负责程序执行的执行单元线程本身依靠程序进行运行线程是程序中的顺序控制流,只能使用分配给程序的资源和环境 2 进程:执行中的程序一个进程至少包含一个线程 3 单线程:程序中只存在一个线程,实际上主方法就是一个主线程 4

多线程基础(五)

5.多线程基础 线程间通信 什么叫线程间通信 在一个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信 线程间通信的体现 1个线程传递数据给另一个线程 在1个线程中执行完特定任务后, 线程间通信的体现 1个线程传递数据给另1个线程 在1个线程中执行完特定任务后,转到另1个线程继续执行任务 线程间通信常用方法  perform执行 selector选择器 - (void)performSelectorOnMainThread:(SEL)aSelectorwithObject:(id)ar

多线程基础(三)

3.多线程基础 NSThread的基本使用 如何使用NSThread创建新线程 创建线程之后是默认不执行的状态 创建三个线程: 通过name属性区别这些线程 接下来就是通过设置线程的优先级来设置线程的被使用频率的高低      不设置默认是0.5 第二种使用NSThread创建线程,分离出一条线程 没有返回值,就没有办法拿到这个线程对象,就没办法设置相关的属性 第三种方法:开启一条后台线程 优点:不需要手动开启,就是start 总结一下: 自定义线程: 重写dealloc方法,然后在deallo

C#编程总结(二)多线程基础

C#编程总结(二)多线程基础 无论您是为具有单个处理器的计算机还是为具有多个处理器的计算机进行开发,您都希望应用程序为用户提供最好的响应性能,即使应用程序当前正在完成其他工作.要使应用程序能够快速响应用户操作,同时在用户事件之间或者甚至在用户事件期间利用处理器,最强大的方式之一是使用多线程技术. 多线程:线程是程序中一个单一的顺序控制流程.在单个程序中同时运行多个线程完成不同的工作,称为多线程.如果某个线程进行一次长延迟操作, 处理器就切换到另一个线程执行.这样,多个线程的并行(并发)执行隐藏了

Java多线程基础(一)

线程与进程 1 线程:进程中负责程序执行的执行单元线程本身依靠程序进行运行线程是程序中的顺序控制流,只能使用分配给程序的资源和环境 2 进程:执行中的程序一个进程至少包含一个线程 3 单线程:程序中只存在一个线程,实际上主方法就是一个主线程 4 多线程:在一个程序中运行多个任务目的是更好地使用CPU资源 5  在Java语言中,引入对象互斥锁的概念,保证共享数据操作的完整性. 每个对象都对应于一个可称为"互斥锁"的标记,这个标记保证在任一时刻,只能有一个线程访问对象用关键字synchr

Java多线程基础(四)Java传统线程同步通信技术

Java多线程基础(四)Java传统线程同步通信技术 编写代码实现以下功能 子线程循环10次,接着主线程循环100次,接着又回到子线程循环10次,接着再回到主线程又循环100次,如此循环50次. 分析 1)子线程循环10次与主线程循环100次必须是互斥的执行,不能出现交叉,下面代码中通过synchronized关键字实现此要求: 2)子线程与主线程必须交替出现,可以通过线程同步通信技术实现,下面代码中通过bShouldSub变量实现此要求: 其他需要注意的地方 1)其中business变量必须声