iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >C++中多线程的执行顺序如你预期吗
  • 134
分享到

C++中多线程的执行顺序如你预期吗

C++多线程执行顺序C++多线程 2022-11-13 18:11:15 134人浏览 独家记忆
摘要

目录一个简单的例子诡异的输出结果你看到的执行顺序不是真的执行顺序你看到的执行顺序还不是真正的执行顺序c++多线程内存模型一个简单的例子 先来看一个多线程的例子: 如图所示,我们将变

一个简单的例子

先来看一个多线程的例子:

如图所示,我们将变量x和y初始化为0,然后在线程1中执行:

	x = 1, m = y;

同时在线程2中执行:

	y = 1, n = x;

当两个线程都执行结束以后,m和n的值分别是多少呢?

对于已经工作了n年、写过无数次并发程序的的我们来说,这还不是小case吗?让我们来分析一下,大概有三种情况:

  • 如果程序先执行了x = 1, m = y代码段,后执行了y = 1, n = x代码段,那么结果是m = 0, n = 1;
  • 如果程序先执行了y = 1, n = x代码段,后执行了x = 1, m = y代码段,那么结果是m = 1, n = 0;
  • 如果程序的执行顺序先是 x = 1, y = 1, 后执行m = y, n = x, 那么结果是m = 1, n = 1;

所以(m, n)的组合一共有3种情况,分别是(0, 1), (1, 0)和(1, 1)。

那有没有可能程序执行结束后,(m, n)的值是(0, 0)呢?嗯...我们又仔细的回顾了一下自己的分析过程:在m和n被赋值的时候,x = 1和y = 1至少有一条语句被执行了...没有问题,那应该就不会出现m和n都是0的情况。

诡异的输出结果

不过人在江湖上混,还是要严谨一点。好在这代码逻辑也不复杂,那就写一段简单的程序来验证下吧:

#include <iOStream>
#include <thread>

using namespace std;

int x = 0, y = 0, m = 0, n = 0;
int main()
{
	while (1) {
		x = y = 0;
		thread t1([&]() { x = 1; m = y; });
		thread t2([&]() { y = 1; n = x; });
		t1.join(); t2.join();

		if (m == 0 && n == 0) {
			cout << " m == 0 && n == 0 ? impossible!\n";
		}
	}
	return 0;
}

考虑到多线程的随机性,就写一个无限循环多跑一会吧,反正屏幕也不会有什么输出。我们信心满满的把程序跑了起来,但很快就发现有点不太对劲:

m和n居然真的同时为0了?不可能不可能...这难道是windows或者msvc的bug?那我们到linux上用g++编译试一下,结果程序跑起来之后,又看到了熟悉的输出:

这...打脸未免来得也太快了吧!

你看到的执行顺序不是真的执行顺序

看来这不是bug,真的是有可能出现m和n都是0的情况。可是,到底是为什么呢?恍惚之间,我们突然想起曾经似乎在哪看过这样一个as-if规则:

The rule that allows any and all code transfORMations that do not change the observable behavior of the program.

也就是说,在不影响可观测结果的前提下,编译器是有可能对程序的代码进行重排,以取得更好的执行效率的。比如像这样的代码:

int a, b;
void test()
{
	a = b + 1;
	b = 1;
}

编译器是完全有可能重新排列成下面的样子的:

int a, b;
void test()
{
	int c = b;
	b = 1;
	c += 1;
	a = c;
}

这样,程序在实际执行过程中对a的赋值就晚于对b的赋值之后了。不过,有了前车之鉴,我们还是先验证一下在下结论吧。我们使用GCc的-S选项,生成汇编代码(开启-O2优化)来看一下,编译器生成的指令到底是什么样子的:

哈哈,果然如我们所料,对a的赋值被调整到对b的赋值后面了!那上面m和n同时为0也一定是因为编译器重新排序我们的指令顺序导致的!想到这里,我们的底气又渐渐回来了。那就生成汇编代码看看吧:

果然不出所料,因为我们在编译的时候开了-O3优化,赋值的顺序被重排了!代码实际的执行顺序大概是下面这个样子:

	int t1 = y; x = 1; m = t1; //线程1
	int t2 = x; y = 1; n = t2; //线程2

这就难怪会出现m = 0, n = 0这样的结果了。分析到这里,我们终于有点松了一口气,这多年的编程经验可不是白来的,总算是给出了一个合理的解释。
那我们在编译的时候把-O3优化选项去掉,尽量让编译器不要进行优化,保持原来的指令执行顺序,应该就可以避免m和n同时为0的结果了吧?试试,保险起见,我们还是先看一看汇编代码吧:

跟我们的预期一致,汇编代码保持了原来的执行顺序,这回肯定没有问题了。那就把程序跑起来吧。然而...不一会儿,熟悉的打印又出现了...

这...到底是怎么回事?!!!

你看到的执行顺序还不是真正的执行顺序

如果不是编译器重排了我们的指令顺序,那还会是什么呢?难道是CPU?!
还真是。实际上,现代CPU为了提高执行效率,大多都采用了流水线技术。例如:一个执行过程可以被分为:取指(IF),译码(ID),执行(EX),访存(MEM),回写(WB)等阶段。这样,当第一条指令在执行的时候,第二条指令可以进行译码,第三条指令可以进行取指...于是CPU被充分利用了,指令的执行效率也大大提高。一个标准的5级流水线的工作过程如下表所示(实际的CPU流水线远比这复杂得多):

序号/时钟周期1234567...
1IFIDEXMEMWB   
2 IFIDEXMEMWB  
3  IFIDEXMEMWB 
4   IFIDEXMEMWB
5    IFIDEXMEM
6     IFIDEX

上面展示的指令流水线是完美的,然而实际情况往往没有这么理想。考虑这样一种情况,假设第二条指令依赖于第一条指令的执行结果,而第一条指令恰巧又是一个比较耗时的操作,那么整个流水线就停止了。即使第三条指令与前两条指令完全无关,它也必须等到第一条指令执行完成,流水线继续运转时才能得已执行。这就浪费了CPU的执行带宽。乱序执行(Out-Of-Order Execution)就是被用来解决这一问题的,它也是现代CPU提升执行效率的基础技术之一。
简单来说,乱序执行是指CPU提前分析待执行的指令,调整指令的执行顺序,以期发挥更高流水线执行效率的一种技术。引入乱序执行技术以后,CPU执行指令过程大概是下面这个样子:

所以,上面的程序出现(m, n)结果为(0, 0)的情况,应该就是因为指令的执行顺序被CPU重排了!

C++多线程内存模型

我们通常将读取操作称为load,存储操作称为store。对应的内存操作顺序有以下几种:

  • load->load(读读)
  • load->store(读写)
  • store->load(写读)
  • store->store(写写)

CPU在执行指令的时候,会根据情况对内存操作顺序进行重新排列。也就是说,我们只要能够让CPU不要进行指令重排优化,那么应该就不会出现(m, n)为(0, 0)的情况了。但具体要怎么做呢?

实际上,在C++11之前,我们很难在语言层面做到这件事情。那时的C++甚至连线程都不支持,更别提什么内存模型了。在C++98的年代,我们只能通过嵌入汇编的方式添加内存屏障来达到这样的目的:

asm volatile("mfence" ::: "memory");

不过在现代C++中,要做这样的事情就简单多了。C++11引入了原子类型(atomic),同时规定了6种内存执行顺序:

  • memory_order_relaxed: 松散的,在保证原子性的前提下,允许进行任务的重新排序;
  • memory_order_release: 代码中这条语句前的所有读写操作, 不允许被重排到这个操作之后;
  • memory_order_acquire: 代码中这条语句后的所有读写操作,不允许被重排到这个操作之前;
  • memory_order_consume: 代码中这条语句后所有与这块内存相关的读写操作,不允许被重排到这个操作之前;注意,这个类型已不建议被使用;
  • memory_order_acq_rel: 对读取和写入施加acquire-release语义,无法被重排;
  • memory_order_seq_cst: 顺序一致性,如果是写入就是release语义,如果是读取是acquire语义,如果是读取-写入就是acquire-release语义;也是原子变量的默认语义。

所以,我们只需要将x和y的类型改为atmioc_int,就可以避免m和n同时为0的结果出现了。修改后的代码如下:

#include <iostream>
#include <thread>
#include <atomic>

using namespace std;

atomic_int x(0);
atomic_int y(0);
int m = 0, n = 0;
int main()
{
        while (1) {
                x = y = 0;
                thread t1([&]() { x = 1; m = y; });
                thread t2([&]() { y = 1; n = x; });
                t1.join(); t2.join();

                if (m == 0 && n == 0) {
                        cout << " m == 0 && n == 0 ? impossible!\n";
                }
        }
        return 0;
}

现在编译运行一下,看看结果:

已经不会再出现"impossible"的打印了。我们再来看看生成的汇编代码:

原来编译器已经自动帮我们插入了内存屏障,这样就再也不会出现(m, n)为(0, 0)的情况了。

到此这篇关于C++中多线程的执行顺序如你预期吗的文章就介绍到这了,更多相关C++多线程执行顺序内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: C++中多线程的执行顺序如你预期吗

本文链接: https://www.lsjlt.com/news/169794.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • C++中多线程的执行顺序如你预期吗
    目录一个简单的例子诡异的输出结果你看到的执行顺序不是真的执行顺序你看到的执行顺序还不是真正的执行顺序C++多线程内存模型一个简单的例子 先来看一个多线程的例子: 如图所示,我们将变...
    99+
    2022-11-13
    C++多线程执行顺序 C++多线程
  • java如何实现多线程的顺序执行
    场景 编写一个程序,启动三个线程,三个线程的name分别是A,B,C;,每个线程将自己的ID值在屏幕上打印5遍,打印顺序是ABCABC... 使用 synchronized 实现 ...
    99+
    2024-04-02
  • java怎么实现多线程的顺序执行
    这篇文章主要介绍java怎么实现多线程的顺序执行,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!场景编写一个程序,启动三个线程,三个线程的name分别是A,B,C;,每个线程将自己的ID值在屏幕上打印5遍,打印顺序是A...
    99+
    2023-06-15
  • Java怎么让多线程按顺序执行
    本文小编为大家详细介绍“Java怎么让多线程按顺序执行”,内容详细,步骤清晰,细节处理妥当,希望这篇“Java怎么让多线程按顺序执行”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。在子线程中通过join()方法指定...
    99+
    2023-06-30
  • Java中try-catch-finally执行顺序你知道吗
    目录引言正文try块中有returncatch块中有returntry块和finally块中有returncatch块和finally块中有returntry块、catch块和fin...
    99+
    2024-04-02
  • Java让多线程按顺序执行的几种方法
    目录在子线程中通过join()方法指定顺序在主线程中通过join()方法指定顺序通过倒数计时器CountDownLatch实现通过创建单一化线程池newSingleThreadExe...
    99+
    2024-04-02
  • ReentrantLock条件变量使多个线程顺序执行
    目录一. 前言二. 解决方案三. 使用ReentrantLock 条件变量四. 后话一. 前言 近日一个学生在参加某公司校招面试时,遇到一个多个线程顺序执行的面试题,特意记录下来和大...
    99+
    2022-12-19
    ReentrantLock多个线程顺序执行 ReentrantLock条件变量
  • PHP 多线程环境中的函数执行顺序是如何处理的?
    在 php 多线程环境中,函数执行顺序取决于:php 本身:默认单线程,但可以通过启用多线程创建多个并行线程执行任务。服务器环境:如 apache 服务器,每个请求创建新进程包含 php...
    99+
    2024-04-17
    php 多线程 apache
  • Java中保证线程顺序执行的操作代码
    只要了解过多线程,我们就知道线程开始的顺序跟执行的顺序是不一样的。如果只是创建三个线程然后执行,最后的执行顺序是不可预期的。这是因为在创建完线程之后,线程执行的开始时间取决于CPU何...
    99+
    2024-04-02
  • 聊聊golang中多个defer的执行顺序
    golang 中多个 defer 的执行顺序 引用 Ture Go 中的一个示例: package main import "fmt" func main() { fmt...
    99+
    2024-04-02
  • C语言的线性表之顺序表你了解吗
    目录线性表 —— 顺序表 (C语言) 1. 顺序表的储存结构2. 顺序表的基本操作2.1 顺序表的插入2.2 顺序表的查找2.3 顺序表的删除总结线...
    99+
    2024-04-02
  • C#类中方法的执行顺序是什么
    有些中级开发小伙伴还是搞不太明白在继承父类以及不同场景实例化的情况下,父类和子类的各种方法的执行顺序到底是什么,下面通过场景的举例来重新认识下方法的执行顺序: (下面内容涉及到了C#...
    99+
    2024-04-02
  • 你知道如何在Java打包中实现同步函数的顺序执行吗?
    当我们在Java中编写多线程程序时,经常会遇到需要同步执行一些函数的情况。而在Java中,我们可以使用synchronized关键字来实现同步执行。但是,当我们需要按照一定的顺序来执行同步函数时,就需要一些特殊的技巧。本文将介绍如何在Jav...
    99+
    2023-09-29
    打包 同步 函数
  • 怎么在java中利用多线程执行多个程序
    这期内容当中小编将会给大家带来有关怎么在java中利用多线程执行多个程序,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。常用的java框架有哪些1.SpringMVC,Spring Web MVC是一种基于...
    99+
    2023-06-14
  • C#类中方法执行顺序指的是什么
    小编给大家分享一下C#类中方法执行顺序指的是什么,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!有些中级开发小伙伴还是搞不太明白在继承父类以及不同场景实例化的情况下...
    99+
    2023-06-15
  • 三个线程顺序执行的实现方法是什么
    本篇内容主要讲解“三个线程顺序执行的实现方法是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“三个线程顺序执行的实现方法是什么”吧!先说下要求,就是三个线程,...
    99+
    2024-04-02
  • vue2与vue3中生命周期执行顺序的区别说明
    目录vue2与vue3中生命周期执行顺序区别生命周期比较简单例子说明三种情况下的生命周期执行顺序1、单页面下生命周期顺序2、父子、兄弟组件的生命周期顺序3、不同页面跳转时各页面生命周...
    99+
    2024-04-02
  • 微信小程序中如实现按顺序同步执行
    这篇文章给大家分享的是有关微信小程序中如实现按顺序同步执行的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。小程序按顺序同步执行有如下两种方式:第一种方式:回调函数执行,后一个方法写到前一个的回调函数中从而实现顺序执...
    99+
    2023-06-14
  • 在Java中实现让线程按照自己指定的顺序执行
    目录如何让线程按照自己指定的顺序执行认识Join利用Executors线程池线程的优先级及执行顺序优先级概述使用优先级如何让线程按照自己指定的顺序执行 我们在日常的多线程开发中,可能...
    99+
    2024-04-02
  • vue2与vue3中的生命周期执行顺序有什么区别
    vue2与vue3中生命周期执行顺序区别生命周期比较vue2中执行顺序 beforeCreate=>created=>beforeMount =>mounted=>beforeUpdate =>upd...
    99+
    2023-05-16
    Vue3 vue2
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作