c++11多线程

本文介绍了C++11中的多线程概念,包括线程构造、执行、线程安全、互斥量、原子操作、线程独立变量、条件变量等内容,详细讲解了各种线程同步和通信机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

demo1

#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

void output(int i)
{
   sleep(1);
   cout << std::this_thread::get_id()<< endl;//线程id, 如果在main中,可以用成员函数t.get_id()
}

int main()
{

    for (uint8_t i = 0; i < 10; i++)
    {
        thread t(output, i);//创建线程,这里放到了循环中,所以用join()才会变成11秒,如果先在之前承建了10个线程,执行时间只有2s。
        t.detach();//线程直接放后台执行,主程序继续向下执行
        //t.join();//线程执行完再执行主程序,用这种的话实际上没有并发,程序执行约11秒。输出结果是10个线程id一样,
    }
    sleep(1);//如果不加这句,主线程结束,子线程来不及执行
    return 0;
}

输出结果

140509965727488
140509990905600140509957334784
140509974120192140509940549376
140509982512896
140509948942080
140509932156672


140509923763968
140509915371264

//整个程序执行时间只有1秒

windows 版本的

#include <iostream>
#include <thread>
#include <windows.h>//Sleep()延时函数,windows是以秒为单位
using namespace std;

void output(int i)
{
	Sleep(100);//设置成1000ms,发现没有输出,主线程执行已经结束
	cout << std::this_thread::get_id() <<  i<<endl;//线程id, 如果在main中,可以用成员函数t.get_id()
}

int main()
{

	for (uint8_t i = 0; i < 10; i++)
	{
		thread t(output, i);//创建线程
		t.detach();//线程直接放后台执行,主程序继续向下执行
		//t.join();//线程依次执行,用这种的话实际上没有并发,程序执行约11秒。输出结果是10个线程id一样
	}
	Sleep(1000);//如果不加这句,主线程结束,子线程来不及执行
	return 0;
}
160367
268726
260003
180320
278885
267321
172324
234569
277202
223208

基础:

1. 线程的构造

template <class _Fn, class... _Args, class = enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>>>
    explicit thread(_Fn&& _Fx, _Args&&... _Ax)

这是一个模板函数,第一个参数可以是函数指针(你可以简单理解为函数名,也可以是一个仿函数。第二个是一个可变长度的参数,表示需要传入的参数。线程构造之后并不会执行,你需要选择用join()或者detach()来让创建的线程执行。

2.线程的执行

两种方式:

  1. join():子线程并发,但主线程需要等所有子线程执行完毕才往下执行。
  2. detach():线程分离。 该thread绑定的底层线程分离出来,任该底层线程继续运行(thread失去对该底层线程的控制)。

3.线程安全

多个线程保证互斥访问一个资源,可以通过加锁。参考

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex
#include <unistd.h>
using namespace std;

int counter = 0; // non-atomic counter
std::mutex mtx;           // locks access to counter

void increase()
{
    sleep(1);
    for (int i = 0; i < 10; ++i)
    {
        if (mtx.try_lock())
        {   //only increase if currently not locked:
                ++counter;
            mtx.unlock();
        }
        else
            cout<<"lock"<<endl;
    }
}

int main()
{
    std::thread threads[10];
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(increase);//先创建10个子线程,之后再选择join还是detach

    for (int i =0;i<10;++i)
    {
        cout<<"线程"<<i<<"的id "<<threads[i].get_id()<<endl;
        threads[i].join();
    }
    //sleep(2);
    std::cout << counter <<std::endl;
    return 0;
}
线程0的id 139890223736576
lock
线程1的id 139890215343872
线程2的id 139890206951168
线程3的id 139890198558464
lock
线程4的id 139890190165760
线程5的id 139890181773056
线程6的id 139890173380352
线程7的id 139890164987648
线程8的id 139890156594944
线程9的id 139890148202240
98

real	0m1.004s

我们看到,被锁住两次,所以最后的结果是100-2=98。实际执行时间是1s,说明10个子线程是并发执行的。

如果我们把所有加锁的地方去掉

线程0的id 140375303755520
线程1的id 140375295362816
线程2的id 140375286970112
线程3的id 140375278577408
线程4的id 140375270184704
线程5的id 140375261792000
线程6的id 140375253399296
线程7的id 140375245006592
线程8的id 140375236613888
线程9的id 140375228221184
100

real	0m1.004s

 

线程

  • std::thread

  1. joinable():是否可以阻塞至该thread绑定的底层线程运行完毕(倘若该thread没有绑定底层线程等情况,则不可以join)
  2. join():子线程并发,主线程等待。 主线程阻塞直至该thread的底层线程运行完毕。
  3. detach():子线程后台并发。线程分离。 该thread绑定的底层线程分离出来,任该底层线程继续运行(thread失去对该底层线程的控制)。

 

互斥量

  • std::mutex

  1. 互斥体,一般搭配锁使用,也可自己锁住自己(lock(),unlock())。
  2. 若互斥体被第二个锁请求锁住,则第二个锁所在线程被阻塞直至第一个锁解锁。
  • std::lock_guard

  1. 简单锁,构造时请求上锁,释放时解锁,性能耗费较低。适用区域的多线程互斥操作。
  • std::unique_lock

  1. 更多功能也更灵活的锁,随时可解锁或重新锁上(减少锁的粒度),性能耗费比前者高一点点。适用灵活的区域的多线程互斥操作。

原子操作互斥:

原子变量的意思就是单个最小的、不可分割的变量(例如一个int),原子操作则指单个极小的操作(例如一个自增操作)

C++的原子类封装了这种数据对象,使多线程对原子变量的访问不会造成竞争。(可以利用原子类可实现无锁设计)

  • std::atomic_flag

  1. 它是一个原子的布尔类型,可支持两种原子操作。(实际上mutex可用atomic_flag实现)
  2. test_and_set(): 如果atomic_flag对象被设置,则返回true; 如果atomic_flag对象未被设置,则设置之,返回false。
  3. clear():清除atomic_flag对象。
  • std::atomic<T>

  1. 对int, char, bool等基本数据类型进行原子性封装(其实是特化模板)。
  2. store():修改被封装的值。
  3. load() 读取被封装的值

 

线程独立变量

  • thread_local

  1. 变量在每个线程各自独立(类似static),并在线程结束时释放。

 

条件变量

条件变量一般是用来实现多个线程的等待队列,即主线程通知(notify)有活干了,则等待队列中的其它线程就会被唤醒,开始干活。

  • std::condition_variable

  1. wait(std::unique_lock<std::mutex>& lock, Predicate pred = [](){return true;}):pred()为true时直接返回,pred()为false时,lock必须满足已被当前线程锁定的前提。执行原子地释放锁定,阻塞当前线程,并将其添加到等待*this的线程列表中。
  2. notify_one()/notify_all():激活某个或者所有等待的线程,被激活的线程重新获得锁。

虚假唤醒:

处于等待的添加变量可以通过notify_one/notify_all进行唤醒,调用函数进行信号的唤醒时,处于等待的条件变量会重新进行互斥锁的竞争。

没有得到互斥锁的线程就会发生等待转移(wait morphing),从等待信号量的队列中转移到等待互斥锁的队列中,一旦获取到互斥锁的所有权就会接着向下执行,

但是此时其他线程已经执行并重置了执行条件(例如一个活只需要两个线程来干,通知完两个线程后重置执行条件),这可能导致该线程执行引发未定义的错误。

//不能应对虚假唤醒
if(pred()){
   cv.wait(lock);
}
//利用while重复判断执行条件,可以应对虚假唤醒
while(pred()){
   cv.wait(lock);
}
//C++11提供了更方便的语法,将判断条件作为一个参数,实际上等价于前者
cv.wait(lock,pred);

 

提供方

  • std::promise<T>

  1. 构造时,产生一个未就绪的共享状态(包含存储的T值和是否就绪的状态)。可设置T值,并让状态变为ready。
  2. get_future():共享状态绑定到future对象。
  3. set_value():设置共享状态的T值,并让状态变为ready,则绑定的future对象可get()。
  • std::packaged_task<Func>

  1. 构造时绑定一个函数对象,也产生一个未就绪的共享状态。通过thread启动或者仿函数形式启动该函数对象。
  2. 但是相比promise,没有提供set_value()公用接口,而是当执行完绑定的函数对象,其执行结果返回值或所抛异常被存储于能通过 std::future 对象访问的共享状态中。
  3. get_future():共享状态绑定到future对象。

 

获取方

  • std::future<T>

  1. 用于访问共享状态(即获取值)。
  2. 当future的状态还不是ready时就调用一个绑定的promise, packaged_task等的析构函数,会在期望里存储一个异常。
  3. std::future有局限性,在很多线程等待时,只有一个线程能获取等待结果。
  4. share():分享同一个共享状态给另一个future
  5. wait():若共享状态不是ready,则阻塞直至ready。
  6. get():获得共享状态的值,若共享状态不是ready,则阻塞直至ready。
  • std::shared_future<T>

  1. 当需要多个线程等待相同的事件的结果(即多处访问同一个共享状态),需要用std::shared_future来替代std::future。
  2. shared_future与future类似,但shared_future可以拷贝、多个shared_future可以共享某个共享状态的最终结果(即共享状态的某个值或者异常)。
  3. shared_future可通过某个future对象隐式转换,或通过future::share()显示转换,无论哪种转换,被转换的那个future对象都会变为not-valid

 

异步(封装的异步操作)

  • std::async(std::launch::async | std::launch::deferred, Func, Args...)

  1. 异步执行一个函数,其函数执行完后的返还值绑定给使用std::async的futrue(其实是封装了thread,packged_task的功能,使异步执行一个任务更为方便)。
  2. 若用创建std::thread执行异步行为,硬件底层线程可能不足,产生错误。而std::async将这些底层细节掩盖住,如果使用默认参数则与标准库的线程管理组件一起承担线程创建和销毁、避免过载、负责均衡的责任。
  3. 所以尽量使用以任务为驱动的async操作设计,而不是以线程为驱动的thread设计。
  4. std::async中的第一个参数是启动策略,它控制std::async的异步行为,我们可以用三种不同的启动策略来创建std::async:

           std::launch::async参数 保证异步行为,即传递函数将在单独的线程中执行。

           std::launch::deferred参数 当其他线程调用get()/wait()来访问共享状态时,将调用非异步行为。

           std::launch::async | std::launch::deferred参数 是默认行为。有了这个启动策略,它可以异步运行或不运行,这取决于系统的负载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值