【C++】多线程与互斥锁【二】
文章目录
- 1. 并发是什么
- 1.1 并发与并行
- 1.2 硬件并发与任务切换
- 1.3 多线程并发与多进程并发
- 2. 为什么使用并发
- 2.1 为什么使用并发
- 2.2 并发和多线程
- 3. 并发需要注意的问题
- 3.1 多线程中的数据竞争
- 实例1:
- 3.2 如何处理数据竞争?
- 实例2:
- 实例3:
- 注意
- 3.3 C++11新标准多线程支持库
- 3.4 **lock_guard与unique_lock保护共享资源*`**
- 3.4.1 lock_guard:
- 实例4:
- 实例5:
- 3.4.2 unique_lock
- 3.4.3 lock_guard与unique_lock的区别如下:
- 实例6:
- 3.5 [`timed_mutex`](`https://www.cplusplus.com/reference/mutex/timed_mutex/`)与[`recursive_mutex`](https://www.cplusplus.com/reference/mutex/recursive_mutex/)提供更强大的锁
- 实例7:
- 4. 小结
1. 并发是什么
1.1 并发与并行
并发指的是两个或多个独立的活动在同一时段内发生。并发在生活中随处可见:比如在跑步的时候同时听音乐,在看电脑显示器的同时敲击键盘等。
与并发相近的另一个概念是并行。它们两者存在很大的差别,图示如下:
并发:同一时间段内可以交替处理多个操作,强调同一时段内交替发生。
并行:同一时刻内同时处理多个操作,强调同一时刻点同时发生。
1.2 硬件并发与任务切换
既然并发是在同一时间段内交替发生即可,不要求同时发生。单核心处理器上的多任务并发是靠任务切换实现的,跟多核处理器上的并行多任务处理还是有较大区别的,但对处理器的使用和多任务调度工作主要由操作系统完成了,所以我们在两者之间编写应用程序区别倒是不大。下面再贴个直观的图示:
- 双核处理器并行执行(硬件并发)对比单核处理器并发执行(任务上下文切换)
双核处理器均并发执行(一般任务数远大于处理器核心数,多核并发更常见)
1.3 多线程并发与多进程并发
多任务并发,线程与进程,三者的主要区别如下:
任务:从我们认知角度抽象出来的一个概念,放到计算机上主要指由软件完成的一个活动。一个任务既可以是一个进程,也可以是一个线程。简而言之,它指的是一系列共同达到某一目的的操作。例如,读取数据并将数据放入内存中。这个任务可以作为一个进程来实现,也可以作为一个线程(或作为一个中断任务)来实现。
进程:资源分配的基本单位,也可能作为调度运行的单位。可以把一个进程看成是一个独立的程序,在内存中有其完备的数据空间和代码空间。一个进程所拥有的数据和变量只属于它自己。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O设备等。然后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。所以,进程是系统中的并发执行的单位。
详细见【操作系统(二)】
线程:执行处理器调度的基本单位。一个进程由一个或多个线程构成,各线程共享相同的代码和全局数据,但各有其自己的堆栈。由于堆栈是每个线程一个,所以局部变量对每一线程来说是私有的。由于所有线程共享同样的代码和全局数据,它们比进程更紧密,比单独的进程间更趋向于相互作用,线程间的相互作用更容易些,因为它们本身就有某些供通信用的共享内存:进程的全局数据。
详细见【操作系统(三)】
- 多线程并发:在同一时间段内交替处理多个操作,线程切换时间片是很短的(毫秒级),一个时间片多数时候来不及处理完对某一资源的访问;
- 线程间通信:一个任务被分割为多个线程并发处理,多个线程可能都要处理某一共享内存的数据,多个线程对同一共享内存数据的访问需要准确有序。
如果像前一篇文章中的示例,虽然创建了三个线程,但线程间不需要访问共同的内存分区实例,对线程间的执行顺序没有更多要求。但如果多个进程都需要访问相同的共享内存数据,如果都是读取数据还好,如果有读取有写入或者都要写入(数据并发访问或数据竞争),就需要使读写有序(同步化),否则可能会造成数据混乱,得不到我们预期的结果。下面再介绍两个用于理解线程同步的概念:
- 同步:是指在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
- 互斥:是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
多个线程对共享内存数据访问的竞争条件的形成,取决于一个以上线程的相对执行顺序,每个线程都抢着完成自己的任务。C++标准中对数据竞争的定义是:多个线程并发的去修改一个独立对象,数据竞争是未定义行为的起因。
2. 为什么使用并发
2.1 为什么使用并发
使用并发的原因主要有两个:关注点分离和性能**。
- 关注点分离:通过将相关的代码放在一起并将无关的代码分开,可以使你的程序更容易理解和测试,从而减少出错的可能性。你可以使用并发来分隔不同的功能区域,即使在这些不同功能区域的操作需要在同一时刻发生的情况下;若不显式地使用并发,你要么被迫编写任务切换框架,要么在操作中主动地调用不相关的一段代码。
- 更高效的性能:为了充分发挥多核心处理器的优势,使用并发将单个任务分成几部分且各自并行运行,从而降低总运行时间。根据任务分割方式的不同,又可以将其分为两大类:一类是对同样的数据应用不同的处理算法(任务并行);另一类是用同样的处理算法共同处理数据的几部分(数据并行)。本质上,目标就是加速,提高处理性能。
知道何时不使用并发与知道何时使用它一样重要。基本上,不使用并发的唯一原因就是在收益比不上成本的时候。使用并发的代码在很多情况下难以理解,额外的复杂性也可能导致更多的错误。除非潜在的性能增益足够大或关注点分离地足够清晰,能抵消确保其正确所需的额外的开发时间以及与维护多线程代码相关的额外成本,否则不要使用并发。 一般multi task任务数远大于线程数量时候,比如一个线程开启4-8个乃至更多的task时候,操作系统可以更好的调度task的完成,程序粒度更加容易控制,多任务操作开销远小于或者小于性能提升时候,才开始使用,具体的task数量需要根据具体任务测试性能。
2.2 并发和多线程
早期的C++标准中,如1998 C++标准版不承认线程的存在,并且各种语言要素的操作效果都以顺序抽象机的形式编写。内存模型也没有被正式定义,所以对于1998 C++标准,没办法在缺少编译器相关扩展的情况下编写多线程应用程序。如果在之前想使用多线程并发编程,可以借助编译器厂商提供的平台相关的扩展多线程支持API(比如POSIX C和Microsoft Windows API),但这种多线程支持对平台依赖度较高,导致可移植性较差 。
为了解决平台相关多线程API使用上的问题,逐渐开发出了Boost、ACE等平台无关的多线程支持类库。直到C++11标准的发布,借鉴了很多Boost类库的经验,将多线程支持纳入C++标准库。C++11标准不仅提供了一个全新的线程感知内存模型,也包含了用于管理线程、保护共享数据、线程间同步操作以及低级原子操作的各个类。
对于C++整体以及包含低级工具的C++类——特别是在新版C++线程库里的那些,参与高性能计算的开发者常常关注的一点就是效率。如果你正寻求极致的性能,那么理解与直接使用底层的低级工具相比,使用高级工具所带来的实现成本,是很重要的。这个成本就是抽象惩罚(abstraction penalty)。标准C++线程库在设计时,就非常注重高效的性能,提供了足够的低级工具(比如原子操作库),以付出尽可能低的抽象惩罚。C++标准库也提供了更高级别的抽象和工具,它们使得编写多线程代码更简单和不易出错。有时候运用这些工具确实会带来性能成本,因为必须执行额外的代码。但是这种性能成本并不一定意味着更高的抽象惩罚;总体来看,这种性能成本并不比通过手工编写等效的函数而招致的成本更高,同时编译器可能会很好地内联大部分额外的代码。
3. 并发需要注意的问题
3.1 多线程中的数据竞争
一个多线程C++程序是什么样子的?它看上去和其他所有C++程序一样,通常是变量、类以及函数的组合。唯一真正的区别在于某些函数可以并发运行,所以你需要确保共享数据的并发访问是安全的。当然,为了并发地运行函数,必须使用特定的函数以及对象来管理各个线程。
多线程编程在许多领域是不可或缺的。但是,多线程并行,非常容易引发数据竞争,而且还非常不容易被发现和debug。下面,我们用C++语言来演示一下,什么是数据竞争:
实例1:
#include <iostream>
#include <stdlib.h>
#include <thread>
#include <string>#define COUNT 1000
volatile int num = 0;void thread1()
{for (int i=0; i<COUNT; i++){num++;}
}void thread2()
{for (int i=0; i<COUNT; i++){num--;}
}int main(int argc, char* argv[])
{std::thread t1(thread1);std::thread t2(thread2);t1.join();t2.join();std::cout << "final value:" << num << std::endl;getchar();return 0;
}
如果说没有数据竞争(data race)的话,这两个线程执行完毕后,数据最后一定是回到初始值0。然而,我们尝试运行后发现,事与愿违,每次执行的结果都不是0,而且每次的结果都不一样。
下面这种是正确的写法:
std::thread t1(thread1);t1.join();std::thread t2(thread2);t2.join();
为什么会发生这样的现象呢?因为为普通变量加1减1这样的操作并非“原子”操作。我们简化一下这个过程,它可以分为三个步骤,读数据,执行计算,写数据。理想情况下,我们期望的执行流程应该是这样的:
然而,线程的调度是不受我们控制的,即便线程1和线程2内部的执行流程不变,只要调度时机发生了变化,结果也会不同,比如说,实际的执行过程中,有可能是这样的情况:
随着调度情况的不同,最终的结果也会有所差异,所以我们可以看到,这个程序的执行结果不是0,而且循环次数越多,发生数据竞争的机会也越大。
3.2 如何处理数据竞争?
从数据竞争形成的条件入手,数据竞争源于并发修改同一数据结构,那么最简单的处理数据竞争的方法就是对该数据结构采用某种保护机制,确保只有进行修改的线程才能看到数据被修改的中间状态,从其他访问线程的角度看,修改不是已经完成就是还未开始。C++标准库提供了很多类似的机制,最基本的就是互斥量,有一个< mutex >库文件专门支持对共享数据结构的互斥访问。
mutex
类是能用于保护共享数据免受从多个线程同时访问的同步原语。
mutex
提供排他性非递归所有权语义:
- 调用方线程从它成功调用
lock
或try_lock
开始,到它调用unlock
为止占有mutex
。 - 线程占有
mutex
时,所有其他线程若试图要求mutex
的所有权,则将阻塞(对于lock
的调用)或收到 false 返回值(对于try_lock
). - 调用方线程在调用
lock
或try_lock
前必须不占有mutex
。
若 mutex
在仍为任何线程所占有时即被销毁,或在占有 mutex
时线程终止,则行为未定义。 mutex
类满足互斥体 (Mutex) 和标准布局类型 (StandardLayoutType) 的全部要求。
std::mutex
既不可复制亦不可移动。
互斥锁类型是*可锁定的类型,用于保护对代码关键部分的访问:锁定互斥锁可防止其他线程锁定它(独占访问),直到被解锁*为止:互斥体, recursive_mutex, timed_mutex, recursive_timed_mutex。
锁是通过将互斥锁与自己的生命周期相关联的访问来管理互斥锁的对象:lock_guard, unique_lock。
同时锁定多个互斥锁的功能(try_lock, 锁)并直接阻止并发执行特定功能(call_once)。
Mutex全名mutual exclusion(互斥体),是个object对象,用来协助采取独占排他方式控制对资源的并发访问。这里的资源可能是个对象,或多个对象的组合。**为了获得独占式的资源访问能力,相应的线程必须锁定(lock) mutex,这样可以防止其他线程也锁定mutex,直到第一个线程解锁(unlock) mutex。**mutex类的主要操作函数见下表:
实例2:
操作 | 效果作用 | |
---|---|---|
1 | mutex | 构造函数,建立一个未锁定的(unlocked)mutex |
2 | m.~mutex | 销毁mutex,它必须未被锁定 |
3 | m.lock() | 尝试锁住mutex,它会造成阻塞 |
5 | m.try_lock() | 尝试锁住mutex,锁定成功返回true |
6 | m.try_lock_for(dur) | 尝试在时间段dur内锁定,锁定成功返回True |
7 | m.try_lock_until(tp) | 尝试在时间点tp之前锁定,锁定成功返回True |
8 | m.unlock() | 解决mutex。如果它未曾被锁定则行为不明确 |
9 | m.native_handle() | 返回一个因平台而异的类型native_handle_type,为了不具有可移植性的拓展 |
// mutex1.cpp 通过互斥体lock与unlock保护共享全局变量#include <chrono>
#include <mutex>
#include <thread>
#include <iostream> std::chrono::milliseconds interval(100);std::mutex mutex;
int job_shared = 0; //两个线程都能修改'job_shared',mutex将保护此变量
int job_exclusive = 0; //只有一个线程能修改'job_exclusive',不需要保护//此线程只能修改 'job_shared'
void job_1()
{mutex.lock();std::this_thread::sleep_for(5 * interval); //令‘job_1’持锁等待++job_shared;std::cout << "job_1 shared (" << job_shared << ")\n";mutex.unlock();
}
// 此线程能修改'job_shared'和'job_exclusive'
void job_2()
{while (true) { //无限循环,直到获得锁并修改'job_shared'if (mutex.try_lock()) { //尝试获得锁成功则修改'job_shared'++job_shared;std::cout << "job_2 shared (" << job_shared << ")\n";mutex.unlock();return;} else { //尝试获得锁失败,接着修改'job_exclusive'++job_exclusive;std::cout << "job_2 exclusive (" << job_exclusive << ")\n";std::this_thread::sleep_for(interval);}}
}
int main()
{std::thread thread_1(job_1);std::thread thread_2(job_2); thread_1.join();thread_2.join();getchar();return 0;
}
实例3:
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream> std::mutex mtx; // mutex for critical sectionvoid print_block(int n, char c) {// critical section (exclusive access to std::cout signaled by locking mtx):mtx.lock();for (int i = 0; i<n; ++i) { std::cout << c; }std::cout << '\n';mtx.unlock();
}int main()
{std::thread th1(print_block, 50, '*');std::thread th2(print_block, 50, '$');th1.join();th2.join();getchar();return 0;
}
从上代码看,创建了两个线程和两个全局变量,其中一个全局变量job_exclusive是排他的,两线程并不共享,不会产生数据竞争,所以不需要锁保护。另一个全局变量job_shared是两线程共享的,处于可读写的状态,会引起数据竞争,因此需要锁保护。线程thread_1持有互斥锁lock的时间较长,线程thread_2为免于空闲等待,使用了尝试锁try_lock,如果获得互斥锁则操作共享变量job_shared,未获得互斥锁则操作排他变量job_exclusive,提高多线程效率。
可以看出持有锁的线程可以对竞争的数据进行操作,所以,锁的含义像一个令牌,持有令牌者,具有优先权限,其余线程需要等待令牌转移,也即解锁操作unlock。
注意
通常不直接使用 std::mutex
: std::unique_lock 、 std::lock_guard 或 std::scoped_lock (C++17 起)以更加异常安全的方式管理锁定。
3.3 C++11新标准多线程支持库
- < thread > : 提供线程创建及管理的函数或类接口;
- < mutex > : 为线程提供获得独占式资源访问能力的互斥算法,保证多个线程对共享资源的同步访问;
- < condition_variable > : 允许一定量的线程等待(可以定时)被另一线程唤醒,然后再继续执行;
- < future > : 提供了一些工具来获取异步任务(即在单独的线程中启动的函数)的返回值,并捕捉其所抛出的异常;
- < atomic > : 为细粒度的原子操作(不能被处理器拆分处理的操作)提供组件,允许无锁并发编程。
3.4 lock_guard与unique_lock保护共享资源*`
lock与unlock必须成对合理配合使用,使用不当可能会造成资源被永远锁住,甚至出现死锁(两个线程在释放它们自己的lock之前彼此等待对方的lock)。是不是想起了C++另一对儿需要配合使用的对象new与delete,若使用不当可能会造成内存泄漏等严重问题,为此C++引入了智能指针shared_ptr
与unique_ptr
。智能指针借用了RAII技术(Resource Acquisition Is Initialization
—使用类来封装资源的分配和初始化,在构造函数中完成资源的分配和初始化,在析构函数中完成资源的清理,可以保证正确的初始化和资源释放)对普通指针进行封装,达到智能管理动态内存释放的效果。同样的,C++也针对lock
与unlock
引入了智能锁lock_guard
与unique_lock
,同样使用了RAII技术对普通锁进行封装,达到智能管理互斥锁资源释放的效果。
3.4.1 lock_guard:
类 lock_guard
是互斥体包装器,为在作用域块期间占有互斥提供便利 RAII 风格机制。
创建 lock_guard
对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard
对象的作用域时,销毁 lock_guard
并释放互斥。
lock_guard
类不可复制。
实例4:
操作 | 作用 | |
---|---|---|
1 | lock_guard lg(m) | 为mutex m建立一个lock guard并锁定之 |
2 | lock_guard lg(m,adopt_lock) | 为已经被锁定的mutex m建立一个lock guard |
3 | lg.~lock_guard() | 解锁(unlock)mutex并销毁lock_guard |
// lock_guard example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::lock_guard
#include <stdexcept> // std::logic_errorstd::mutex mtx;void print_even(int x) {if (x % 2 == 0) std::cout << x << " is even\n";else throw (std::logic_error("not even"));
}void print_thread_id(int id) {try {// using a local lock_guard to lock mtx guarantees unlocking on destruction / exception://使用本地lock_guard锁定mtx可以保证在销毁/异常时解锁:std::lock_guard<std::mutex> lck(mtx);print_even(id);}catch (std::logic_error&) {std::cout << "[exception caught]\n";}
}int main()
{std::thread threads[10];// spawn 10 threads:for (int i = 0; i<10; ++i)threads[i] = std::thread(print_thread_id, i + 1);for (auto& th : threads) th.join();getchar();return 0;
}
实例5:
#include <thread>
#include <mutex>
#include <iostream>int g_i = 0;
std::mutex g_i_mutex; // 保护 g_ivoid safe_increment()
{std::lock_guard<std::mutex> lock(g_i_mutex);++g_i;std::cout << std::this_thread::get_id() << ": " << g_i << '\n';// g_i_mutex 在锁离开作用域时自动释放
}int main()
{std::cout << "main: " << g_i << '\n';std::thread t1(safe_increment);std::thread t2(safe_increment);t1.join();t2.join();std::cout << "main: " << g_i << '\n';getchar();
}
采用RALL风格:使用类类封装资源的分配和初始化, 控制离开创建 lock_guard
对象t1
的作用域时,销毁 lock_guard
并释放互斥(持有lock锁也即令牌者,拥有对资源的权限)。
3.4.2 unique_lock
类 unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。
类 unique_lock 可移动,但不可复制——它满足可移动构造 (MoveConstructible) 和可移动赋值 (MoveAssignable) 但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 。
类 unique_lock 满足基本可锁定 (BasicLockable) 要求。若 Mutex 满足可锁定 (Lockable) 要求,则 unique_lock 亦满足可锁定 (Lockable) 要求(例如:能用于 std::lock ) ;若 Mutex 满足可定时锁定 (TimedLockable) 要求,则 unique_lock 亦满足可定时锁定 (TimedLockable) 要求。
3.4.3 lock_guard与unique_lock的区别如下:
从上面两个支持的操作函数表对比来看,unique_lock功能丰富灵活得多。如果需要实现更复杂的锁策略可以用unique_lock,如果只需要基本的锁功能,优先使用更严格高效的lock_guard。两种锁的概述与策略对比如下:
类模板 | 描述 | 策略 |
---|---|---|
std::lock_guard | 严格基于作用域(scope-based)的锁管理类模板,构造时是否加锁是可选的(不加锁时假定当前线程已经获得锁的所有权—使用std::adopt_lock策略),析构时自动释放锁,所有权不可转移,对象生存期内不允许手动加锁和释放锁 | std::adopt_lock |
std::unique_lock | 更加灵活的锁管理类模板,构造时是否加锁是可选的,在对象析构时如果持有锁会自动释放锁,所有权可以转移。对象生命期内允许手动加锁和释放锁 | std::adopt_lock std::defer_lock std::try_to_lock |
实例6:
如果将上面实例2中针对job_1函数,如果普通锁lock/unlock替换为智能锁lock_guard,针对job_2的尝试锁try_lock也使用智能锁替代,由于**lock_guard锁策略不支持尝试锁,只好使用unique_lock来替代,**代码修改如下:
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream> std::chrono::milliseconds interval(100);std::mutex mutex;
int job_shared = 0; //两个线程都能修改'job_shared',mutex将保护此变量
int job_exclusive = 0; //只有一个线程能修改'job_exclusive',不需要保护//此线程只能修改 'job_shared'
void job_1()
{mutex.lock();std::this_thread::sleep_for(5 * interval); //令‘job_1’持锁等待++job_shared;std::cout << "job_1 shared (" << job_shared << ")\n";mutex.unlock();
}
void job_11()
{std::lock_guard<std::mutex> lockg(mutex); //获取RAII智能锁,离开作用域会自动析构解锁std::this_thread::sleep_for(5 * interval); //令‘job_1’持锁等待++job_shared;std::cout << "job_1 shared (" << job_shared << ")\n";
}
// 此线程能修改'job_shared'和'job_exclusive'
void job_2()
{while (true) { //无限循环,直到获得锁并修改'job_shared'if (mutex.try_lock()) { //尝试获得锁成功则修改'job_shared'++job_shared;std::cout << "job_2 shared (" << job_shared << ")\n";mutex.unlock();return;}else { //尝试获得锁失败,接着修改'job_exclusive'++job_exclusive;std::cout << "job_2 exclusive (" << job_exclusive << ")\n";std::this_thread::sleep_for(interval);}}
}
void job_22()
{while (true) { //无限循环,直到获得锁并修改'job_shared'std::unique_lock<std::mutex> ulock(mutex, std::try_to_lock);//以尝试锁策略创建智能锁//尝试获得锁成功则修改'job_shared'if (ulock) {++job_shared;std::cout << "job_2 shared (" << job_shared << ")\n";return;}else { //尝试获得锁失败,接着修改'job_exclusive'++job_exclusive;std::cout << "job_2 exclusive (" << job_exclusive << ")\n";std::this_thread::sleep_for(interval);}}
}
int main()
{std::thread thread_1(job_11);std::thread thread_2(job_22);thread_1.join();thread_2.join();getchar();return 0;
}
3.5 timed_mutex
与recursive_mutex
提供更强大的锁
3.3介绍的互斥量mutex提供了普通锁lock/unlock和3.4介绍了智能锁lock_guard/unique_lock,基本能满足我们大多数对共享数据资源的保护需求。但在某些特殊情况下,我们需要更复杂的功能,比如某个线程中函数的嵌套调用可能带来对某共享资源的嵌套锁定需求,mutex在一个线程中却只能锁定一次;再比如我们想获得一个锁,但不想一直阻塞,只想等待特定长度的时间,mutex也没提供可设定时间的锁。针对这些特殊需求,< mutex >库也提供了下面几种功能更丰富的互斥类,它们间的区别见下表:
类模板 | 描述 |
---|---|
std::mutex | 同一时间只可被一个线程锁定。如果它被锁住,任何其他lock()都会阻塞(block),直到这个mutex再次可用,且try_lock()会失败。 |
std::recursive_mutex | 允许在同一时间多次被同一线程获得其lock。其典型应用是:函数捕获一个lock并调用另一函数而后者再次捕获相同的lock。 |
std::timed_mutex | 额外允许你传递一个时间段或时间点,用来定义多长时间内它可以尝试捕获一个lock。为此它提供了try_lock_for(duration)和try_lock_until(timepoint)。 |
std::recursive_timed_mutex | 允许同一线程多次取得其lock,且可指定期限。 |
详情见3.5链接。
实例7:
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream> std::chrono::milliseconds interval(100);std::timed_mutex tmutex;int job_shared = 0; //两个线程都能修改'job_shared',mutex将保护此变量
int job_exclusive = 0; //只有一个线程能修改'job_exclusive',不需要保护//此线程只能修改 'job_shared'
void job_13()
{std::lock_guard<std::timed_mutex> lockg(tmutex); //获取RAII智能锁,离开作用域会自动析构解锁std::this_thread::sleep_for(5 * interval); //令‘job_1’持锁等待++job_shared;std::cout << "job_1 shared (" << job_shared << ")\n";
}// 此线程能修改'job_shared'和'job_exclusive'
void job_23()
{while (true) { //无限循环,直到获得锁并修改'job_shared'std::unique_lock<std::timed_mutex> ulock(tmutex, std::defer_lock); //创建一个智能锁但先不锁定//尝试获得锁成功则修改'job_shared'if (ulock.try_lock_for(3 * interval)) { //在3个interval时间段内尝试获得锁++job_shared;std::cout << "job_2 shared (" << job_shared << ")\n";return;}else { //尝试获得锁失败,接着修改'job_exclusive'++job_exclusive;std::cout << "job_2 exclusive (" << job_exclusive << ")\n";std::this_thread::sleep_for(interval);}}
}int main()
{std::thread thread_1(job_13);std::thread thread_2(job_23);thread_1.join();thread_2.join();getchar();return 0;
}
4. 小结
本文由谈到并发引发的数据竞争,以及数据竞争的初始解决方法,mutex互斥锁解决方法(3.2),以及更高级的基于RALL的lock_guard与unique_lock智能锁(3.4)(原因,防止mutex使用不当,造成死锁)。3.5介绍一种更强大的锁,用于线程中函数嵌套带来的共享资源的嵌套锁定需求,以及其它更复杂的情况。
//thread2.cpp 增加对cout显示终端资源并发访问的互斥锁保护#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>using namespace std;std::mutex mutex1;void thread_function(int n)
{std::thread::id this_id = std::this_thread::get_id(); //获取线程IDfor (int i = 0; i < 5; i++) {mutex1.lock();cout << "Child function thread " << this_id << " running : " << i + 1 << endl;mutex1.unlock();std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒}
}class Thread_functor
{
public:// functor行为类似函数,C++中的仿函数是通过在类中重载()运算符实现,使你可以像使用函数一样来创建类的对象void operator()(int n){std::thread::id this_id = std::this_thread::get_id();for (int i = 0; i < 5; i++) {{std::lock_guard<std::mutex> lockg(mutex1);cout << "Child functor thread " << this_id << " running: " << i + 1 << endl;}std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒}}
};int main()
{thread mythread1(thread_function, 1); // 传递初始函数作为线程的参数if (mythread1.joinable()) //判断是否可以成功使用join()或者detach(),返回true则可以,返回false则不可以mythread1.join(); // 使用join()函数阻塞主线程直至子线程执行完毕Thread_functor thread_functor;thread mythread2(thread_functor, 3); // 传递初始函数作为线程的参数if (mythread2.joinable())mythread2.detach(); // 使用detach()函数让子线程和主线程并行运行,主线程也不再等待子线程auto thread_lambda = [](int n) {std::thread::id this_id = std::this_thread::get_id();for (int i = 0; i < 5; i++){mutex1.lock();cout << "Child lambda thread " << this_id << " running: " << i + 1 << endl;mutex1.unlock();std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒}};thread mythread3(thread_lambda, 4); // 传递初始函数作为线程的参数if (mythread3.joinable())mythread3.join(); // 使用join()函数阻塞主线程直至子线程执行完毕unsigned int n = std::thread::hardware_concurrency(); //获取可用的硬件并发核心数mutex1.lock();std::cout << n << " concurrent threads are supported." << endl;mutex1.unlock();std::thread::id this_id = std::this_thread::get_id();for (int i = 0; i < 5; i++) {{std::lock_guard<std::mutex> lockg(mutex1);cout << "Main thread " << this_id << " running: " << i + 1 << endl;}std::this_thread::sleep_for(std::chrono::seconds(1));}getchar();return 0;
}
参考资料:
https://zh.cppreference.com/w/cpp/thread/unique_lock
https://www.tutorialspoint.com/cpp_standard_library/memory.htm
https://www.cplusplus.com/reference/mutex/lock_guard/
相关文章:

用hosting.json配置ASP.NET Core站点的Hosting环境
通常我们在 Prgram.cs 中使用硬编码的方式配置 ASP.NET Core 站点的 Hosting 环境,最常用的就是 .UseUrls() 。 public class Program {public static void Main(string[] args){var host new WebHostBuilder().UseUrls("http://*:5000").UseKestrel().U…

鼠标按键获取感兴趣区域 2
#include <opencv2/imgproc/imgproc.hpp> #include <opencv2/highgui/highgui.hpp> #include <iostream> #include <stdio.h> using namespace cv; using namespace std; // 全局变量图像源 cv::Mat srcImage; // 所选矩形区域 cv::Rect roiRect; …

偷看日历?9款 APP 涉嫌过度获取权限
最近网友已经看累了APP搞事的瓜,3月29日,“9款 APP 涉嫌过度获取权限”又上了热搜。 不久前上海消保委针对网购平台、旅游出行、生活服务等39款手机APP进行了涉及个人信息权限的评测,主要包括四个方面:App所使用的目标API级别、A…

【C++】多线程与条件变量【三】
文章目录1 条件变量是什么?实例1:2 条件变量本质?3 引入条件变量的原因?实例2:实例3:实例4:4 如何使用条件变量?4.1 std::condition_variable实例5:4.2 std::condition_v…

图像遍历反色处理,遍历多通道图片
#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <iostream> using namespace cv; // 下标M.at<float>(i,j) 方法1-1 cv::Mat inverseColor1(cv::Mat srcImage) {cv::Mat tempImage srcImage.clone();int row t…

【Treap】bzoj1588-HNOI2002营业额统计
一、题目 Description 营业额统计 Tiger最近被公司升任为营业部经理,他上任后接受公司交给的第一项任务便是统计并分析公司成立以来的营业情况。 Tiger拿出了公司的账本,账本上记录了公司成立以来每天的营业额。分析营业情况是一项相当复杂的工作。由于节…

推荐一款 Flutter Push 推送功能插件
又到了推荐好插件的时候了。开发 APP 避免不了使用「推送」功能。比如,新上架一个商品,或者最新的一条体育新闻,实时推送给用户。 比较了几家推送平台,貌似「极光」出了 Flutter 插件,所以就拿它试试手,顺便…

【C++】多线程与异步编程【四】
文章目录【C】多线程与异步编程【四】0.三问1.什么是异步编程?1.1同步与异步1.2 **阻塞与非阻塞**2、如何使用异步编程2.1 使用全局变量与条件变量传递结果实例1:2.2 使用promise与future传递结果实例2实例32.3使用packaged_task与future传递结果实例42.…

[LintCode] Maximum Subarray 最大子数组
Given an array of integers, find a contiguous subarray which has the largest sum. Notice The subarray should contain at least one number. Have you met this question in a real interview? YesExample Given the array [−2,2,−3,4,−1,2,1,−5,3], the contiguo…

图像补运算:反色处理
cv::Mat inverseColor1(cv::Mat srcImage) {cv::Mat tempImage srcImage.clone();int row tempImage.rows;int col tempImage.cols;// 对各个像素点遍历进行取反for (int i 0; i < row; i){for (int j 0; j < col; j){// 分别对各个通道进行反色处理tempImage.at<…

2018-2019-2 网络对抗技术 20165239Exp3 免杀原理与实践
2018-2019-2 网络对抗技术 20165239 Exp3 免杀原理与实践 win10 ip地址 192.168.18.1 fenix ip地址为 192.168.18.128 (1)杀软是如何检测出恶意代码的? •根据计算机病毒课程知道了每个病毒都有其对应的特征码,杀软是根据这些特征…

【C++】多线程与原子操作和无锁编程【五】
【C】多线程与原子操作和无锁编程【五】 1、何为原子操作 前面介绍了多线程间是通过互斥锁与条件变量来保证共享数据的同步的,互斥锁主要是针对过程加锁来实现对共享资源的排他性访问。很多时候,对共享资源的访问主要是对某一数据结构的读写操作&#…

jquery中ajax的dataType属性包括哪几项
参考ajax api文档:http://www.w3school.com.cn/jquery/ajax_ajax.asp dataType类型:String预期服务器返回的数据类型。如果不指定,jQuery 将自动根据 HTTP 包 MIME 信息来智能判断,比如 XML MIME 类型就被识别为 XML。在 1.4 中&a…

图像补运算:ptr反色处理
cv::Mat inverseColor3(cv::Mat srcImage) {cv::Mat tempImage srcImage.clone();int row tempImage.rows;// 将3通道转换为单通道int nStep tempImage.cols * tempImage.channels();for(int i 0; i < row; i) {// 取源图像的指针const uchar* pSrcData srcImage.ptr&l…

Android 在运行时请求权限
2019独角兽企业重金招聘Python工程师标准>>> 从 Android 6.0(API 级别 23)开始,用户开始在应用运行时向其授予权限,而不是在应用安装时授予。此方法可以简化应用安装过程,因为用户在安装或更新应用时不需要…

Markdown解决图片存储问题
文章目录Markdown1.前言2.图片引用方式方式1:可以任意比例放缩图片方式2:原比例引用图片3.推荐公式编辑器4.此外简单介绍下Markdown的一种轻量化工具Typora的使用方法。Markdown 1.前言 相信大家在使用Typora,经常会遇到图片编辑的问题&…

jenkins添加git源码目录时报Error performing command错误
简介 这是我在构建一个自动化部署项目中遇到的一个异常 解决步骤: 1、进入的jenkins的home目录,执行下面命令生成公钥和私钥 [rootjacky .jenkins]# ssh-keygen -t dsa 2、查看生成的公钥 [rootjacky .ssh]# cat /root/.ssh/id_dsa.pub ssh-dss AAAAB3Nz…

图像补运算:MatIterator_迭代器反色处理
#include <opencv2/opencv.hpp>#include <opencv2/video/background_segm.hpp>// 注意srcImage为3通道的彩色图片 cv::Mat inverseColor4(cv::Mat &srcImage) {cv::Mat tempImage srcImage.clone();// 初始化源图像迭代器 cv::MatConstIterator_<cv::Vec3…

浅谈同一家公司多个系统,共用登录用户名和密码
主要解决系统使用的加密方式不一致的问题, 比如几年前的系统A, 某某牵头无中生有的系统B 原先A用的php语言开发,比如叫做tap,是国外用来做项目管理的一款BS平台,(和国内发禅道类似,省略***&…

Eigen/Matlab 使用小结
文章目录[Eigen Matlab使用小结](https://www.cnblogs.com/rainbow70626/p/8819119.html)Eigen初始化0.[官网资料](http://eigen.tuxfamily.org/index.php?titleMain_Page)1. Eigen Matlab矩阵定义2. Eigen Matlab基础使用3. Eigen Matlab特殊矩阵生成4. Eigen Matlab矩阵分块…

GitHUb 代码提交遇到的问题以及解决办法
git 添加代码出现以下错误: fatal: Unable to create F:/wamp/www/ThinkPhpStudy/.git/index.lock: File exists. If no other git process is currently running, this probably means a git process crashed in this repository earlier. Make sure no other git …

isContinuous 反色处理
cv::Mat inverseColor5(cv::Mat srcImage) {int row srcImage.rows;int col srcImage.cols;cv::Mat tempImage srcImage.clone();// 判断是否是连续图像,即是否有像素填充if( srcImage.isContinuous() && tempImage.isContinuous() ){row 1;// 按照行展…

阿里云智能对话分析服务
2019独角兽企业重金招聘Python工程师标准>>> 关于智能对话分析服务 智能对话分析服务 (Smart Conversation Analysis) 依托于阿里云语音识别和自然语言分析技术,为企业用户提供智能的对话分析服务,支持语音和文本数据的接入。可用于电话/在线…

【Smooth】非线性优化
文章目录非线性优化0 .case实战0.1求解思路0.2 g2o求解1. 状态估计问题1.1 最大后验与最大似然1.2 最小二乘的引出2. 非线性最小二乘2.1 一阶和二阶梯度法2.2 高斯牛顿法2.2 列文伯格-马夸尔特方法(阻尼牛顿法)3 Ceres库的使用4 g2o库的使用非线性优化 0 .case实战…

.net 基于Jenkins的自动构建系统开发
先让我给描述一下怎么叫一个自动构建或者说是持续集成 : 就拿一个B/S系统的合作开发来说,在用SVN版本控制的情况下,每个人完成自己代码的编写,阶段性提交代码,然后测试-修改,最后到所有代码完工,…

LUT 查表反色处理
cv::Mat inverseColor6(cv::Mat srcImage) {int row srcImage.rows;int col srcImage.cols;cv::Mat tempImage srcImage.clone();// 建立LUT 反色tableuchar LutTable[256];for (int i 0; i < 256; i)LutTable[i] 255 - i;cv::Mat lookUpTable(1, 256, CV_8U);uchar* p…

个人怎么发表期刊具体细节
目前在国内期刊发表,似乎已经成为非常普遍的一种现象,当然普通期刊发表的人数是比较多的,但是同样也有很多人选择核心期刊进行发表在众多期刊当中核心期刊,绝对是比较高级的刊物。很多人都想了解个人怎么发表期刊,那么…

【Math】P=NP问题
文章目录**P vs NP****0 PNP基本定义**0.1 Definition of NP-Completeness0.2 NP-Complete Problems0.3 NP-Hard Problems0.4 TSP is NP-Complete0.5 Proof**1 PNP问题****2 千禧年世纪难题****3 P类和NP类问题特征****4 多项式时间****5 现实中的NP类问题****6 大突破之NPC问题…
窥探react事件
写在前面 本文源于本人在学习react过程中遇到的一个问题;本文内容为本人的一些的理解,如有不对的地方,还请大家指出来。本文是讲react的事件,不是介绍其api,而是猜想一下react合成事件的实现方式 遇到的问题 class Eve…

Python内置方法
一、常用的内置方法 1、__new__ 和 __init__: __new__ 构造方法 、__init__初始化函数1、__new__方法是真正的类构造方法,用于产生实例化对象(空属性)。重写__new__方法可以控制对象的产 生过程。也就是说会通过继承object的new方…