C++ 多线程 精讲 面试八股

  • 什么是C++多线程?

  • 线程:线程是操作系统进行任务调度的基本单位。
  • 进程:进程是操作系统进行资源分配的基本单位。
  • 多线程:多线程是实现并行的手段,并行即多个线程同时执行
  • C++多线程:(简单情况下)C++多线程使用多个函数实现各自功能,然后将不同函数生成不同线程,并同时执行这些线程(不同线程可能存在一定程度的执行先后顺序,但总体上可以看做同时执行)。
  • 并行:同一时刻有复数的任务执行。
  • 并法:同一时间段内有复数任务执行。
  • C++多线程基础

    • 创建线程

  • 头文件#include<thread>(C++11的标准库中提供了多线程库),该头文件中定义了thread类,创建一个线程即实例化一个该类的对象。
  • do_task();
    std::thread(do_task);

  • C++ 11的线程库创建一个std::thread对象,就会启动一个线程,并使用该std::thread对象来管理该线程。构造函数需要的是可调用(callable)类型,只要是有函数调用类型的实例都是可以的。所有除了传递函数外,还可以使用:
  • lambda表达式
  • for (int i = 0; i < 4; i++)
    {
    	thread t([i]{
    		cout << i << endl;
    	});
    	t.detach();
    }
  • 重载了()运算符的类的实例
  • class Task
    {
    public:
    	void operator()(int i)
    	{
    		cout << i << endl;
    	}
    };
    
    int main()
    {
    	
    	for (uint8_t i = 0; i < 4; i++)
    	{
    		Task task;
    		thread t(task, i);
    		t.detach();	
    	}
    }

  • 注意: C++的语法解析错误(C++'s most vexing parse)。向std::thread的构造函数中传入的是一个临时变量,而不是命名变量就会出现语法解析错误。如下代码:
  • std::thread t(Task()); // 等价于std::thread t(Task (*) (void));

    这里相当于声明了一个函数t,其返回类型为thread,而不是启动了一个新的线程。可以使用新的初始化语法避免这种情况

  • std::thread t{Task()};
  • 线程结束

  • 当线程启动后,一定要在和线程相关联的thread销毁前,确定以何种方式等待线程执行结束。C++11有两种方式来等待线程结束:
  • detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。
  • join方式,等待启动的线程完成,才会继续往下执行。
  • 注意:
  • detach()使用不当会发生引用对象失效的错误。创建的新线程对当前作用域的变量的使用,创建新线程的作用域结束后,有可能线程仍然在执行,这时局部变量随着作用域的完成都已销毁,如果线程继续使用局部变量的引用或者指针,会出现意想不到的错误,并且这种错误很难排查。
  • 当线程启动后,一定要在和线程相关联的thread对象销毁前,对线程运用join()或者detach()。如果一个 std::thread 对象在销毁时,关联的线程仍然是可连接状态,系统会调用 std::terminate() 终止程序。
  • 可连接状态:
  • 1.线程既没有使用 join() 使线程执行完毕
  • 2.也没有使用detach()使线程完成分离
  • 向线程传递参数

  • 向线程调用的函数传递参数也是很简单的,只需要在构造thread的实例时,依次传入即可。需要注意的是,默认的会将传递的参数以拷贝的方式复制到线程空间,即使用传值的方式传参。
  • 默认行为:参数拷贝

  • 当你创建 std::thread 时,传递给线程函数的参数会默认被复制。这意味着即使你传递的是一个引用类型,std::thread 仍然会对其进行拷贝。这可能会导致你以为传递的是引用,实际上是传递了一个值拷贝。char*类型传入线程空间后,在线程的空间内转换为string
  • 传递引用:std::ref

  • 如果你希望将参数以引用的方式传递给线程函数,可以使用 std::ref 或 std::cref(用于常量引用)。这告诉 std::thread 你想要传递。
  • void func(int *a,int n){}
    
    int buffer[10];
    thread t(func,buffer,10);//传值
    //thread t(func,std::ref(buffer),10); //传址
    t.join();

  • 转移线程的所有权

  • thread是可移动的(movable)的,但不可复制(copyable)。可以通过move来改变线程的所有权,灵活的决定线程在什么时候join或者detach。
  • thread t1(f1);
    thread t3(move(t1));

    将线程从t1转移给t3,这时候t1就不再拥有线程的所有权,调用t1.join或t1.detach会出现异常,要使用t3来管理线程。这也就意味着thread可以作为函数的返回类型,或者作为参数传递给函数,能够更为方便的管理线程。

  • 线程的标识类型为std::thread::id,有两种方式获得到线程的id。
  • 通过thread的实例调用get_id()直接获取
  • 在当前线程上调用this_thread::get_id()获取
  • 互斥量

  • 互斥量是用于保证多个线程,互斥地使用临界资源(共享资源)的信号量。互斥量保证了同一时刻只被一个进程使用。
  • 程序实例化mutex对象m,线程调用成员函数m.lock()会发生下面 3 种情况
  • 如果该互斥量当前未上锁,则调用线程将该互斥量锁住,直到调用unlock()之前,该线程一直拥有该锁。
  • 如果该互斥量当前被锁住,则调用线程被阻塞,直至该互斥量被解锁。
  • 不推荐实直接去调用成员函数lock(),因为如果忘记unlock(),将导致锁无法释放,使用lock_guard或者unique_lock能避免忘记解锁这种问题。
  • #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_guard

  • 其原理(RAII一种编程范式)是:声明一个局部的lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。加锁绑定于对象的创建,解锁绑定于对象的析构,利于栈自动回收临时变量的机制,通过使用{}来调整作用域范围(对象的生命周期),可使得互斥量m在合适的地方被解锁
  • 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> g2(m,defer_lock);
  •     unique_lock<mutex> g3(move(g2));
  • 所有权转移,此时由g3来管理互斥量m
  • 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

  •  std::condition_variable 提供了一种机制,允许一个线程阻塞自己,直到某个条件被其他线程通知为止。典型的使用场景是一个或多个线程等待某个事件发生,比如等待一个队列中的数据变为可用,或者等待某个计算完成
  • wait(locker): 如果locker处于未锁状态,wait() 函数的行为将是未定义的(可能导致程序崩溃)。因此,调用 wait() 之前必须确保 lock 已经锁定。
  • 如果在线程被阻塞时,该函数会自动调用 locker.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(通常是另外某个线程调notify_* 唤醒了当前线程),wait() 函数此时再自动调用 locker.lock()。
  • notify_once():随机唤醒一个等待的线程
  • notify_all():唤醒所有等待的线程
  • async

  • 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;
    }
    
  • shared_future

  • future与shard_future的用途都是为了占位,但是两者有些许差别。
  • future的get()成员函数是转移数据所有权;shared_future的get()成员函数是复制数据。
  • 因此:future对象的get()只能调用一次;无法实现多个线程等待同一个异步线程,一旦其中一个线程获取了异步线程的返回值,其他线程就无法再次获取。
  • shared_future对象的get()可以调用多次;可以实现多个线程等待同一个异步线程,每个线程都可以获取异步线程的返回值。
  • future
  • shared_future
  • 语义
  • 转移
  • 赋值
  • 可否多次调用
#include <iostream>
#include <thread>
#include <future>

// 一个简单的函数,模拟一些计算任务
int compute(int x) {
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
    return x * x;
}

int main() {
    // 创建一个异步任务,并获取其 future 对象
    std::shared_future<int> sharedFuture = std::async(std::launch::async, compute, 10).share();

    // 启动多个线程来读取 shared_future 的结果
    std::thread t1([sharedFuture]() {
        std::cout << "Thread 1 got result: " << sharedFuture.get() << std::endl;
    });

    std::thread t2([sharedFuture]() {
        std::cout << "Thread 2 got result: " << sharedFuture.get() << std::endl;
    });

    std::thread t3([sharedFuture]() {
        std::cout << "Thread 3 got result: " << sharedFuture.get() << std::endl;
    });

    // 等待所有线程完成
    t1.join();
    t2.join();
    t3.join();

    return 0;
}
  • 原子automic

  • 原子操作指“不可分割的操作”;也就是说这种操作状态要么是完成的,要么是没完成的。互斥量的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量。automic是一个模板类,使用该模板类实例化的对象,提供了一些保证原子性的成员函数来实现共享数据的常用操作。
  • automic对象提供了常见的原子操作(通过调用成员函数实现对数据的原子操作):
  • store是原子写操作,load是原子读操作。exchange是于两个数值进行交换的原子操作。
  • 即使使用了automic,也要注意执行的操作是否支持原子性。一般atomic原子操作,针对++,–,+=,-=,&=,|=,^=是支持的。

下次开线程池

参考:C++多线程基础教程 - zizbee - 博客园 (cnblogs.com)

           C++ 11 多线程--线程管理 - Brook_icv - 博客园 (cnblogs.com)

  • 18
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值