当前位置:网站首页 > 技术博客 > 正文

stl 多线程



前方高能:本文字数接近2万

什么是多线程?

百度百科中的解释:

进程与线程的区别

定义:

进程是正在运行的程序的实例,而线程是是进程中的实际运作单位。

区别:

  • 一个程序有且只有一个进程,但可以拥有至少一个的线程。
  • 不同进程拥有不同的地址空间,互不相关,而不同线程共同拥有相同进程的地址空间。

看了上述介绍,你应该明白进程与线程的区别了。什么,还不明白?下面这幅图应该能让你搞清楚:
进程与线程的区别
(自己画的图,不好看请见谅)

在C中已经有一个叫做pthread的东西来进行多线程编程,但是并不好用 (如果你认为句柄、回调式编程很实用,那请当我没说),所以c++11标准库中出现了一个叫作std::thread的东西。

构造&析构函数
函数 类别 作用 thread() noexcept 默认构造函数 创建一个线程,
什么也不做 template <class Fn, class… Args>
explicit thread(Fn&& fn, Args&&… args) 初始化构造函数 创建一个线程,
以为参数
执行函数 thread(const thread&) = delete 复制构造函数 (已删除) thread(thread&& x) noexcept 移动构造函数 构造一个与
相同的对象,会破坏对象 ~thread() 析构函数 析构对象




常用成员函数
函数 作用 void join() 等待线程结束并清理资源(会阻塞) bool joinable() 返回线程是否可以执行join函数 void detach() 将线程与调用其的线程分离,彼此独立执行(此函数必须在线程创建时立即调用,且调用此函数会使其不能被join) std::thread::id get_id() 获取线程id thread& operator=(thread &&rhs) 见移动构造函数
(如果对象是joinable的,那么会调用结果程序)
例一:thread的基本使用
 
  

输出结果:

 
  

或者是

 
  

多线程运行时是以异步方式执行的,与我们平时写的同步方式不同。异步方式可以同时执行多条语句。

在上面的例子中,我们定义了2个thread,这2个thread在执行时并不会按照一定的顺序。打个比方,2个thread执行时,就好比赛跑,谁先跑到终点,谁就先执行完毕。

例二:thread执行有参数的函数
 
  

你的输出有可能是这样

 
  

注意:我说的是有可能。你的运行结果可能和我的不一样,这是正常现象,在上一个例子中我们分析过原因。

这个例子中我们在创建线程时向函数传递了一些参数,但如果要传递引用参数呢?是不是像这个例子中直接传递就行了?让我们来看看第三个例子:

例三:thread执行带有引用参数的函数
 
  

如果你尝试编译这个程序,那你的编译器一定会报错

 
  

这是怎么回事呢?原来thread在传递参数时,是以右值传递的:

 
  

划重点:
很明显的右值引用,那么我们该如何传递一个左值呢?和很好地解决了这个问题。
可以包装按引用传递的值。
可以包装按const引用传递的值。
针对上面的例子,我们可以使用以下代码来修改:



 
  

这次编译可以成功通过,你的程序输出的结果应该是这样的:

 
  

(中间省略了一堆数)

  • 线程是在thread对象被定义的时候开始执行的,而不是在调用join函数时才执行的,调用join函数只是阻塞等待线程结束并回收资源。
  • 分离的线程(执行过detach的线程)会在调用它的线程结束或自己结束时释放资源。
  • 线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄漏。
  • 没有执行或的线程在程序结束时会引发异常

我们现在已经知道如何在c++11中创建线程,那么如果多个线程需要操作同一个变量呢?

 
  

我的2次输出结果分别是:

 
  

是 C++11 中最基本的互斥量,一个线程将mutex锁住时,其它的线程就不能操作mutex,直到这个线程将mutex解锁。根据这个特性,我们可以修改一下上一个例子中的代码:

例四:std::mutex的使用

 
  

执行了好几次,输出结果都是,说明正确。

mutex的常用成员函数

(这里用代指)

函数 作用 void lock() 将mutex上锁。
如果mutex已经被其它线程上锁,
那么会阻塞,直到解锁;
如果mutex已经被同一个线程锁住,
那么会产生死锁。 void unlock() 解锁mutex,释放其所有权。
如果有线程因为调用lock()不能上锁而被阻塞,则调用此函数会将mutex的主动权随机交给其中一个线程;
如果mutex不是被此线程上锁,那么会引发未定义的异常。 bool try_lock() 尝试将mutex上锁。
如果mutex未被上锁,则将其上锁并返回true;
如果mutex已被锁则返回false。







例五:std::atomic的使用

根据atomic的定义,我又修改了例四的代码:

 
  

输出结果:,正常

代码解释

可以看到,我们只是改动了n的类型(->),其他的地方一点没动,输出却正常了。
有人可能会问了:这个是个什么玩意儿?其实,只是的别名罢了。
atomic,本意为原子,官方 (我不确定是不是官方,反正继续解释就对了) 对其的解释是

原子操作是最小的且不可并行化的操作。

这就意味着即使是多线程,也要像同步进行一样同步操作atomic对象,从而省去了mutex上锁、解锁的时间消耗。

std::atomic常用成员函数

构造函数

对,atomic没有显式定义析构函数

函数 类型 作用 atomic() noexcept = default 默认构造函数 构造一个atomic对象(未初始化,可通过atomic_init进行初始化) constexpr atomic(T val) noexcept 初始化构造函数 构造一个atomic对象,用的值来初始化 atomic(const atomic&) = delete 复制构造函数 (已删除)
常用成员函数

atomic能够直接当作普通变量使用,成员函数貌似没啥用,所以这里就不列举了,想搞明白的点这里 (英语渣慎入,不过程序猿中应该没有英语渣吧)

注:std::async定义在头文件中。

为什么大多数情况下使用async而不用thread

std::async参数

不同于thread,async是一个函数,所以没有成员函数。

重载版本 作用 template <class Fn, class… Args>
  future<typename result_of<Fn(Args…)>::type>
    async (Fn&& fn, Args&&… args) 异步或同步(根据操作系统而定)以args为参数执行fn
同样地,传递引用参数需要或 template <class Fn, class… Args>
  future<typename result_of<Fn(Args…)>::type>
    async (launch policy, Fn&& fn, Args&&… args); 异步或同步(根据参数而定(见下文))以args为参数执行fn,引用参数同上




std::launch强枚举类(enum class)

std::launch有2个枚举值和1个特殊值:

标识符 实际值(以Visual Studio 2019为标准) 作用 枚举值:launch::async 0x1(1) 异步启动 枚举值:launch::deferred 0x2(2) 在调用future::get、future::wait时同步启动(std::future见后文) 特殊值:launch::async | launch::defereed 0x3(3) 同步或异步,根据操作系统而定

例六:std::async的使用

暂且不管它的返回值std::future是啥,先举个例再说。

 
  

你的编译器可能会给出一条警告:

 
  
 
  

不过如果你输出的是

 
  

也别慌,正常现象,多线程嘛!反正我执行了好几次也没出现这个结果。

我们已经知道如何使用async来异步或同步执行任务,但如何获得函数的返回值呢?这时候,async的返回值std::future就派上用场了。

例七:使用std::future获取线程的返回值

在之前的所有例子中,我们创建线程时调用的函数都没有返回值,但如果调用的函数有返回值呢?

 
  

输出:

 
  
代码解释

我们定义了一个函数sum,它可以计算多个数字的和,之后我们又定义了一个对象,它的类型是,这里的代表这个函数的返回值是int类型。在创建线程后,我们使用了future::get()来阻塞等待线程结束并获取其返回值。至于sum函数中的折叠表达式(fold expression),不是我们这篇文章的重点。

std::future常用成员函数

构造&析构函数
函数 类型 作用 future() noexcept 默认构造函数 构造一个空的、无效的future对象,但可以 移动分配到另一个future对象 future(const future&) = delete 复制构造函数 (已删除) future (future&& x) noexcept 移动构造函数 构造一个与相同的对象并破坏 ~future() 析构函数 析构对象
常用成员函数
函数 作用 一般:T get()
当类型为引用:R& future<R&>::get()
当类型为void:void future::get() 阻塞等待线程结束并获取返回值。
若类型为void,则与相同。
只能调用一次。 void wait() const 阻塞等待线程结束 template <class Rep, class Period>
  future_status wait_for(const chrono::duration<Rep,Period>& rel_time) const; 阻塞等待(是一段时间),
若在这段时间内线程结束则返回
若没结束则返回
若async是以启动的,则 不会阻塞并立即返回 不知道std::chrono::duration的点这里







std::future_status强枚举类

见上文解释

为啥要有void特化的std::future?

std::future的作用并不只有获取返回值,它还可以检测线程是否已结束、阻塞等待,所以对于返回值是void的线程来说,future也同样重要。

例八:void特化std::future
 
  

如果你运行一下这个代码,你也许就能搞懂那些软件的加载画面是怎么实现的。

 
  

还记得之前我们讲的thread成员函数吗?thread::join()的返回值是void类型,所以你不能通过join来获得线程返回值。那么thread里有什么函数能获得返回值呢?
答案是:没有。
惊不惊喜?意不意外?thread竟然不能获取返回值!难道thread真的就没有办法返回点什么东西吗?如果你真是那么想的,那你就太低估C++了。一些聪明的人可能已经想到解决办法了:可以通过传递引用的方式来获取返回值。

例九:引用传递返回值

这个例子中我们先不牵扯多线程的问题。假如你写一个函数,需要返回3个值,那你会怎么办呢?vector?嵌套pair?不不不,都不需要,3个引用参数就可以了。

 
  

输入5,输出:

 
  

如果你和我输出有一些误差,是正常现象,不同编译器、不同机器处理精度也有所不同

std::promise到底是啥

例十:std::future的值不能改变,那么如何利用引用传递返回值
 
  
 
  

future的值不能改变,promise的值可以改变。

std::promise常用成员函数

构造&析构函数
函数 类型 作用 promise() 默认构造函数 构造一个空的promise对象 template <class Alloc> promise(allocator_arg_t aa, const Alloc& alloc) 构造函数 与默认构造函数相同,但使用特定的内存分配器构造对象 promise (const promise&) = delete 复制构造函数 (已删除) promise (promise&& x) noexcept 移动构造函数 构造一个与相同的对象并破坏 ~promise() 析构函数 析构对象
常用成员函数
函数 作用 一般:
void set_value (const T& val)
void set_value (T&& val)
当类型为引用:void promise<R&>::set_value (R& val)
当类型为void:void promise::set_value (void) 设置promise的值并将共享状态设为ready(将future_status设为ready)
void特化:只将共享状态设为ready future get_future() 构造一个future对象,其值与promise相同,status也与promise相同




例十一:std::promise的使用

以例七中的代码为基础加以修改:

 
  

输出:

 
  

std::this_thread常用函数

std::this_thread是个命名空间,所以你可以使用这样的语句来展开这个命名空间,不过我不建议这么做。

函数 作用 std::thread::id get_id() noexcept 获取当前线程id template<class Rep, class Period>
void sleep_for( const std::chrono::duration<Rep, Period>& sleep_duration ) 等待(是一段时间) void yield() noexcept 暂时放弃线程的执行,将主动权交给其他线程
(放心,主动权还会回来)

例十二:std::this_thread中常用函数的使用

 
  

我的输出:

 
  

你的输出几乎不可能和我一样,不仅是多线程并行的问题,而且每个线程的id也可能不同。

这篇文章到这里就结束了 (说不定以后还会写个c++20的讲解)。 感谢各位在评论区提出的建议。 这是我第一篇接近2万字的文章。其实我刚开始写这篇文章时,也没想到这篇文章会吸引这么多人看,评论里还会有很多的好评,并且还上过一次热榜:
热榜
(厚颜无耻地给自己点赞)
又入选过C/C++领域内容榜:
C/C++领域内容榜
这着实是出乎我的意料的。在此也感谢评论区里各位的好评,我就不一一回复了。
如果你觉得这篇文章有不对、不标准之处,也可以在评论区里说一下,感谢支持。





版权声明


相关文章:

  • 图形验证码是啥意思2025-06-30 10:01:00
  • 栅格系统布局网页效果2025-06-30 10:01:00
  • oracle数据泵导入导出方式2025-06-30 10:01:00
  • 存储器分类及用途2025-06-30 10:01:00
  • 增删改查mysql语句2025-06-30 10:01:00
  • 键盘鼠标记录大师怎么用2025-06-30 10:01:00
  • 串口调试助手推荐2025-06-30 10:01:00
  • mac登录不了app store2025-06-30 10:01:00
  • python基础教程哔哩哔哩2025-06-30 10:01:00
  • python中,第三方库安装的三种方法2025-06-30 10:01:00