C++多线程操作

1 篇文章 0 订阅
1 篇文章 0 订阅

本文主要介绍了thread创建线程的相关知识点,方便日后自己回顾也希望能够帮助到大家,主要的内容包含了thread创建线程、互斥量mutex、条件变量condition_variable、aysnc创建线程以及原子操作等相关知识,如果哪里有错误希望能够私聊我或者在评论区留言!非常感谢!

  1. std::thread

头文件:#include <thread>

我们可以通过std::thread()来创建线程,线程的入口函数可以是函数指针、lambda表达式、类的成员函数以及匿名函数的形式等,如果我们需要获取线程函数的返回值,我们后续可以通过package_task来将任务进行封装通过该类的get_future()方法得到一个future对象,可以调用这个对象的get()方法来获取值。这里先介绍如何创建线程。

1.1无参数传入

对于无参数传入比较简单,直接看下面的几个例子就可以明白。

a.普通函数

b.类作匿名函数以及类的成员函数

c.lambda表达式

d.main函数

注意:在类的成员函数做线程的入口函数的时候需要注意,还需要把该类对象的引用当作第二个参数传入(这里的&是取引用很奇怪)或者直接std::ref(t),才能找到对应的函数。此外在创建完线程后,必须调用对应线程对象的.join()或者.detach()方法

join()是阻塞当前线程来等待子线程运行完毕;

detach()方法是让当前线程和子线程进行分离;

比如说某个线程a创建了一个新的线程b,如果a在创建完b后(thread b(线程入口函数))调用了b.join(),那么a会等待b执行完再继续运行。如果a在创建完b后调用了b.detach(),那么a线程不会阻塞,两个线程各自运行

1.2有参数传入

有参数传入的情况,我们可以自定义一个类,通过这个类作为线程函数的传入参数为例。

a.首先定义一个Person类

b.普通函数

注意如果有函数重载的情况,直接把函数名作线程入口函数会报错我们可以通过函数指针的形式传入。

可以看到拷贝构造函调用了两次,这是因为在构造thread类对象的时候,参数会进行一次拷贝构造,又由于print_thread()的参数是采用值拷贝的方式传入又会进行一次拷贝构造。

我们可以修改print_thread()的参数改为采用引用的形式。注意这里必须加入const否则会报错。

或者创建thread的时候参数加上std::ref(xxx)防止在构造thread类对象的时候,参数会进行一次拷贝构造。

我们看到这里只调用了一次拷贝构造,如果想真正传入参数本身就需要将这两种方式结合使用。注意此时不需要加const也不会报错,可以这样理解之前会报错是因为传入的并不是它真正的值线程不允许对他进行修改,而这里传入的是它本身那么就可以进行修改(个人理解)。这样我们就可以通过子线程来修改主线程的某些值。

注意这里必须使用join(),如果用detach()可能会出现错误的结果因为可能还没有等子线程赋完值,主线程就执行打印语句了,这种情况需要使用互斥量来进行加锁操作。

c.如果使用指针的方式,也可以做到传入参数本身。

d.成员函数\类的匿名函数\lambda表达式有参

对于成员函数、类的匿名函数以及lambda有参数的情况也是和上面类似的

到这我们把thread线程类的入口函数大致说清楚了。之前我们也提到过如果两个线程操作同一个内存的内容会出现问题,如果都是读取那不会出错,但如果同时写或者有读有写那么就会出错,我们需要用互斥量(锁)来解决这个问题。

  1. std::mutex

mutex是互斥量,也就是锁,可以对线程进行保护。如果有多个线程同时操作同一个内存的数据,可能会出现一个线程刚写到一半就切换到了另一个线程来读写那么就会出现错误,这时我们在操作这个内存数据之前需要进行加锁。

头文件:#include <mutex>

原本num的值应该等于20000,但是程序运行的结果却并不是20000并且每次的结果都不一样,这是因为没有加上锁,导致改写内存数据的时候出现错误。

为此我们为此添上一把锁!

可见加锁后结果对了!我们的线程得到了保护。

  1. std::lock_guard

头文件:#include <mutex>

这也是一个管理锁的类,lock_guard构造函数执行了mutex::lock();在作用域结束时,调用析构函数,执行mutex::unlock()。

用法:lock_guard<mutex> 对象名(mutex的对象,[OPTIONS])

OPTIONS:

std::adopt_lock:加入adopt_lock后,在调用lock_guard的构造函数时,不再进行lock();

  1. std::unique_lock

头文件:#include <mutex>

该类和lock_guard差不多,也是在构造函数执行了mutex::lock();在作用域结束时,调用析构函数,执行mutex::unlock()。但该类比lock_guard的使用方法更加多,他的参数也更多但效率会差一点。在后续使用condition_varible必须用该类的对象。

用法:unique_lock<mutex> 对象名(mutx对象,[OPTIONS]);

OPTIONS:

a. std::adopt_lock和lock_guard类似,表示在构造函数不lock这个互斥量需要我们手动增加。

b. std::try_to_lock顾名思义该参数会尝试lock mutex,但如果lock失败他并不会阻塞而是继续往下运行,可以通过owns_lock()方法来进行判断是否拿到锁,拿到返回true。不要在之前进行mutex::lock()

c. std::defer_lock该方法不会对mutex进行加锁,但和adopt_lock参数不太一样,该方法调用主要为了使用unique_lock的一些方法。不能提前lock()。

unique_lock方法:

a. lock():加锁,作用域结束会自动解锁

b. unlock():解锁,主要是为了在作用域内部进行解锁,如作用域内容很多并且大部分内容不需要进行加锁就可以使用unlock()提前进行解锁,当遇到共享的数据时再进行加锁操作。

c. try_lock():尝试加锁,拿不到锁返回false

d. release():释放对mutex对象的所有权,并返回mutex对象的指针。我这里测试了一下release()貌似不会调用unique_lock的析构

  1. std::condition_variable

我们假设有这样一个例子,一个发送端线程每隔一秒发送数据,一个接收端线程需要接受这些数据并进行存储,在接受前接收端肯定先要判断是否有数据发过来,我们一般会想到用while循环来一直判断是否有数据过来,但这样资源的占用率会很高,这时我们可以使用条件变量,它的作用是让线程在不满足条件情况下进行休眠,只有当其他线程调用了notify_all()或notify_one的时候才会苏醒,然后再抢锁和判断条件,如此反复,这样可以有效减少资源占有。

头文件:#include <condition_variable>

语法:std::condition_variable condition

condition_variable的成员函数:

a. wait():用来等待一个条件成立,第一个参数是unique_lock对象,第二个参数可以是一个lambda表达式,如果返回false则将互斥量解锁,并阻塞到本行;如果返回true,那么wait()不会解锁互斥量并继续执行。如果没有第二个参数则和返回false的情况类似。

b. notify_one():通知一个线程的wait()

c. notify_all():通知所有线程的wait()

上述提到的阻塞也就是休眠,它会持续到其他线程调用notify_all/notify_one方法线程才会苏醒,那么线程苏醒后不会立刻继续往下执行,它首先还会去抢锁,然后如果是没有第二个参数情况就会继续运行否则还会再去判断条件,只有条件成立的情况下才会继续运行下去。

所以程序运行下去的时候一定满足了条件并且获得了锁!!

首先我们在Server的成员变量中增加一个条件变量的对象。

对于接受端而言,如果queue队列中没有数据,该线程就会进行休眠,直到发送端发送了数据(此时queue队列中不为空)并且调用了notify_all()函数,接收端线程苏醒后它首先会进行抢锁如果抢到了锁就会进一步判断条件(queue队列是否为空),如果queue队列有数据它就会继续往下运行,否则它会把锁解开再次休眠(如果有多个接收端线程,数据可能会被先抢到锁的线程拿走,那么对于其他线程就是queue队列没数据的情况)。

  1. std::async&&std::future

头文件:#include <future>

该类有点类似于thread,也是用于创建一个线程,创建的方法和thread差不多,可以用函数名、类的匿名函数和成员函数等进行创建,但它会有一个std::future类的返回值,这个返回值主要是用来接受线程运行结束的return 值(也就是说可以捕获不久的未来,线程执行结束的返回值)。

这里虽然我没调用f1的任何方法但是主线程也会等待子线程运行完才会结束。可以看到自线程运行结束test对象才会析构,这看起来和thread也不太一样。

std::future是一个类模板,可以捕获线程未来运行结束的返回值。

成员函数如下:

a. get():程序会阻塞在这里,直到所创建的子线程运行完,future对象通过get()方法拿到返回结果后程序才继续往下运行。

b. wait():几乎和join()一模一样,不能拿到返回值。

c. wait_for():阻塞当前流程,等待异步任务运行一段时间后返回其状态 std::future_status,状态是枚举值:

  1. std::future_status::deferred:异步操作还没开始;

  1. std::future_status::ready:异步操作已经完成;

  1. std::future_status::timeout:异步操作超时。

通过下面的例子来学习如何用get()接受返回值:

但是要注意get()只能调用一次因为它是转移(move)数据,而不是复制数据,多次调用就会报错。如果我们要复制数据,我们要使用std::shared_future

wait()类似于join这里就不再深入。接下来我们在编写个wait_for()的用法,从下面例子中我们可以看出get()和wait_for()并不冲突。

我们也可以注释调do_task()中的sleep(1)

那么std::future_status::deferred什么时候会被触发?这需要我们有个新知识点,就是std::async创建线程的时候还可以加参数

  1. std::lunch::deferred

该参数表示线程入口函数的调用会被延迟,一直到std::future的wait()或者get()函数被调用时(由主线程调用)才会执行。并且创建的“线程”其实压根没被创建,这个参数一用线程函数就和平时调用的函数类似就是在主线程运行,可以通过std::this_thread::get_id()来看看是不是和主线程的线程ID一模一样。

  1. std::launch::async

在调用async函数的时候就开始创建新线程。

具备上面的知识后我们可以写个demo测试一下。

可见子线程和主线程的线程ID一致!并且std::future_status::deferred被触发。

  1. std::packaged_task

头文件:#include <future>

前面学了async来创建线程,我们可以发现它可以通过返回的future对象拿到子线程返回值,那么thread难道就不能拿到返回值了吗?其实不是的,我们可以通过std::packaged_task将线程函数打包起来在传入thread,并且可以通过这个类的get_future()方法来拿到future对象从而获得返回值。

这里我本来也想打包类的成员函数,但是一直有问题,如果有会的小伙伴可以私聊我或者在评论区留言。所以我先用普通函数(lambda也可以)。

注意这里传入thread的时候要加std::ref()否则会报错。

  1. std::promise

头文件:#include <future>

在thread中我们不仅可以拿到先子线程的返回值,而且我们还可以用std::promise来获取子线程中某些变量的值。我们来看一个例子:

注意之前的person的拷贝函数没有写完整,需要补全;此外,传入thread的promise对象需要传入本身。

  1. std::atomic

头文件:#include <atomic>

原子操作:用atomic生成的对象不需要用到互斥量加锁,就可以安全的再多个线程中进行操作。

网上参考了https://blog.csdn.net/TM1695648164/article/details/119716935的表格,可以看到atomic有很多成员函数。

接下来我用个例子来说明atomic用法。

要注意std::atomic<int> num生成的是atomic的对象,所以在printf中并不能直接通过%d来直接获取num的值,需要使用.load()方法

可以很清楚的看到我们就算不用互斥量来加锁也不会出现数据错误!所以这是线程安全的

本文到这里也就结束!接下来我们可以写个线程池来锻炼一下自己!

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

永不秃头的三三

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值