单线程,多线程,异步,同步详解

news2024/11/16 16:20:46

关于异步与多线程,笔者在刚接触的时候一直存在诸多疑惑,甚至一度以为这俩概念是用来描述同一种技术在不同场景下的应用,进而导致对很多与它们相关的概念都一知半解,代码中的async/await关键词也是莫名其妙地在用。

但是在不断地接触这类概念(因为现在的场景中异步与多线程几乎无处不在)的过程中,还是不断地修正了这种思维。代码写起来也顺手多了。

所以这篇文章也是有感而发,在去年的时间里因为多线程和异步踩了不少雷,希望能够给大家做一点简单的解释和区分把。

TL, DR: 请参照文章最后的例子 :)

多线程是什么
多线程 技术有时又称 并行 技术,就是同时做多件事情。这十分好理解,也很直观。

现在的CPU都不止有一个核,每个核都至少具备一个线程,某些CPU具备超线程能力,一个核可以具备多个线程:打开Windows自带的任务管理器,切到性能一栏,选中CPU,线程总数显示在“逻辑处理器”部分。可以看到,笔者这颗 性价比之选 Intel 8700K具备12个线程。

每个线程可以看作是一个流水线,有多个流水线就可以同时运行多段代码,对于某些计算量巨大、同时计算任务又可以拆分的代码,可以将计算任务分配到各个流水线上去,这样就能够更高效地完成指定任务。

总而言之,多线程即 “同时做多件事情”。

下面的代码是一个简单的多线程例子。运行这段代码发现,最后打印的总耗时比每段加起来的耗时要少,这就是并行计算的结果。读者感兴趣可以自行把foreach循环中的有关Task类和lambda函数封包去掉,直接每段运行再进行总耗时求和。

List<Task> tasks = new List<Task>();
Stopwatch sw = Stopwatch.StartNew();
foreach (var item in Enumerable.Range(0,3))
{
    tasks.Add(Task.Run(
    // lambda函数体
    () => {
        Stopwatch sw = Stopwatch.StartNew();
        Thread.Sleep(500);
        Console.WriteLine($"{sw.ElapsedMilliseconds}ms cost");
    }
    ));
}
// 等待所有线程退出
Task.WaitAll(tasks.ToArray());
// 打印计算总耗时
Console.WriteLine(string.Format("Total cost: {0}ms", sw.ElapsedMilliseconds));

异步是什么
“异步” 这个概念是对应于 “同步” 概念而言的。“同步”的意思是,所有代码从头至尾按顺序逐条执行,在一行代码执行完之前,不能执行后面的所有代码。下面的例子的中,当Sum()函数被调用的时候,for循环之后的打印 sum 和 Hello World 一定需要等到这个循环结束之后才能被执行。
 

int Sum(int target)
{
    int sum = 0;
    for (int ind = 0; ind < target; ++ind)
    {
        sum += ind;
    }
    Console.WriteLine(sum.ToString()); // 求和结果打印
    Console.WriteLine("Hello World!"); // Hello World打印
    return sum;
}

而“异步”相对应的,就是代码不按从头至尾的顺序执行,比如我们如果以某种方式让上面代码示例中的打印 Hello World 在打印 sum 求和结果之前执行,那就是异步。

实现异步一般是有两种方式,其一是通过 多线程 (Multithreading),其二是通过 协程 (Coroutine)。

我们平时提到“异步”时,更多地是指 “协程异步”。

线程异步
通过 多线程 来实现异步十分简单直观:把要延后执行的部分扔个一个子程序即可。上述例子中,把for循环封包在一个lambda函数中,然后指派至一个Task类的实例,使用这个实例来进行任务管理即可:

int Sum(int target)
{
    int sum = 0;
    Task<int> task = Task.Run(() =>
    {
        for (int ind = 0; ind < target; ++ind)
        {
            sum += ind;
        }
        Console.WriteLine(sum.ToString()); // 求和结果打印
        return sum;
    });
    Console.WriteLine("Hello World!"); // Hello World打印
    return task.Result;
}

由于函数封装和线程的指派十分灵活,以这种方式实现的异步逻辑在流程控制管理上需要格外小心,并且在处理线程返回值、线程之间的通信上需要更加谨慎,不留意时很容易造成程序死锁。

协程异步
协程异步的提出就是为了解决线程异步时需要格外小心程序死锁这个问题。但要提到协程异步,不得不说到什么是 “协程” (Coroutine)。

协程是什么
协程的全程应该被叫做“协程子程序”,是“协作式多任务子程序”的另一个名字。“Coroutine” 一词是由Melvin Conway于1958年提出汇编语言新架构时提出的,指代“能够随时暂停、恢复的子程序”。

在我们学习编程时,子程序给我们的初印象一般都是“可被复用的代码片段”,它有十分明显的特征:

仅有一个返回值,且仅能返回一次
从头至尾执行
一旦使用return关键词返回,其剩余代码均不再执行
两次执行之间的状态无关,执行结果仅决定于参数
但协程子程序不一样,它可以返回多次而不停止执行,也可以在返回点恢复执行(不从第一行开始执行),两次执行之间的状态会互相关联(虽给定参数一样但执行结果不一样)。

如果大家对Python稍有了解的话,那一定知道生成器的概念,而生成器就是一种协程的架构的实现:

可以返回多次
能够在返回点开始执行,而非代码片段头部开始执行
可以在代码片段中间通过yield关键词返回,其剩余代码会在下次调用时执行
两次执行直接的状态有关,执行结果不单单仅取决于参数
通过协程,可以实现许多十分有意思的功能,且 所有代码均由一个线程执行。

协程如何实现异步
协程天然具备“不从头到位按顺序执行”的特性,所以可以实现“异步”。下述代码即是通过C#中的生成器来实现“生产者-消费者”、并由主线程作为线程调度者的一个简单异步代码示例:

生产者和消费者共享一个队列 q
消费者每次消费 1 个 q 队列中的对象
生产者每次随机生成 0~2 个对象添加至队列 q 中
每个调度循环中,消费者消费2次,生产者生产1次
由于使用了随机数生成器,每次运行的结果会不一样
在Producer和Consumer子程序中,每次程序执行时都是从上一次yield关键词后开始执行,而非从头开始执行。

static IEnumerable<object> Producer(Queue<int> q) // 生产者
{
    while (true)
    {
        if (q.Count < 100)
        {
            RandomNumberGenerator rng = RandomNumberGenerator.Create();
            byte[] num = new byte[1];
            rng.GetBytes(num);
            int n = (int)Math.Round((num[0] / 255.0) * 2);
            byte[] buff = new byte[n];
            rng.GetBytes(buff);
            Console.WriteLine(buff.Aggregate("    Produced:[", (s1, s2) => s1 + $" {s2},") + " ]");
            foreach (int item in buff) q.Enqueue(item);
            Console.WriteLine(q.Aggregate("    Queue:[", (s1, s2) => s1 + $" {s2},") + " ]");
            yield return null; // yield返回,下次进入时,会从此处继续执行
        }
        else if (q == null) yield break;
        else yield return null;
    }
}
static IEnumerable<object> Consumer(Queue<int> q) // 消费者
{
    while (true)
    {
        if (q.Count > 0)
        {
            Console.WriteLine(string.Format("    Consumed: {0}", q.Dequeue()));
            Console.WriteLine(q.Aggregate("    Queue:[", (s1, s2) => s1 + $" {s2},") + " ]");
            yield return null; // yield返回,下次进入时,会从此处继续执行
        }
        else yield break;
    }
}
static void Main(string[] args) // 主线程作为调度者 (Dispatcher)
{
    Queue<int> q = new Queue<int>();                // 共享队列 q
    Console.WriteLine($"Initialization:");
    Producer(q).GetEnumerator().MoveNext();         // 使用生产者生成初始对象
    int maxRunCount = 0;
    while (q.Count > 0 && maxRunCount++ < 500)      // 控制循环
    {
        Console.WriteLine($"Loop {maxRunCount}:");
        Consumer(q).GetEnumerator().MoveNext();     // 消费
        Producer(q).GetEnumerator().MoveNext();     // 生产
    }
}

为什么需要异步
这个场景我们是常常遇到的:

我们想要在一个十分耗时的操作结束后,更新某UI元素。

一般UI是由程序的主线程来维护的,在需要执行这个十分耗时的操作时,我们可以开启一个子线程去做这件事情,并且在子线程结束时对UI进行更新。

但正是因为各个对象是由主线程维护的,一般不允许子线程直接访问UI元素,那么我们在子线程里 无法对UI元素进行更新。

于是聪明的我们直接在子线程开启的同时让主线程等待子线程完成,这样做的结果就是导致这个主线程等待子线程完成的过程中,UI元素会因为主线程在等待而失去对鼠标、键盘事件的响应 —— 窗口处于冻结状态。

那么问题就来了:如何在进行一个耗时操作时,保证主线程不冻结,且耗时操作完成后能更新属于主线程的UI元素。

此时“异步”一个很重要的概念在这个环境下就十分有用了 —— 乱序执行。

下面就是一个使用异步实现读取一个超大文件的一个代码,主程序Main()的执行并没有因ReadHeavy()函数的执行而冻结,“Read finished” 的打印在子函数ReadHeavy()中,子函数被调用的代码是在 “Read file started” 被打印之前,但其真正被执行则在其之后,且在编写这段程序的程序员手里,这段代码仅有一个线程,因此这是一个协程异步程序。

static async Task Main(string[] args)
{
    var task = ReadHeavy();                 // 开始文件读取
    Console.WriteLine("Read file started.");
    await task;
}
async static Task ReadHeavy()
{
    await System.IO.File.ReadAllBytesAsync(@"E:\Downloads\6_26_2018__2_02_17_PM.tdms");
    Console.WriteLine("Read finished.");
}

“6_26_2018__2_02_17_PM.tdms” 是笔者某传感器采集的数据,大约有600MB左右的大小,算得上一个比较大的文件了,而且存储在非SSD磁盘中,所以读取时花费的时间会比较多。

异步大多数情况下是使用多线程实现的
看到这里,相信大家心里已经一万个问号了,前面大费周章介绍了半天异步不是多线程、异步大多数时候指的“协程异步”,怎么到头来又来一个“异步大多数时候是用多线程实现”?这难道不是自相矛盾?

当然不是。这里需要弄清楚的一个很重要的概念 —— 程序员手里的代码与操作系统对处理器硬件的调度执行代码并不是一回事。

异步的“协程”是针对于程序员手里的代码而言,而目前的编程语言对异步的支持大部分时候是通过多线程来实现的。

对于程序员来说,代码仅仅执行在一个线程上 —— 这是代码协程构架。
对于操作系统/代码编译器而言,异步的执行是通过将子程序放入新线程中执行,在执行完毕后,通知主线程,再由主线程来继续执行剩余代码
很难理解对不对?还是上面文件读取的例子,直接上代码

async static Task ReadHeavy()
{
    Console.WriteLine($"In sub, thread id: {Thread.CurrentThread.ManagedThreadId}");
    await System.IO.File.ReadAllBytesAsync(@"E:\Downloads\6_26_2018__2_02_17_PM.tdms");
    Console.WriteLine("Read finished.");
    Console.WriteLine($"In sub, after read, id: {Thread.CurrentThread.ManagedThreadId}");
}

static async Task Main(string[] args)
{
    Console.WriteLine(string.Format("Current Thread: {0}",
        Thread.CurrentThread.ManagedThreadId));
    var dummy = ReadHeavy();
    Console.WriteLine("Read file started.");
    Console.WriteLine($"In main: {Thread.CurrentThread.ManagedThreadId}");
    await dummy;
    Console.WriteLine($"After await, in main: {Thread.CurrentThread.ManagedThreadId}");
    Console.ReadKey();
}

为了能够看清楚到底是哪个线程执行了代码,笔者在之前的代码里加入了大量的打印当前线程的操作。执行结果如下

可见,在 “Read finished.” 打印结束之后,线程编号变了,即便是最后在Main()中的打印也跟着变了。

情况是这样的:

主线程进入Main()
由于遇到var dummy = ReadyHeavy(),主线程进入ReadHeavy()函数
主线程执行打印函数,打印 “In sub, …” 至控制台
【关键点来了】 主线程遇到 await 关键词,主线程直接返回,并将 ReadyAllBytesAsync() 函数交给后台某子线程执行。
主线程由ReadHeavy()返回后,继续按顺序执行打印 “Read file started.”,以及 “In main: …”
主线程遇到 await 关键词,由于这已经是最顶层函数Main(),因此无法返回,此时主线程进入等待
此时由子线程执行的 ReadyAllBytesAsync() 完成,子线程继续执行后续打印 “Read finished.” 以及 “In sub, after read, …”
子线程遇到 ReadHeavy() 函数的尾部,结束执行函数,并通知一直在等待的主线程
【关键点又来】主线程收到子线程发来的贺电,直接退出,将Main()及其所有资源交由子线程处理,此时这个子线程“升级”为新的主线程,负责执行后续代码
相信看到这里大家已经明白了,为什么整个程序员代码中,仅仅只有一个线程,因为除了主线程之外,代码编写者根本无需关心其他线程,整体对于代码编写者而言,其仅仅“感知”到一个线程的存在,这是标准的协程异步。

而在底层的实现中,操作系统的的确确是调用了另一个线程去执行程序中“异步”部分的代码。但是很巧妙的是,在异步执行结束时,原来的主线程直接被子线程取而代之,给人的感觉上是仅有一个线程在做所有的事情,且主线程也一直都可以响应事件。这也是为什么上文中一直在使用“一个线程”而非“同一个线程”措辞的缘由。

async/await关键词的配对出现就是用来告诉编译器这种异步的情况,通过async来表明这个函数是可以从中间返回,也可以从中间开始继续执行,而await关键词来表明这是一个函数的“暂停”点。

一个现实生活中稍微有点形象的例子
作为总结,笔者举个现实生活中一个例子 —— 银行的工作窗口,来说明这一切的一切的区别。

假设我现在去银行柜台窗口办业务,一个柜员接待了我,这个柜员就可以看作是主线程(UI),在负责跟我(用户)进行互动。我提出了一个需要取20万现金的请求,由于数额比较大,需要有人清点现钞。

【单线程】:此时柜员直接自己去清点,花了15分钟,然后把钱给我,中间这15分钟我被晾在了一边,我对着一个空的窗口,十分尴尬。
花费15分钟拿到现钞。

【多线程,死锁】:此时柜员喊了3位同事,四个人一起清点,花了5分钟,然后他们四个同时把自己点好的那一部分试图递给我,但是因为窗口太小,他们四个为了争着第一个给我而产生了争执,并且一直都没有吵出来个结果,我一直被晾在一边。
一直没能拿到现钞。

【多线程,合理管理】:此时柜员喊了3位同事,四个人一起清点,花了5分钟,由于提前商量好了,他们把点完的钱给其中之前接待我的那位柜员,然后这位柜员把钱递到了我手中。但这个过程中,我仍然对着一个空窗口尴尬了5分钟。
花费了5分钟拿到了现钞。

【经典异步】:此时柜员喊了1位同事,将清点现钞的事情交给这位同事处理,交代完事情之后,继续回到窗口与我互动。在同事花费15分钟清点完毕后,柜员接过现金,将现金转交给我。整个过程柜员一直与我互动。
花费了15分钟拿到了现钞。

【多线程异步】:此时柜员喊了1位同事,将清点现钞的事情交给这位同事处理,交代完事情之后,继续回到窗口与我互动。在同事花费清点完毕后,由同事直接把现金交给我,并且他/她坐下来作为柜员继续与我进行后续互动,原来的柜员去后台干别的了。整个过程中,始终有一个人与我互动,但前半段是柜员A,后半段是A的同事B。
花费15分钟拿到了现钞。

【多线程异步,进一步提高效率】:此时柜员喊了3位同事,由这三位同事负责清点,花了7分钟,由于提前商量好了,他们把清点完的现钞交由他们其中一个人,由这位柜员将现钞由窗口递给我。在这7分钟中,原来的柜员一直与我互动,我收到现钞后,由递交给我现钞的那位柜员坐下继续负责与我互动,其余柜员去后台干别的去了。整个过程中始终又一位柜员与我互动,前半段是柜员A,后半段是A的同事B/C/D中提前商量好负责交接的那位。
花费7分钟拿到现钞。

总而言之,异步是为了解决主线程(UI)冻结而提出的基于协程的架构,大部分时候底层是通过多线程实现的。

实际工作中,我们其实记住一个点就可以很轻易分辨我们到底更需要关注哪种技术的实现:

I/O密集型操作 —— 异步
计算密集型操作 —— 多线程
————————————————

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2076317.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【解析几何笔记】8.向量的投影与内积

8. 向量的投影与内积 复习前面的知识&#xff1a;&#xff0c;若BCE三点共线&#xff0c;则 A E ⃗ ( 1 − s ) A B ⃗ s A C ⃗ , ( B , C , E ) μ ⇒ s μ 1 μ , 1 − s 1 1 μ \vec{AE}(1-s)\vec{AB}s\vec{AC},(B,C,E)\mu\Rightarrow s\frac{\mu}{1\mu},1-s\frac…

【案例59】WebSphere类加载跟踪开启方法

问题现象 WAS加载代码时&#xff0c;模块开发怀疑是WebSphere本身加载某个类的代码出现了问题。但不知道怎么排查。故寻求帮助。 问题分析 WebSphere本身是提供相关类加载跟踪的方法的。 解决方案 经过排查资料。如果实际诊断中&#xff0c;能够明确断定是某个类的加载出了…

MySQL集群技术详解

目录 一、MySQL在服务器中的部署方法 1.1 编译安装MySQL 1.2 部署MySQL 二、MySQL主从复制 2.1 配置master 2.2 配置slave 2.3 添加slave2 测试&#xff1a; 2.4 延迟复制 2.5 慢查询日志 2.6 MySQL的并行复制 2.7 MySQL主从复制原理剖析 2.8 架构缺陷 三、MySQL…

猫咪掉毛严重,新手铲屎官不知如何处理?推荐使用宠物空气净化器

把小猫接回来一起生活没几天&#xff0c;我就感觉好日子就到头了...猫咪掉毛怎么这么严重啊&#xff0c;我都不敢怎么撸它&#xff0c;一撸满天都是毛&#xff0c;轻轻一搓就是一大团。而且想到还要清理就很头疼&#xff0c;每天都要很多的时间搞卫生。尝试过把它的毛剪短&…

【时时三省】(C语言基础)指针进阶3

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 一级指针的传参 示例: 这就是一个一级指针传参 思考:当一个函数的参数部分为一级指针的时候&#xff0c;函数能接受什么参数&#xff1f; 二级指针的传参 二级指针示例: pa是一级指针 p…

K8S 1.31 新功能: 跨核分发CPU

​在Kubernetes的最新版本1.31中&#xff0c;一个超酷的新功能&#xff0c;叫做CPUManager的静态策略&#xff0c;里面有个选项叫做distribute-cpus-across-cores。虽然这个功能现在还在测试阶段&#xff0c;也就是alpha版&#xff0c;而且默认是藏起来的&#xff0c;但它的目的…

Backtrader 实现和理解海龟交易法

Backtrader 实现和理解海龟交易法 1. 海龟交易的理解 &#xff08;1&#xff09;资金管理 海龟将总资金分为N个交易单位&#xff0c;每个单位即称为头寸&#xff0c;划分的标准主要是参考标的的波动性。 波动性用一个指标量化即真实波动幅度均值&#xff08;ATR&#xff09;…

SSRF - 服务器端请求伪造

目录 SSRF dict协议 file协议 gopher协议 工具Gopherus 练习 练习1 练习2 docker镜像加速的方法 SSRF SSRF(Server-Side Request Forgery:服务器端请求伪造) 其形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能,但又没有对目标地址做严格过滤与限制&…

【物理学】什么是运动学和动力学?

Kinematics 和 Kinetics 是力学中的两个重要分支&#xff0c;它们虽然都涉及物体的运动&#xff0c;但关注的方面不同。 Kinematics&#xff08;运动学&#xff09; Kinematics 主要研究物体的运动&#xff0c;而不涉及导致运动的力。它关注的是运动的几何特性&#xff0c;比…

UE5学习笔记18-使用FABRIK确定骨骼的左手位置

一、在武器的骨骼资产中创建一个新的插槽 二、在动画类中添加代码 xxx.h UPROPERTY(BlueprintReadOnly, Category Character, meta (AllowPrivateAccess "true"))/** 蓝图只读 类型是Character 允许私有访问 */ FTransform LeftHandTransform;//拿武器时知道左手…

STL经典案例(三)——俺是歌手挑战赛管理系统(涉及的STL内容较多,篇幅有点长,耐心看完,相信我,一定会有收获的!)

一、需求&#xff1a;俺是歌手挑战赛比赛规则如下 目前共计12名选手报名参赛&#xff0c;选手编号为1-12号比赛分为A和B两组&#xff0c;每组6人&#xff0c;选手随机抽签进行分组比赛共两轮赛事&#xff0c;第一轮为淘汰赛&#xff0c;第二轮为决赛&#xff0c;淘汰赛中每组前…

Git —— 2、创建本地版本库

版本库 版本库又名仓库&#xff0c;英文名repository&#xff0c;这个仓库里面的所有文件都可以被Git管理起来&#xff0c;每个文件的修改.删除&#xff0c;Git都能跟踪&#xff0c;以便任何时刻都可以追踪历史&#xff0c;或者在将来某个时刻可以“还原“。   创建本地版本库…

day31-测试之性能测试工具JMeter的功能概要、元件作用域和执行顺序

目录 一、JMeter的功能概要 1.1.文件目录介绍 1).bin目录 2).docs目录 3).printable_docs目录 4).lib目录 1.2.基本配置 1).汉化 2).主题修改 1.3.基本使用流程 二、JMeter元件作用域和执行顺序 2.1.名称解释 2.2.基本元件 2.3.元件作用域 1).核心 2).提示 3).作用域的原则 2.…

【AI绘画】Midjourney前置/imagine与单图指令详解

文章目录 &#x1f4af;Midjourney前置指令/imagine什么是前置指令&#xff1f;/imaginepromptUpscale(放大)Variations&#xff08;变化&#xff09;&#x1f504;&#xff08;重新生成一组图片&#xff09; &#x1f4af;单张图片指令Upscale (细节优化)Vary&#xff08;变体…

计算机二级真题--程序填空大题 章节

每类题有一些规律&#xff0c;这里来总结一下个人做题遇到的一些规律&#xff0c;大家可以自行掌握 1.在while循环里常常会将将最后一行空着作为考点&#xff0c;例如下面第的10题&#xff0c;因为需要联系整个循环所以经常分析不出来&#xff0c;实际上for训话中也有过这种考…

无需标注数据:引领视频编辑模型达到新高度

人工智能咨询培训老师叶梓 转载标明出处 由于标注视频编辑数据的稀缺&#xff0c;开发精确且多样化的视频编辑模型一直是个挑战。目前研究者们大多聚焦于无需训练的方法&#xff0c;但这些方法在性能和编辑能力范围上都有所限制。为此Meta AI的研究团队提出了一种新的方法&…

Code Llama: Open Foundation Models for Code论文阅读

整体介绍 Code Llama 发布了3款模型&#xff0c;包括基础模型、Python 专有模型和指令跟随模型&#xff0c;参数量分别为 7B、13B、34B 和 70B。这些模型在长达 16k tokens 的序列上训练。都是基于 Llama 2。 作者针对infilling (FIM) 、长上下文、指令专门做了微调 long-con…

CSS Sprite(精灵图)

CSS Sprite也叫CSS精灵图、CSS雪碧图&#xff0c;是一种网页图片应用处理方式。它允许你将一个页面涉及到的所有零星图片都包含到一张大图中去 优点&#xff1a; 减少图片的字节减少网页的http请求&#xff0c;从而大大的提高页面的性能 原理&#xff1a; 通过background-i…

《黑神话悟空》幽魂怎么打?大头怪幽魂打法攻略

黑神话悟空第一章中许多玩家都容易被大头怪幽魂给难住&#xff0c;这算是渡劫中的第一难&#xff0c;所以不少玩家想了解黑神话悟空大头怪怎么打&#xff1f;其实大头怪打起来并不难&#xff0c;只是很多的玩家们没有了解他的弱点以及特性。小编今天就为大家整理了大头怪幽魂的…

吴艳妮牵手创维,奥运后的首个代言为什么是她?

​近日&#xff0c;吴艳妮参加奥运会的表彰大会&#xff0c;晒出了与孙颖莎、王楚钦等运动员的合照&#xff0c;并写道&#xff1a;“最幸福的一天”在网络上引发讨论&#xff0c;让人不禁想起在巴黎奥运会刚结束的时候&#xff0c;家电巨头创维官宣她作为其品牌大使的消息也是…