C++ 使用多线程时,有多种方案可供选择。比如 POSIX 线程pthread、boost::thread 库、C++11开始支持的 std::thread 库,以及其他一些第三方库 libdispatch(GCD)和 OpenMP 等,需要根据实际项目、运行平台、团队协作等因素来考虑。一般而言,如果使用的是 Linux 操作系统,那么可以直接使用系统提供的 pthread 库编写多线程 C++ 程序;如果需要跨平台,则推荐使用 C++ 标准的 std::thread 库。
一、pthread
基于 POSIX 开发多线程程序需要包含头文件 <pthread.h>。
pthread 提供了一个 pthread_t类型用来表示一个线程。由于 pthread 库不是 Linux系统默认的库,因此编译时需要加上-lpthread 选项以链接pthread 库
g++ main.cpp -lpthread
pthread库提供了一系列 API 用于操作线程,常用的接口函数原型如下所示。
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);//创建线程
void pthread_exit(void *retval);//终止线程
int pthread_cancel(pthread_t thread);// 线程连接和分离
int pthread_join(pthread_t thread, void **retval);
int pthread_detach(pthread_t thread);
使用 pthread 接口实现多线程。
#include <iostream>
#include <pthread.h>
using namespace std;
//定义线程数量
#define NUM_THREADS 10
void *thread_entry(void* args){
// 对传入的参数进行强制类型转换
// 由无类型指针变为整形数指针,然后再读取
int tid = *(int *)args;
cout << tid << ": Hello thread!" << endl;
pthread_exit(NULL);
}
int main(void){
pthread_t tids[NUM_THREADS];
int index[NUM_THREADS];
for (int i=0; i<NUM_THREADS; i++){
index[i] = i;
//创建线程
pthread_create(&tids[i],NULL,thread_entry, (void *)&index[i]);
}
//等待线程完成
for (int i=0; i<NUM_THREADS; i++){
pthread_join(tids[i], NULL);
}
pthread_exit(NULL);
return 0;
}
执行下面命令编译程序,注意需要添加 -pthread 选项以链接 pthread 库。
g++ main.cpp -pthread
运行结果:
0: Hello thread!
5: Hello thread!
1: Hello thread!
9: Hello thread!
8: Hello thread!
2: Hello thread!
7: Hello thread!
4: Hello thread!
6: Hello thread!
3: Hello thread!
你看到的顺序和这里的结果可能不一样,不过没关系,这正是多线程运行的效果。
二、thread
C++11 中加入了 <thread> 头文件,此头文件主要声明了 std::thread 线程类。thread 类对线程进行了封装,定义了一些表示线程的类、用于互斥访问的类与方法等。
查看 C++ Reference 手册,std::thread 类有以下成员
其中,成员属性说明如下:
thread::id 表示线程 ID,定义了在运行时操作系统内唯一能够标识该线程的标识符,同时其值还能指示所标识的线程的状态。
native_handle_type是连接thread类和操作系统SDK API之间的桥梁,如在 Linux g++(libstdc++)里,native_handle_type其实就是pthread里面的 pthread_t类型。
成员函数的说明如下:
get_id:获取线程 ID,返回一个类型为thread::id 的对象。
joinable:检查线程是否可被join。检查thread对象是否标识一个活动(active)的可行性线程。缺省构造的 thread 对象、已经完成 join 的 thread 对象、已经 detach 的 thread 对象都不是 joinable 的。
join:调用该函数会阻塞当前线程。阻塞调用者(caller)所在的线程直至被 join 的 std::thread 对象标识的线程执行结束。
detach:将当前线程对象所代表的执行实例与该线程对象分离,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放。
swap:交换两个线程对象所代表的底层句柄。
native_handle:该函数返回与 std::thread 具体实现相关的线程句柄。当 thread 类的功能不能满足我们的要求的时候(比如改变某个线程的优先级),可以通过 thread 类实例的 native_handle() 返回值作为参数来调用相关的 pthread 函数达到目的。
hardware_concurrency:静态成员函数,返回当前计算机最大的硬件并发线程数目。基本上可以视为处理器的核心数目。
#include <iostream> // std::cout
#include <thread> // std::thread
using namespace std;
void foo() {
cout << "1" << endl;
}
void bar(int x) {
cout << "2" << endl;
}
int main() {
thread first(foo); // spawn new thread that calls foo()
thread second(bar, 0); // spawn new thread that calls bar(0)
cout << "main, foo and bar now execute concurrently...\n";
// synchronize threads:
first.join(); // pauses until first finishes
second.join(); // pauses until second finishes
cout << "foo and bar completed.\n";
return 0;
}
输出:
使用thread 创建多个线程并传递参数
#include <iostream>
#include <thread>
using namespace std;
static const int nt=10;
void Hello(int num)
{
cout<<num<<": Hello thread!" << endl;
}
int main(void)
{
thread t[nt];
// 创建线程
for (int i=0; i<nt; i++) {
t[i] = thread(Hello, i);
}
// 等待线程完成
for (int i=0; i<nt; i++) {
t[i].join();
}
return 0;
}
可以看到,使用thread创建线程、传递参数,比使用pthread库接口方便多了!
执行g++ main.cpp -pthread && ./a.out编译运行以上程序,输出结果如下:
0: Hello thread!
4: Hello thread!
3: Hello thread!
5: Hello thread!
6: Hello thread!
7: Hello thread!
8: Hello thread!
9: Hello thread!
2: Hello thread!
1: Hello thread!
上述例子使用 join() 等待子线程结束,也可以使用 detach() 不等待子线程。
join() 表示主线程需要等待子线程结束方可执行下一步(串行),而 detach() 则表示让子线程放飞自我,独立于主线程并发执行,主线程后续代码段无需等待。
使用detach实现多线程并发
#include <iostream>
#include <thread>
using namespace std;
static const int nt=10;
void Hello(int num){
cout << num << ": Hello thread!" << endl;
}
int main(void){
thread t[nt];
for (int i=0; i<nt; i++) {
t[i] = thread(Hello, i);
t[i].detach();
}
cout << "Main thread exit." << endl;
return 0;
}
执行g++ main.cpp -pthread && ./a.out 编译运行以上程序,输出结果如下:
0: Hello thread!
1: Hello thread!
3: Hello thread!
2: Hello thread!
4: Hello thread!
6: Hello thread!
8: Hello thread!
Main thread exit.
9: Hello thread!
可以看到,主线程比子线程先退出了。
什么是C++多线程?
线程:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,进程包含一个或者多个线程。进程可以理解为完成一件事的完整解决方案,而线程可以理解为这个解决方案中的的一个步骤,可能这个解决方案就这只有一个步骤,也可能这个解决方案有多个步骤。
多线程:多线程是实现并发(并行)的手段,并发(并行)即多个线程同时执行,一般而言,多线程就是把执行一件事情的完整步骤拆分为多个子步骤,然后使得这多个步骤同时执行。
C++多线程:(简单情况下)C++多线程使用多个函数实现各自功能,然后将不同函数生成不同线程,并同时执行这些线程(不同线程可能存在一定程度的执行先后顺序,但总体上可以看做同时执行)。
上述概念很容易因表述不准确而造成误解,这里没有深究线程与进程,并发与并行的概念
多线程操作单线程访问,使用线程锁保护数据一致性。
C++多线程基础知识
首先要引入头文件#include thread类,创建一个线程即实例化一个该类的对象,实例化对象时候调用的构造函数需要传递一个参数,该参数就是函数名,thread th1(proc1);如果传递进去的函数本身需要传递参数,实例化对象时将这些参数按序写到函数名后面,thread th1(proc1,a,b);只要创建了线程对象(传递“函数名/可调用对象”作为参数的情况下),线程就开始执行(std::thread 有一个无参构造函数重载的版本,不会创建底层的线程)。
有两种线程阻塞方法join()与detach(),阻塞线程的目的是调节各线程的先后执行顺序,这里重点讲join()方法,不推荐使用detach(),detach()使用不当会发生引用对象失效的错误。当线程启动后,一定要在和线程相关联的thread对象销毁前,对线程运用join()或者detach()。
join(), 当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续。th1.join(),即该语句所在的线程(该语句写在main()函数里面,即主线程内部)暂停,等待指定线程(指定线程为th1)执行结束后,主线程再继续执行。
整个过程就相当于你在做某件事情,中途你让老王帮你办一个任务(你办的时候他同时办)(创建线程1),又叫老李帮你办一件任务(创建线程2),现在你的这部分工作做完了,需要用到他们的结果,只需要等待老王和老李处理完(join(),阻塞主线程),等他们把任务做完(子线程运行结束),你又可以开始你手头的工作了(主线程不再阻塞)
#include<iostream>
#include<thread>
using namespace std;
void proc(int a)
{
cout << "我是子线程,传入参数为" << a << endl;
cout << "子线程中显示子线程id为" << this_thread::get_id()<< endl;
}
int main()
{
cout << "我是主线程" << endl;
int a = 9;
thread th2(proc,a);//第一个参数为函数名,第二个参数为该函数的第一个参数,如果该函数接收多个参数就依次写在后面。此时线程开始执行。
cout << "主线程中显示子线程id为" << th2.get_id() << endl;
th2.join();//此时主线程被阻塞直至子线程执行结束。
return 0;
}
互斥量使用:
什么是互斥量?
这样比喻,单位上有一台打印机(共享数据a),你要用打印机(线程1要操作数据a),同事老王也要用打印机(线程2也要操作数据a),但是打印机同一时间只能给一个人用,此时,规定不管是谁,在用打印机之前都要向领导申请许可证(lock),用完后再向领导归还许可证(unlock),许可证总共只有一个,没有许可证的人就等着在用打印机的同事用完后才能申请许可证(阻塞,线程1lock互斥量后其他线程就无法lock,只能等线程1unlock后,其他线程才能lock),那么,这个许可证就是互斥量。互斥量保证了使用打印机这一过程不被打断。
程序实例化mutex对象m,线程调用成员函数m.lock()会发生下面 3 种情况:
(1)如果该互斥量当前未上锁,则调用线程将该互斥量锁住,直到调用unlock()之前,该线程一直拥有该锁。
(2)如果该互斥量当前被锁住,则调用线程被阻塞,直至该互斥量被解锁。
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
m.lock();
cout << "proc1函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 2 << endl;
m.unlock();
}
void proc2(int a)
{
m.lock();
cout << "proc2函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 1 << endl;
m.unlock();
}
int main()
{
int a = 0;
thread proc1(proc1, a);
thread proc2(proc2, a);
proc1.join();
proc2.join();
return 0;
}
不推荐实直接去调用成员函数lock(),因为如果忘记unlock(),将导致锁无法释放,使用lock_guard或者unique_lock能避免忘记解锁这种问题。
lock_guard():
其原理是:声明一个局部的lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。最终的结果就是:创建即加锁,作用域结束自动解锁。从而使用lock_guard()就可以替代lock()与unlock()。
通过设定作用域,使得lock_guard在合适的地方被析构(在互斥量锁定到互斥量解锁之间的代码叫做临界区(需要互斥访问共享资源的那段代码称为临界区),临界区范围应该尽可能的小,即lock互斥量后应该尽早unlock),通过使用{}来调整作用域范围,可使得互斥量m在合适的地方被解锁:
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
lock_guard<mutex> g1(m);//用此语句替换了m.lock();lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
cout << "proc1函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 2 << endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁
void proc2(int a)
{
{
lock_guard<mutex> g2(m);
cout << "proc2函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 1 << endl;
}//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
cout << "作用域外的内容3" << endl;
cout << "作用域外的内容4" << endl;
cout << "作用域外的内容5" << endl;
}
int main()
{
int a = 0;
thread proc1(proc1, a);
thread proc2(proc2, a);
proc1.join();
proc2.join();
return 0;
}
lock_gurad也可以传入两个参数,第一个参数为adopt_lock标识时,表示不再构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
m.lock();//手动锁定
lock_guard<mutex> g1(m,adopt_lock);
cout << "proc1函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 2 << endl;
}//自动解锁
void proc2(int a)
{
lock_guard<mutex> g2(m);//自动锁定
cout << "proc2函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 1 << endl;
}//自动解锁
int main()
{
int a = 0;
thread proc1(proc1, a);
thread proc2(proc2, a);
proc1.join();
proc2.join();
return 0;
}
unique_lock:
unique_lock类似于lock_guard,只是unique_lock用法更加丰富,同时支持lock_guard()的原有功能。
使用lock_guard后不能手动lock()与手动unlock();使用unique_lock后可以手动lock()与手动unlock();
unique_lock的第二个参数,除了可以是adopt_lock,还可以是try_to_lock与defer_lock;
try_to_lock: 尝试去锁定,得保证锁处于unlock的状态,然后尝试现在能不能获得锁;尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里
defer_lock: 始化了一个没有加锁的mutex;
lock_guard unique_lock
手动lock与手动unlock 不支持 支持
参数 支持adopt_lock 支持adopt_lock/try_to_lock/defer_lock
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void proc1(int a)
{
unique_lock<mutex> g1(m, defer_lock);//始化了一个没有加锁的mutex
cout << "不拉不拉不拉" << endl;
g1.lock();//手动加锁,注意,不是m.lock();注意,不是m.lock();注意,不是m.lock()
cout << "proc1函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 2 << endl;
g1.unlock();//临时解锁
cout << "不拉不拉不拉" << endl;
g1.lock();
cout << "不拉不拉不拉" << endl;
}//自动解锁
void proc2(int a)
{
unique_lock<mutex> g2(m,try_to_lock);//尝试加锁,但如果没有锁定成功,会立即返回,不会阻塞在那里;
cout << "proc2函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 1 << endl;
}//自动解锁
int main()
{
int a = 0;
thread proc1(proc1, a);
thread proc2(proc2, a);
proc1.join();
proc2.join();
return 0;
}
unique_lock所有权的转移
mutex m;
{
unique_lock<mutex> g2(m,defer_lock);
unique_lock<mutex> g3(move(g2));//所有权转移,此时由g3来管理互斥量m
g3.lock();
g3.unlock();
g3.lock();
}
condition_variable:
需要#include<condition_variable>;
wait(locker):在线程被阻塞时,该函数会自动调用 locker.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(通常是另外某个线程调用 notify_* 唤醒了当前线程),wait() 函数此时再自动调用 locker.lock()。
notify_all():随机唤醒一个等待的线程
notify_once():唤醒所有等待的线程
3 异步线程
需要#include
async与future:
async是一个函数模板,用来启动一个异步任务,它返回一个future类模板对象,future对象起到了占位的作用,刚实例化的future是没有储存值的,但在调用future对象的get()成员函数时,主线程会被阻塞直到异步线程执行结束,并把返回结果传递给future,即通过FutureObject.get()获取函数返回值。
相当于你去办政府办业务(主线程),把资料交给了前台,前台安排了人员去给你办理(async创建子线程),前台给了你一个单据(future对象),说你的业务正在给你办(子线程正在运行),等段时间你再过来凭这个单据取结果。过了段时间,你去前台取结果,但是结果还没出来(子线程还没return),你就在前台等着(阻塞),直到你拿到结果(get())你才离开(不再阻塞)。
#include <iostream>
#include <thread>
#include <mutex>
#include<future>
#include<Windows.h>
using namespace std;
double t1(const double a, const double b)
{
double c = a + b;
Sleep(3000);//假设t1函数是个复杂的计算过程,需要消耗3秒
return c;
}
int main()
{
double a = 2.3;
double b = 6.7;
future<double> fu = async(t1, a, b);//创建异步线程线程,并将线程的执行结果用fu占位;
cout << "正在进行计算" << endl;
cout << "计算结果马上就准备好,请您耐心等待" << endl;
cout << "计算结果:" << fu.get() << endl;//阻塞主线程,直至异步线程return
//cout << "计算结果:" << fu.get() << endl;//取消该语句注释后运行会报错,因为future对象的get()方法只能调用一次。
return 0;
}
原子类型automic
原子操作指“不可分割的操作”;也就是说这种操作状态要么是完成的,要么是没完成的。互斥量的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量。
automic是一个模板类,使用该模板类实例化的对象,提供了一些保证原子性的成员函数来实现共享数据的常用操作。
可以这样理解:
在以前,定义了一个共享的变量(int i=0),多个线程会操作这个变量,那么每次操作这个变量时,都是用lock加锁,操作完毕使用unlock解锁,以保证线程之间不会冲突;
现在,实例化了一个类对象(automic I=0)来代替以前的那个变量,每次操作这个对象时,就不用lock与unlock,这个对象自身就具有原子性,以保证线程之间不会冲突。
automic对象提供了常见的原子操作(通过调用成员函数实现对数据的原子操作):
store是原子写操作,load是原子读操作。exchange是于两个数值进行交换的原子操作。
即使使用了automic,也要注意执行的操作是否支持原子性。一般atomic原子操作,针对++,–,+=,-=,&=,|=,^=是支持的。
生产者消费者问题
*/
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include<Windows.h>
using namespace std;
deque<int> q;
mutex mu;
condition_variable cond;
int c = 0;//缓冲区的产品个数
void producer() {
int data1;
while (1) {//通过外层循环,能保证生成用不停止
if(c < 3) {//限流
{
data1 = rand();
unique_lock<mutex> locker(mu);//锁
q.push_front(data1);
cout << "存了" << data1 << endl;
cond.notify_one(); // 通知取
++c;
}
Sleep(500);
}
}
}
void consumer() {
int data2;//data用来覆盖存放取的数据
while (1) {
{
unique_lock<mutex> locker(mu);
while(q.empty())
cond.wait(locker); //wati()阻塞前先会解锁,解锁后生产者才能获得锁来放产品到缓冲区;生产者notify后,将不再阻塞,且自动又获得了锁。
data2 = q.back();//取的第一步
q.pop_back();//取的第二步
cout << "取了" << data2<<endl;
--c;
}
Sleep(1500);
}
}
int main() {
thread t1(producer);
thread t2(consumer);
t1.join();
t2.join();
return 0;
}
线程池基础知识
不采用线程池时:
创建线程->由该线程执行任务->任务执行完毕后销毁线程。即使需要使用到大量线程,每个线程都要按照这个流程来创建、执行与销毁。
虽然创建与销毁线程消耗的时间远小于线程执行的时间,但是对于需要频繁创建大量线程的任务,创建与销毁线程所占用的时间与CPU资源也会有很大占比。
为了减少创建与销毁线程所带来的时间消耗与资源消耗,因此采用线程池的策略:
程序启动后,预先创建一定数量的线程放入空闲队列中,这些线程都是处于阻塞状态,基本不消耗CPU,只占用较小的内存空间。
接收到任务后,线程池选择一个空闲线程来执行此任务。
任务执行完毕后,不销毁线程,线程继续保持在池中等待下一次的任务。
线程池所解决的问题:
(1) 需要频繁创建与销毁大量线程的情况下,减少了创建与销毁线程带来的时间开销和CPU资源占用。(省时省力)
(2) 实时性要求较高的情况下,由于大量线程预先就创建好了,接到任务就能马上从线程池中调用线程来处理任务,略过了创建线程这一步骤,提高了实时性。(实时)
线程池的实现
创建类,除了传递函数外,还可以使用:Lambda表达式、可调用类的实例。
并发与并行:并发与并行并不是非此即彼的概念
并发:同一时间发生两件及以上的事情。
线程并不是越多越好,每个线程都需要一个独立的堆栈空间,线程切换也会耗费时间。
并行:
detach():
mutex锁,锁的本质属性是为事物提供“访问保护”,例如:大门上的锁,是为了保护房子免于不速之客的到访;自行车的锁,是为了保护自行车只有owner才可以使用;保险柜上的锁,是为了保护里面的合同和金钱等重要东西
在c++,锁也是用来提供“访问保护”的,不过被保护的东西不再是房子、自行车、金钱,而是内存中的各种变量。
Mutex,互斥量,只在多线程编程中起作用,在单线程程序中是没有什么用处的。从c++11开始,c++提供了std::mutex类型,对于多线程的加锁操作提供了很好的支持。下面看一个简单的例子,对于mutex形成一个直观的认识。
对于std::mutex对象,任意时刻最多允许一个线程对其进行上锁
mtx.lock():调用该函数的线程尝试加锁。如果上锁不成功,即:其它线程已经上锁且未释放,则当前线程block。如果上锁成功,则执行后面的操作,操作完成后要调用mtx.unlock()释放锁,否则会导致死锁的产生
mtx.unlock():释放锁
std::mutex还有一个操作:mtx.try_lock(),字面意思就是:“尝试上锁”,与mtx.lock()的不同点在于:如果上锁不成功,当前线程不阻塞。
虽然std::mutex可以对多线程编程中的共享变量提供保护,但是直接使用std::mutex的情况并不多。因为仅使用std::mutex有时候会发生死锁。回到上边的例子,考虑这样一个情况:假设线程1上锁成功,线程2上锁等待。但是线程1上锁成功后,抛出异常并退出,没有来得及释放锁,导致线程2“永久的等待下去”(线程2:我的心在等待永远在等待……),此时就发生了死锁。给一个发生死锁的
那么这种情况该怎么避免呢? 这个时候就需要ock_guard登场了。lock_guard只有构造函数和析构函数。简单的来说:当调用构造函数时,会自动调用传入的对象的lock()函数,而当调用析构函数时,自动调用unlock()函数
Mutex(互斥量)是一种同步原语,用于实现多线程环境下的资源互斥访问。它允许多个线程同时访问共享资源,但在任何给定时间只能有一个线程能够获得对该资源的独占访问权。Mutex主要用于防止数据竞争和确保数据的一致性。
在C++11之前,开发人员通常使用操作系统提供的互斥机制来实现线程间的同步。而C++11引入的Mutex则提供了一种标准化的、跨平台的解决方案,使得多线程编程更加简单和可靠。
Mutex的基本操作包括锁定(lock)和解锁(unlock)。当一个线程需要访问共享资源时,它会尝试对Mutex进行加锁操作,如果Mutex已经被其他线程锁定,那么该线程将被阻塞,直到Mutex被解锁。一旦线程完成对共享资源的操作,它会释放Mutex,允许其他线程获得对资源的访问权。
使用Mutex可以有效地避免数据竞争和保护共享资源的一致性。然而,Mutex也存在一些潜在问题,如死锁(deadlock)和饥饿(starvation)。为了避免这些问题,开发人员需要仔细设计和管理Mutex的使用,并采用合适的同步机制
Mutex类型 描述
std::mutex 最基本的互斥锁类型,用于实现线程间的互斥访问。只允许一个线程获得锁,其他线程需要等待锁被释放才能继续执行。
std::recursive_mutex 与std::mutex类似,但允许同一线程多次获取锁。也就是说,同一线程可以多次对该锁进行加锁操作,每次加锁都需要对应的解锁操作。
std::timed_mutex 可限时等待的互斥锁类型。与std::mutex类似,但允许线程在尝试获取锁时设置一个超时时间。如果锁在指定的时间内无法被获得,线程将不再等待并返回相应的错误代码。
std::recursive_timed_mutex 可限时等待的递归互斥锁类型。结合了std::recursive_mutex和std::timed_mutex的特性,允许同一线程多次获取锁,并且可以设置超时时间。
std::mutex (基本互斥锁)
std::mutex是最基本的互斥锁类型之一。它用于实现线程间的互斥访问,即在一个时间点只允许一个线程获得锁,其他线程需要等待锁被释放才能继续执行。使用std::mutex可以保证多个线程对共享资源的访问顺序,并避免数据竞争产生的问题。
注意:该类的对象之间不能拷贝,也不能进行移动。
mutex最常用的三个函数是:
函数名 描述
lock() 尝试获取互斥锁。如果未被其他线程占用,则当前线程获取锁;否则阻塞等待锁的释放。
unlock()释放互斥锁。如果当前线程持有锁,则释放锁;否则行为未定义。
try_lock() 尝试获取互斥锁,不会阻塞线程。如果未被其他线程占用,则当前线程获取锁并返回true;否则返回false。
这三个函数组成了基本的互斥锁操作,也是使用mutex时最常用的三个函数。其中,lock()和unlock()通常需要成对使用,以确保锁得到正确的管理。try_lock()则可以用于一些特殊情况下的非阻塞式加锁操作,例如在轮询等待某个资源时,可以尝试获取锁并立即返回结果。
注意事项
线程函数调用lock()时,可能会发生以下三种情况:
如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
线程函数调用try_lock()时,可能会发生以下三种情况:
如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock释放互斥量。
如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
在使用互斥锁时,需要注意正确地加锁和解锁,以避免资源竞争和死锁等问题的发生。在大多数情况下,应优先选择最简单的互斥锁类型std::mutex,只有在需要递归、限时等待功能时才考虑其他类型。选择互斥锁类型应根据具体需求和场景来进行。
通过合理使用互斥锁,可以保证多线程程序的正确性和稳定性,提高多线程程序的性能和并发能