Bootstrap

“深入浅出”系列之C++:(2)多线程

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++多线程基础知识

1 创建线程

首先要引入头文件#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,只有在需要递归、限时等待功能时才考虑其他类型。选择互斥锁类型应根据具体需求和场景来进行。

通过合理使用互斥锁,可以保证多线程程序的正确性和稳定性,提高多线程程序的性能和并发能

;