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

目录

一、什么是多线程?

二、流程:

1、创建线程

2、互斥量使用

3、异步线程

三、pthread

四、thread

五、线程池基础知识

Mutex类型


一、什么是多线程?

线程:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,进程包含一个或者多个线程。进程可以理解为完成一件事的完整解决方案,而线程可以理解为这个解决方案中的的一个步骤,可能这个解决方案就这只有一个步骤,也可能这个解决方案有多个步骤。
多线程:多线程是实现并发(并行)的手段,并发(并行)即多个线程同时执行,一般而言,多线程就是把执行一件事情的完整步骤拆分为多个子步骤,然后使得这多个步骤同时执行。
C++多线程:(简单情况下)C++多线程使用多个函数实现各自功能,然后将不同函数生成不同线程,并同时执行这些线程(不同线程可能存在一定程度的执行先后顺序,但总体上可以看做同时执行)。

二、流程:

1、创建线程

创建一个线程即实例化一个该类的对象,实例化对象时候调用的构造函数需要传递一个参数,该参数就是函数名,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;
}

2、互斥量使用

这样比喻,单位上有一台打印机(共享数据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;

}

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!

可以看到,主线程比子线程先退出了。

五、线程池基础知识

不采用线程池时:

创建线程->由该线程执行任务->任务执行完毕后销毁线程。即使需要使用到大量线程,每个线程都要按照这个流程来创建、执行与销毁。

虽然创建与销毁线程消耗的时间远小于线程执行的时间,但是对于需要频繁创建大量线程的任务,创建与销毁线程所占用的时间与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,只有在需要递归、限时等待功能时才考虑其他类型。选择互斥锁类型应根据具体需求和场景来进行。

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

------------

在多线程编程中,锁是一种重要的同步机制,用于避免多个线程在并发访问共享资源时发生竞态条件。

1. std::mutex(互斥锁)

std::mutex 是最基本的互斥锁(mutex)。它确保同一时刻只有一个线程可以访问共享资源。当一个线程获得锁时,其他线程必须等待直到锁被释放。std::mutex 提供了一个简单的方式来同步对共享资源的访问。

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;  // 创建一个互斥锁
int shared_data = 0;  // 共享数据

void increment_data(int id) {
    mtx.lock();  // 获取锁
    shared_data++;  // 修改共享数据
    std::cout << "Thread " << id << " incremented shared_data to " << shared_data << std::endl;
    mtx.unlock();  // 释放锁
}

int main() {
    std::thread t1(increment_data, 1);
    std::thread t2(increment_data, 2);
    t1.join();
    t2.join();
    return 0;
}

在上述示例中,std::mutex 被用来保护 shared_data 变量,确保多个线程在访问时不会导致竞态条件。每次只有一个线程可以修改 shared_data,其他线程会被阻塞直到锁被释放。

  • 适用于保护 单个资源,防止多个线程同时访问造成数据不一致。

  • 适用于较为简单的同步场景,不需要复杂的锁策略。


2. std::unique_lock(独占锁)

std::unique_lock 是比 std::mutex 更灵活的锁机制。它支持锁的延迟获取、提前释放和重新锁定等功能。std::unique_lock 会在其作用域结束时自动释放锁,避免忘记手动解锁。

#include <iostream>
#include <mutex>
#include <thread>

struct Box
{
    explicit Box(int num) : num_things{num} {}

    int num_things;
    std::mutex m;
};

void transfer(Box& from, Box& to, int num)
{
    // don't actually take the locks yet
    std::unique_lock lock1{from.m, std::defer_lock};
    std::unique_lock lock2{to.m, std::defer_lock};

    // lock both unique_locks without deadlock
    std::lock(lock1, lock2);

    from.num_things -= num;
    to.num_things += num;

    // “from.m” and “to.m” mutexes unlocked in unique_lock dtors
}

int main()
{
    Box acc1{100};
    Box acc2{50};

    std::thread t1{transfer, std::ref(acc1), std::ref(acc2), 10};
    std::thread t2{transfer, std::ref(acc2), std::ref(acc1), 5};

    t1.join();
    t2.join();

    std::cout << "acc1: " << acc1.num_things << "\n"
                 "acc2: " << acc2.num_things << '\n';
}

输出:

acc1: 95
acc2: 55
  • 使用 std::defer_lock 延迟加锁,避免立即锁定资源。

  • 使用 std::lock 一次性安全地锁定多个互斥锁,避免因锁的顺序不同而发生死锁。

  • std::unique_lock 会在作用域结束时自动解锁,确保锁的释放是安全的。

  • 当需要更多的 灵活性,例如在一个线程内临时释放锁或延迟加锁时,std::unique_lock 比 std::mutex 更适合。

  • 适合在需要锁定多个资源并避免死锁的场景中使用。


3. std::shared_mutex(共享锁)

std::shared_mutex 允许多个线程 共享 读取资源(以共享锁的形式),但在写入资源时要求 独占 锁。当某个线程获取了独占锁时,其他线程无法获取任何形式的锁;而当多个线程获取共享锁时,它们可以同时读取共享资源。

#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <syncstream>
#include <thread>

class ThreadSafeCounter
{
public:
    ThreadSafeCounter() = default;

    // Multiple threads/readers can read the counter's value at the same time.
    unsigned int get() const
    {
        std::shared_lock lock(mutex_);
        return value_;
    }

    // Only one thread/writer can increment/write the counter's value.
    void increment()
    {
        std::unique_lock lock(mutex_);
        ++value_;
    }

    // Only one thread/writer can reset/write the counter's value.
    void reset()
    {
        std::unique_lock lock(mutex_);
        value_ = 0;
    }

private:
    mutable std::shared_mutex mutex_;
    unsigned int value_{};
};

int main()
{
    ThreadSafeCounter counter;

    auto increment_and_print = [&counter]()
    {
        for (int i = 0; i < 3; ++i)
        {
            counter.increment();
            std::osyncstream(std::cout)
                << std::this_thread::get_id() << ' ' << counter.get() << '\n';
        }
    };

    std::thread thread1(increment_and_print);
    std::thread thread2(increment_and_print);

    thread1.join();
    thread2.join();
}

可能的输出:

131495872169536 2
131495882655296 2
131495872169536 3
131495872169536 5
131495882655296 4
131495882655296 6
  • 在这个例子中,多个线程可以同时获取 std::shared_lock 来读取共享资源,而在写入时必须使用 std::unique_lock 获取独占锁。

  • std::shared_mutex 适用于读多写少的场景,因为它允许多个线程并行读取资源,而写操作则需要独占访问。

  • 适用于读多写少的情况,例如缓存管理系统,数据库中的某些操作等。


4. std::timed_mutex(带超时的互斥锁)

std::timed_mutex 是 std::mutex 的扩展,支持超时机制。当线程尝试获取锁时,可以指定一个超时时间。如果在该时间内无法获得锁,线程将返回并继续执行其他任务。

#include <chrono>
#include <iostream>
#include <mutex>
#include <sstream>
#include <thread>
#include <vector>

usingnamespacestd::chrono_literals;

std::mutex cout_mutex; // control access to std::cout
std::timed_mutex mutex;

void job(int id)
{
    std::ostringstream stream;

    for (int i = 0; i < 3; ++i)
    {
        if (mutex.try_lock_for(100ms))
        {
            stream << "success ";
            std::this_thread::sleep_for(100ms);
            mutex.unlock();
        }
        else
            stream << "failed ";

        std::this_thread::sleep_for(100ms);
    }

    std::lock_guard<std::mutex> lock{cout_mutex};
    std::cout << '[' << id << "] " << stream.str() << '\n';
}

int main()
{
    std::vector<std::thread> threads;
    for (int i{0}; i < 4; ++i)
        threads.emplace_back(job, i);

    for (auto& th : threads)
        th.join();
}

可能的输出:

[2] failed failed failed 
[1] failed success failed 
[0] success failed success 
[3] failed success success
  • 如果 try_lock_for 在指定时间内未能获得锁,线程将返回并输出失败信息。

  • 这种锁适用于线程不希望长时间等待锁的场景。

  • 当线程尝试获取锁时,如果无法立即获取锁,则需要继续执行其他操作或处理任务,而不是一直等待锁。


5. std::recursive_mutex(递归锁)

std::recursive_mutex 允许同一个线程多次获取同一把锁,而不会发生死锁。这是通过为每个线程维护一个锁计数器来实现的。

#include <iostream>
#include <mutex>
#include <thread>

class X
{
    std::recursive_mutex m;
    std::string shared;
public:
    void fun1()
    {
        std::lock_guard<std::recursive_mutex> lk(m);
        shared = "fun1";
        std::cout << "in fun1, shared variable is now " << shared << '\n';
    }
    void fun2()
    {
        std::lock_guard<std::recursive_mutex> lk(m);
        shared = "fun2";
        std::cout << "in fun2, shared variable is now " << shared << '\n';
        fun1(); // recursive lock becomes useful here
        std::cout << "back in fun2, shared variable is " << shared << '\n';
    }
};

int main() 
{
    X x;
    std::thread t1(&X::fun1, &x);
    std::thread t2(&X::fun2, &x);
    t1.join();
    t2.join();
}

输出:

in fun1, shared variable is now fun1
in fun2, shared variable is now fun2
in fun1, shared variable is now fun1
back in fun2, shared variable is fun1
  • 递归锁允许同一个线程多次获取锁而不引发死锁,适用于递归函数或需要多次加锁的场景。

  • 一旦递归调用结束,锁才会被释放。

  • 适用于递归算法或者在函数内部需要多次加锁的情况。


6. std::lock_guard(轻量级锁)

std::lock_guard 是一个轻量级的锁,用于在构造时自动加锁,在析构时自动释放锁。它非常适合在作用域有限的情况下保护共享资源。

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;  // 创建一个互斥锁
int shared_data = 0;

void increment_data(int id) {
    std::lock_guard<std::mutex> lock(mtx);  // 自动加锁,作用域结束时自动释放锁
    shared_data++;
    std::cout << "Thread " << id << " incremented shared_data to " << shared_data << std::endl;
}

int main() {
    std::thread t1(increment_data, 1);
    std::thread t2(increment_data, 2);
    t1.join();
    t2.join();
    return 0;
}

  • std::lock_guard 的主要优点是它非常简单,自动加锁和解锁,不需要显式调用 lock 或 unlock,避免了手动管理锁的复杂性。

  • 适用于临时保护共享资源的情况,尤其是在小范围的代码块内,自动管理锁的生命周期。


C++ 提供了多种锁机制,每种锁都有其适用的场景:

  • std::mutex:最基础的锁,适用于简单的同步。

  • std::unique_lock:更灵活的锁,支持手动解锁和延迟加锁。

  • std::shared_mutex:适用于读多写少的场景,支持多个线程并行读取。

  • std::timed_mutex:支持超时机制的锁,避免线程无限期等待。

  • std::recursive_mutex:递归锁,适用于递归调用等场景。

  • std::lock_guard:轻量级锁,适用于简单的同步场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值