async/await 在 C# 语言中是如何工作的?(下)

news2024/11/15 11:06:46

接《async/await 在 C# 语言中是如何工作的?(上)》、《async/await 在 C# 语言中是如何工作的?(中)》,今天我们继续介绍 SynchronizationContext 和 ConfigureAwait。

 

▌SynchronizationContext 和 ConfigureAwait

我们之前在 EAP 模式的上下文中讨论过 SynchronizationContext,并提到它将再次出现。SynchronizationContext 使得调用可重用的辅助函数成为可能,并自动被调度回调用环境认为合适的任何地方。因此,我们很自然地认为 async/await 能“正常工作”,事实也的确如此。回到前面的按钮单击处理程序:

ThreadPool.QueueUserWorkItem(_ =>{    string message = ComputeMessage();    button1.BeginInvoke(() =>    {        button1.Text = message;    });});

使用 async/await,我们可以这样写:

button1.Text = await Task.Run(() => ComputeMessage());

对 ComputeMessage 的调用被转移到线程池中,这个方法执行完毕后,执行又转移回与按钮关联的 UI 线程,设置按钮的 Text 属性就是在这个线程中进行的。

与 SynchronizationContext 的集成由 awaiter 实现(为状态机生成的代码对 SynchronizationContext 一无所知),因为当所表示的异步操作完成时,是 awaiter 负责实际调用或将所提供的 continuation 排队。而自定义 awaiter 不需要考虑 SynchronizationContext。目前,Task、Task<TResult>、ValueTask、ValueTask<TResult> 的等待器都是 do。这意味着,默认情况下,当你等待一个任务,一个 Task<TResult>,一个 ValueTask,一个 ValueTask<TResult>,甚至 Task. yield() 调用的结果时,awaiter 默认会查找当前的 SynchronizationContext,如果它成功地获得了一个非默认的同步上下文,最终会将 continuation 排队到该上下文。

如果我们查看 TaskAwaiter 中涉及的代码,就可以看到这一点。以下是 Corelib 中的相关代码片段:​​​​​​​

internal void UnsafeSetContinuationForAwait(IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext){    if (continueOnCapturedContext)    {        SynchronizationContext? syncCtx = SynchronizationContext.Current;        if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext))        {            var tc = new SynchronizationContextAwaitTaskContinuation(syncCtx, stateMachineBox.MoveNextAction, flowExecutionContext: false);            if (!AddTaskContinuation(tc, addBeforeOthers: false))            {                tc.Run(this, canInlineContinuationTask: false);            }            return;        }        else        {            TaskScheduler? scheduler = TaskScheduler.InternalCurrent;            if (scheduler != null && scheduler != TaskScheduler.Default)            {                var tc = new TaskSchedulerAwaitTaskContinuation(scheduler, stateMachineBox.MoveNextAction, flowExecutionContext: false);                if (!AddTaskContinuation(tc, addBeforeOthers: false))                {                    tc.Run(this, canInlineContinuationTask: false);                }                return;            }        }    }
    ...}

这是一个方法的一部分,用于确定将哪个对象作为 continuation 存储到任务中。它被传递给 stateMachineBox,如前所述,它可以直接存储到任务的 continuation 列表中。但是,这个特殊的逻辑可能会将 IAsyncStateMachineBox 封装起来,以合并一个调度程序(如果存在的话)。它检查当前是否有非默认的 SynchronizationContext,如果有,它会创建一个 SynchronizationContextAwaitTaskContinuation 作为实际的对象,它会被存储为 continuation;该对象依次包装了原始的和捕获的 SynchronizationContext,并知道如何在与后者排队的工作项中调用前者的 MoveNext。这就是如何在 UI 应用程序中作为事件处理程序的一部分等待,并在等待完成后让代码继续在正确的线程上运行。这里要注意的下一个有趣的事情是,它不仅仅关注一个 SynchronizationContext:如果它找不到一个自定义的 SynchronizationContext 来使用,它还会查看 Tasks 使用的 TaskScheduler 类型是否有一个需要考虑的自定义类型。和 SynchronizationContext 一样,如果有一个非默认值,它就会和原始框一起包装在 TaskSchedulerAwaitTaskContinuation 中,用作 continuation 对象。

但这里最值得注意的可能是方法主体的第一行:if (continueOnCapturedContext)。我们只在 continueOnCapturedContext 为 true 时才对 SynchronizationContext/TaskScheduler 进行这些检查;如果这个值为 false,实现方式就好像两者都是默认值一样,会忽略它们。请问是什么将 continueOnCapturedContext 设置为 false?你可能已经猜到了:使用非常流行的 ConfigureAwait(false)。

可以这样说,作为 await 的一部分,ConfigureAwait(false) 做的唯一一件事是将它的参数布尔值作为 continueOnCapturedContext 值提供给这个函数(以及其他类似的函数),以便跳过对 SynchronizationContext/TaskScheduler 的检查,表现得好像它们都不存在一样。对于进程来说,这允许 Task 在它认为合适的地方调用其 continuation,而不是强制将它们排队在某个特定的调度器上执行。

我之前提到过 SynchronizationContext 的另一个方面,我说过我们会再次看到它:OperationStarted/OperationCompleted。现在是时候了。这是没那么受欢迎的特性:异步 void。除了 configureawait 之外,async void 可以说是 async/await 中最具争议性的特性之一。它被添加的原因只有一个:事件处理程序。在 UI 应用程序中,你可以编写如下代码:​​​​​​​

button1.Click += async (sender, eventArgs) =>{  button1.Text = await Task.Run(() => ComputeMessage());  };

但如果所有的异步方法都必须有一个像 Task 这样的返回类型,你就不能这样做了。Click 事件有一个签名 public event EventHandler? Click;,其中 EventHandler 定义为 public delegate void EventHandler(object? sender, EventArgs e);,因此要提供一个符合该签名的方法,该方法需要是 void-returning。

有各种各样的理由认为 async void 是不好的,为什么文章建议尽可能避免使用它,以及为什么出现了各种 analyzers 来标记使用 async void。最大的问题之一是委托推理。考虑下面的程序:​​​​​​​

using System.Diagnostics;
Time(async () =>{    Console.WriteLine("Enter");    await Task.Delay(TimeSpan.FromSeconds(10));    Console.WriteLine("Exit");});
static void Time(Action action){    Console.WriteLine("Timing...");    Stopwatch sw = Stopwatch.StartNew();    action();    Console.WriteLine($"...done timing: {sw.Elapsed}");}

人们很容易期望它输出至少10秒的运行时间,但如果你运行它,你会发现输出是这样的:​​​​​​​

Timing...Enter...done timing: 00:00:00.0037550

async lambda 实际上是一个异步 void 方法。异步方法会在遇到第一个暂停点时返回调用者。如果这是一个异步 Task 方法,Task 就会在这个时间点返回。但对于 async void,什么都不会返回。Time 方法只知道它调用了 action();委托调用返回;它不知道 async 方法实际上仍在“运行”,并将在稍后异步完成。

这就是 OperationStarted/OperationCompleted 的作用。这种异步 void 方法本质上与前面讨论的 EAP 方法类似:这种方法的初始化是 void,因此需要一些其他机制来跟踪所有此类操作。因此,EAP 实现在操作启动时调用当前 SynchronizationContext 的 OperationStarted,在操作完成时调用 OperationCompleted,async void 也做同样的事情。与 async void 相关的构建器是 AsyncVoidMethodBuilder。还记得在 async 方法的入口,编译器生成的代码如何调用构建器的静态 Create 方法来获得适当的构建器实例吗?AsyncVoidMethodBuilder 利用了这一点来挂钩创建和调用 OperationStarted:​​​​​​​

public static AsyncVoidMethodBuilder Create(){    SynchronizationContext? sc = SynchronizationContext.Current;    sc?.OperationStarted();    return new AsyncVoidMethodBuilder() { _synchronizationContext = sc };}

类似地,当通过 SetResult 或 SetException 将构建器标记为完成时,它会调用相应的 OperationCompleted 方法。这就是像 xunit 这样的单元测试框架如何能够具有异步 void 测试方法,并仍然在并发测试执行中使用最大程度的并发,例如在 xunit 的 AsyncTestSyncContext 中。

有了这些知识,现在可以重写我们的 timing 示例:​​​​​​​

using System.Diagnostics;
Time(async () =>{    Console.WriteLine("Enter");    await Task.Delay(TimeSpan.FromSeconds(10));    Console.WriteLine("Exit");});
static void Time(Action action){    var oldCtx = SynchronizationContext.Current;    try    {        var newCtx = new CountdownContext();        SynchronizationContext.SetSynchronizationContext(newCtx);
        Console.WriteLine("Timing...");        Stopwatch sw = Stopwatch.StartNew();                action();        newCtx.SignalAndWait();
        Console.WriteLine($"...done timing: {sw.Elapsed}");    }    finally    {        SynchronizationContext.SetSynchronizationContext(oldCtx);    }}
sealed class CountdownContext : SynchronizationContext{    private readonly ManualResetEventSlim _mres = new ManualResetEventSlim(false);    private int _remaining = 1;
    public override void OperationStarted() => Interlocked.Increment(ref _remaining);
    public override void OperationCompleted()    {        if (Interlocked.Decrement(ref _remaining) == 0)        {            _mres.Set();        }    }
    public void SignalAndWait()    {        OperationCompleted();        _mres.Wait();    }}

在这里,我已经创建了一个 SynchronizationContext,它跟踪了一个待定操作的计数,并支持阻塞等待它们全部完成。当我运行它时,我得到这样的输出:​​​​​​​

Timing...EnterExit...done timing: 00:00:10.0149074

 

▌State Machine Fields

至此,我们已经看到了生成的入口点方法,以及 MoveNext 实现中的一切是如何工作的。我们还了解了在状态机上定义的一些字段。让我们仔细看看这些。

对于前面给出的 CopyStreamToStream 方法:​​​​​​​

public async Task CopyStreamToStreamAsync(Stream source, Stream destination){    var buffer = new byte[0x1000];    int numRead;    while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0)    {        await destination.WriteAsync(buffer, 0, numRead);    }}

下面是我们最终得到的字段:​​​​​​​

private struct <CopyStreamToStreamAsync>d__0 : IAsyncStateMachine{    public int <>1__state;    public AsyncTaskMethodBuilder <>t__builder;    public Stream source;    public Stream destination;    private byte[] <buffer>5__2;    private TaskAwaiter <>u__1;    private TaskAwaiter<int> <>u__2;
    ...}

< > 1 __state。是“状态机”中的“状态”。它定义了状态机所处的当前状态,最重要的是下次调用 MoveNext 时应该做什么。如果状态为-2,则操作完成。如果状态是-1,要么是我们第一次调用 MoveNext,要么是 MoveNext 代码正在某个线程上运行。如果你正在调试一个 async 方法的处理过程,并且你看到状态为-1,这意味着在某处有某个线程正在执行包含在方法中的代码。如果状态大于等于0,方法会被挂起,状态的值会告诉你在什么时候挂起。虽然这不是一个严格的规则(某些代码模式可能会混淆编号),但通常情况下,分配的状态对应于从0开始的 await 编号,按照源代码从上到下的顺序排列。例如,如果 async 方法的函数体完全是:​​​​​​​

await A();await B();await C();await D();

你发现状态值是2,这几乎肯定意味着 async 方法当前被挂起,等待从 C() 返回的任务完成。

< > t__builder。这是状态机的构建器,例如用于 Task 的 AsyncTaskMethodBuilder,用于 ValueTask 的 AsyncValueTaskMethodBuilder<TResult>,用于 async void 方法的 AsyncVoidMethodBuilder,或用于 async 返回类型的 AsyncMethodBuilder(…)] 或通过 async 方法本身的属性覆盖的任何构建器。如前所述,构建器负责 async 方法的生命周期,包括创建 return 任务,最终完成该任务,并充当暂停的中介,async 方法中的代码要求构建器暂停,直到特定的 awaiter 完成。

编译器完全按照参数名称的指定来命名它们。如前所述,所有被方法主体使用的参数都需要被存储到状态机中,以便 MoveNext 方法能够访问它们。注意我说的是 "被使用"。如果编译器发现一个参数没有被异步方法的主体使用,它就可以优化,不需要存储这个字段。例如,给定下面的方法:​​​​​​​

public async Task M(int someArgument){    await Task.Yield();}

编译器会将这些字段发送到状态机:​​​​​​​

private struct <M>d__0 : IAsyncStateMachine{    public int <>1__state;    public AsyncTaskMethodBuilder <>t__builder;    private YieldAwaitable.YieldAwaiter <>u__1;    ...}

请注意,这里明显缺少名为 someArgument 的参数。但是,如果我们改变 async 方法,让它以任何方式使用实参:​​​​​​​

public async Task M(int someArgument){    Console.WriteLine(someArgument);    await Task.Yield();}

它显示:​​​​​​​

private struct <M>d__0 : IAsyncStateMachine{    public int <>1__state;    public AsyncTaskMethodBuilder <>t__builder;    public int someArgument;    private YieldAwaitable.YieldAwaiter <>u__1;    ...}

<buffer>5__2;。这是缓冲区的 "局部",它被提升为一个字段,这样它就可以在等待点上存活。编译器相当努力地防止状态被不必要地提升。注意,在源码中还有一个局部变量 numRead,在状态机中没有相应的字段。为什么?因为它没有必要。这个局部变量被设置为 ReadAsync 调用的结果,然后被用作 WriteAsync 调用的输入。在这两者之间没有 await,因此 numRead 的值需要被存储。就像在一个同步方法中,JIT 编译器可以选择将这样的值完全存储在一个寄存器中,而不会真正将其溢出到堆栈中,C# 编译器可以避免将这个局部变量提升为一个字段,因为它不需要在任何等待中保存它的值。一般来说,如果 C# 编译器能够证明局部变量的值不需要在等待中保存,它就可以省略局部变量的提升。

<>u__1和<>u__2。async 方法中有两个 await:一个用于 ReadAsync 返回的 Task<int>,另一个用于 WriteAsync 返回的 Task。Task. getawaiter() 返回一个 TaskAwaiter,Task<TResult>. getawaiter() 返回一个 TaskAwaiter<TResult>,两者都是不同的结构体类型。由于编译器需要在 await (IsCompleted, UnsafeOnCompleted) 之前获取这些 awaiter,然后需要在 await (GetResult) 之后访问它们,因此需要存储这些 awaiter。由于它们是不同的结构类型,编译器需要维护两个单独的字段来做到这一点(另一种选择是将它们装箱,并为 awaiter 提供一个对象字段,但这会导致额外的分配成本)。不过,编译器会尽可能地重复使用字段。如果我有:​​​​​​​

public async Task M(){    await Task.FromResult(1);    await Task.FromResult(true);    await Task.FromResult(2);    await Task.FromResult(false);    await Task.FromResult(3);}

有五个等待,但只涉及两种不同类型的等待者:三个是 TaskAwaiter<int>,两个是 TaskAwaiter<bool>。因此,状态机上最终只有两个等待者字段:

private struct <M>d__0 : IAsyncStateMachine{    public int <>1__state;    public AsyncTaskMethodBuilder <>t__builder;    private TaskAwaiter<int> <>u__1;    private TaskAwaiter<bool> <>u__2;    ...}

然后,如果我将我的示例改为:​​​​​​​

public async Task M(){    await Task.FromResult(1);    await Task.FromResult(true);    await Task.FromResult(2).ConfigureAwait(false);    await Task.FromResult(false).ConfigureAwait(false);    await Task.FromResult(3);}

仍然只涉及 Task<int>s 和 Task<bool>s,但实际上我使用了四个不同的 struct awaiter 类型,因为从 ConfigureAwait 返回的东西上的 GetAwaiter() 调用返回的 awaiter 与 Task.GetAwaiter() 返回的是不同的类型…从编译器创建的 awaiter 字段可以再次很明显的看出:​​​​​​​

private struct <M>d__0 : IAsyncStateMachine{    public int <>1__state;    public AsyncTaskMethodBuilder <>t__builder;    private TaskAwaiter<int> <>u__1;    private TaskAwaiter<bool> <>u__2;    private ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter <>u__3;    private ConfiguredTaskAwaitable<bool>.ConfiguredTaskAwaiter <>u__4;    ...}

如果您发现自己想要优化与异步状态机相关的大小,您可以查看的一件事是是否可以合并正在等待的事情,从而合并这些 awaiter 字段。

您可能还会看到在状态机上定义的其他类型的字段。值得注意的是,您可能会看到一些字段包含单词“wrap”。考虑下面这个例子:

public async Task<int> M() => await Task.FromResult(42) + DateTime.Now.Second;

这将生成一个包含以下字段的状态机:​​​​​​​

private struct <M>d__0 : IAsyncStateMachine{    public int <>1__state;    public AsyncTaskMethodBuilder<int> <>t__builder;    private TaskAwaiter<int> <>u__1;    ...}

到目前为止没有什么特别的。现在颠倒一下添加表达式的顺序:

public async Task<int> M() => DateTime.Now.Second + await Task.FromResult(42);

这样,你就得到了这些字段:​​​​​​​

private struct <M>d__0 : IAsyncStateMachine{    public int <>1__state;    public AsyncTaskMethodBuilder<int> <>t__builder;    private int <>7__wrap1;    private TaskAwaiter<int> <>u__1;    ...}

我们现在有了另一个函数:<>7__wrap1。为什么?因为我们计算了 DateTime.Now 的值。其次,只有在计算完它之后,我们才需要等待一些东西,并且需要保留第一个表达式的值,以便将其与第二个表达式的结果相加。因此,编译器需要确保第一个表达式的临时结果可以添加到 await 的结果中,这意味着它需要将表达式的结果溢出到临时中,它使用 <>7__wrap1 字段做到了这一点。如果你发现自己对异步方法的实现进行了超优化,以减少分配的内存量,你可以寻找这样的字段,并查看对源代码的微调是否可以避免溢出的需要,从而避免这种临时的需要。

我希望这篇文章有助于解释当你使用 async/await 时背后到底发生了什么。这里有很多变化,所有这些结合在一起,创建了一个高效的解决方案,可以编写可拓展的异步代码,而不必处理回调。然而归根结底,这些部分实际上是相对简单的:任何异步操作的通用表示,一种能够将普通控制流重写为协程的状态机实现的语言和编译器,以及将它们绑定在一起的模式。其他一切都是优化的额外收获。

编程愉快!

点我前往原博客~

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

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

相关文章

【SVN已解决】修改svn服务端地址为ip或者域名地址的方法

介绍 这里是小编成长之路的历程&#xff0c;也是小编的学习之路。希望和各位大佬们一起成长&#xff01; 以下为小编最喜欢的两句话&#xff1a; 要有最朴素的生活和最遥远的梦想&#xff0c;即使明天天寒地冻&#xff0c;山高水远&#xff0c;路远马亡。 一个人为什么要努力&a…

Vue之指令详解与自定义指令

指令 想要了解自定义指令&#xff0c;那肯定得先明白什么是指令。 指令的本质&#xff1a;语法糖&#xff0c;标志位。在编译阶段 render 函数里&#xff0c;会把指令编译成 JavaScript 代码。 常见的Vue内置指令有&#xff1a; v-on 即 。v-on:click”function“&#xff…

Node【Express框架【二】】

文章目录 &#x1f31f;前言&#x1f31f;中间件&#x1f31f;中间件函数&#x1f31f;什么是中间件函数&#x1f31f;中间件函数可以做什么 &#x1f31f;Express中间件的类型&#x1f31f;应用级中间件&#x1f31f;路由器级中间件&#x1f31f;错误处理中间件&#x1f31f;内…

人为惨案之kube-controller-manager 不断重启根因溯源

文章目录 背景问题发现排查CSI provision排查kube-controller-manager查看controller log紧急恢复求助chatgpt 背景 2023年4月21日10:38:07&#xff0c;在集群中测试RBAC的时候&#xff0c;在kuboard的界面神出鬼没的删除了几个clusterRole。练习一个CKA的练习题目. Create a…

如何实现计算机上的文件共享

文件共享 第一步&#xff1a;设置无线热点第二步&#xff1a;设置本地用户权限第三步&#xff1a;设置共享文件夹第四步&#xff1a;打开自己的移动热点&#xff0c;并且让对方连接自己的热点第五步&#xff1a;让对方的电脑进行连接自己共享的文件 第一步&#xff1a;设置无线…

React-Redux详解

React-Redux详解 前言 React-Redux是一个用于在React应用中管理状态的第三方库。它是基于Redux架构的&#xff0c;提供了一种在React应用中高效管理状态的方式。React-Redux通过将Redux的核心概念和React组件相结合&#xff0c;使得在React应用中使用Redux变得更加简单和方便。…

从WebGL到Babylonjs

从WebGL到Babylonjs 一、关于WebGL 前世今生 OpenGL > OpenGL ES > WebGL本质&#xff1a;通过js代码去调用OpenGL的一系列Api 二、WebGL程序的构成 1、一个简单的webgl程序 const canvas document.querySelector(canvas); const gl canvas.getContext(webgl2); c…

llama.cpp一种在本地CPU上部署的量化模型(超低配推理llama)

0x00 背景 前不久&#xff0c;Meta前脚发布完开源大语言模型LLaMA&#xff0c; 随后就被网友“泄漏”&#xff0c;直接放了一个磁力链接下载链接。 然而那些手头没有顶级显卡的朋友们&#xff0c;就只能看看而已了 但是 Georgi Gerganov 开源了一个项目llama.cpp ggergano…

HBase高可用

一、HBase高可用简介 HBase集群如果只有一个master&#xff0c;一旦master出现故障&#xff0c;将导致整个集群无法使用&#xff0c;所以在实际的生产环境中&#xff0c;需要搭建HBase的高可用&#xff0c;也就是让HMaster高可用&#xff0c;也就是需要再选择一个或多个节点也…

【C语言】那些 “虾仁猪心“ 的坑题

本章介绍 最近翻笔记&#xff0c;整理了下那些日子里面掉过的坑题&#xff0c;说多都是泪&#xff01;&#xff01;也许是自己的储备知识不足&#xff0c;才造成的大坑&#xff0c;今天把题拿出来给大家溜溜&#xff0c;看大家做时候有没有踩坑&#xff01; 文章目录 1:第一题2…

项目笔记-瑞吉外卖

文章目录 1.业务开发day011.软件开发整体介绍2.项目整体介绍:star:3.开发环境搭建4.登录功能&#xff1a;star4.1代码实现 5.退出功能6.页面效果出现 1.业务开发 day01 1.软件开发整体介绍 2.项目整体介绍⭐️ 后端&#xff1a;管理菜品和员工信息前台&#xff1a;通过手机端…

根据cadence设计图学习硬件知识 day03 了解 一些芯片 和 数据手册下载的地方

1. MT53D512M32D2DS 芯片&#xff08;动态随机存取存储器&#xff09;的技术指标 1.1 16n Prefetch (预加载) (n --芯片位宽) DDR 体系 链接&#xff1a;DDR扫盲—-关于Prefetch(预取)与Burst(突发)的深入讨论_ddr prefetch_qq_25814297-npl的博客-CSDN博客 1.2 每个通…

网络层重点协议之【IP协议】

0. IP地址组成 IP地址分为两个部分&#xff0c;网络号和主机号 网络号&#xff1a;标识网段&#xff0c;保证相互连接的两个网段具有不同的标识主机号&#xff1a;标识主机&#xff0c;同一网段内&#xff0c;主机之间具有相同的网络号&#xff0c;但是必须有不同的主机号 一…

浅谈: 计算机—JVM—线程

计算机存储模型(CPU、寄存器、高速缓存、内存、外存) 现代计算机系统CPU和内存之间其实是有一个cache的层级结构的。比内存速度更快的存储介质(SRAM)&#xff0c;普通内存一般是DRAM&#xff0c;这种读写速度更快的介质充当CPU和内存之间的Cache&#xff0c;这就是缓存。当CPU…

医学图像分割之TransUNet

目录 一、背景 二、提出问题 三、解决问题 四、网络结构详解 CNN部分&#xff08;ResNet50的前三层&#xff09; transformer部分 U-Net的decoder部分 五、模型性能 开头处先说明下TransUNet的地位&#xff1a;它是第一个将transformer用于U型结构的网络。 一、背景 医…

【Java|golang】1105. 填充书架---动态规划

给定一个数组 books &#xff0c;其中 books[i] [thicknessi, heighti] 表示第 i 本书的厚度和高度。你也会得到一个整数 shelfWidth 。 按顺序 将这些书摆放到总宽度为 shelfWidth 的书架上。 先选几本书放在书架上&#xff08;它们的厚度之和小于等于书架的宽度 shelfWidt…

PCL点云库(1) — 简介与数据类型

目录 1.1 简介 1.2 PCL安装 1.2.1 安装方法 1.2.2 测试程序 1.3 PCL数据类型 1.4 PCL中自定义point类型 1.4.1 增加自定义point的步骤 1.4.2 完整代码 1.1 简介 来源&#xff1a;PCL&#xff08;点云库&#xff09;_百度百科 PCL&#xff08;Point Cloud Library&…

Vue组件间通信的7种方法(全)

大厂面试题分享 面试题库 前后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 web前端面试题库 VS java后端面试题库大全 组件之前的通信方法 1. props/$emit 父传子 props 这个只能够接收父组件传来的数据 不能进…

6.4 一阶方程组与高阶方程的数值解法

学习目标&#xff1a; 学习一阶方程组与高阶方程的数值解法的目标可以分为以下几个方面&#xff1a; 掌握一阶方程组和高阶方程的基本概念和求解方法&#xff1b;理解数值解法的概念和原理&#xff0c;了解常见的数值解法&#xff1b;掌握欧拉方法、改进欧拉方法和龙格-库塔方…

Viu联合华为HMS生态,共创影音娱乐新体验

华为HMS生态携手流媒体平台Viu&#xff0c;为海外消费者打造精品移动娱乐应用体验&#xff0c;并助力提升流量变现能力。Viu在中东非、东南亚等16个国家及地区提供广告合作和付费会员服务&#xff0c;支持优质视频内容高清点播和直播。自2019年起&#xff0c;Viu在中东非区域与…