首页 > 解决方案 > 异步代码是否在 UI 线程或新/不同线程中运行以不阻塞 UI?

问题描述

虽然这个问题似乎已经被问过多次并且已经得到了很高的评价,但我想建议多个答案是相互矛盾的,我永远无法完全理解异步代码的内部结构。我完全理解这意味着继续顺序代码执行并稍后完成任务,我试图理解后面的部分。

答案 1 - 建议 UI/Main

首先,这个问题里面包含了以下测试,表明异步代码运行在 Main/Ui 线程上,并链接了一篇文章,解释了为什么没有其他线程用于异步代码。

问:“在我看来,因为我主要做 UI 开发,所以异步代码是不在 UI 线程上运行的代码,而是在其他线程上运行的代码。”

答:这种信念很普遍,但却是错误的。不要求异步代码在任何第二个线程上运行。

事实上,答案表明使用线程进行异步编程是“错误的”, “线程是工作人员。异步工作流程可以全部发生在一个线程上。异步工作流程的重点是避免雇用更多工作人员,如果可以避免的话。”

答案 2 - 建议 UI/Main

我读到的下一个问题也表明异步代码在 Main/UI 线程上运行,但是使用的比较是 JavaScript,我们都知道它是一种单线程语言。例如,假设我运行这段代码,

 function wait(ms) {
  var start = Date.now(),
      now = start;
  while (now - start < ms) {
    now = Date.now();
  }
}
setTimeout(() => {
    wait(5000); 
}, 3000)

setTimeout将被异步调用,并且在3000ms回调之后将被添加到Event Loop并最终运行。然而,方法执行将在 Main/UI 线程上完成,因此导致 UI 被冻结为5000ms.

答案 3 - 建议新线程

这个答案表明异步代码在他说的答案中运行到了一个新线程中。 “当你异步执行某件事时,你可以在它完成之前继续执行另一个任务。话虽如此,在计算机的上下文中,这转化为在另一个“线程”上执行一个进程或任务。

答案 4 - 建议新线程

最终答案表明异步代码是基于线程的,“在一般情况下,异步调用不一定会创建一个新线程。这是实现它的一种方式,预先存在的线程池或外部进程是其他方式. 它在很大程度上取决于语言、对象模型(如果有的话)和运行时环境。异步只是意味着调用线程不会坐下来等待响应,异步活动也不会发生在调用线程中。” ,重点是“异步活动也不会发生在调用线程中”

我不知道从哪里开始,根据我的理解,我认为异步代码必须在不同的线程中执行,才能永远不会阻塞 UI,我想唯一的例外是 JavaScript。但是,即使涉及在单线程语言上执行异步代码,我也会认为运行的任何函数回调都必须在运行另一个回调之前运行完成。

这里的第二个答案通过绘图表明,当异步代码在单个线程上运行时,它会停止并运行不同的异步回调,来回切换,就像线程在完成之前一样。这对我来说并不完全有意义,因为对于异步代码,它们通常包含一个回调以防止任何类型的竞争条件,假设 B 需要 A 首先运行完成,B 将是 A 内部的回调。回调 A和 B 不会“一起跑”并在彼此之间切换。

标签: multithreadingasynchronousthreadpoolsynchronous

解决方案


让我们从“低级”向上工作。

对于低级硬件,大多数现代设备(网络、磁盘)使用总线主控或 DMA 向/从计算机的主存储器进行传输,并在传输完成时生成一个 IRQ;并支持“驱动程序向设备发出命令,然后设备在命令完成时发出 IRQ”模型,用于不涉及 IO 的事情。举个例子;如果您想(例如)从单 CPU 计算机上的磁盘异步读取一些数据;CPU(设备驱动程序)可以告诉磁盘控制器硬件要做什么,然后 CPU 可以在磁盘控制器传输数据时继续做有用的工作,然后当 IRQ 到达时(表示传输完成)它可以触发某种“异步传输完成”动作。

如果设备不支持这一点(例如,必须使用某种“编程 IO”,其中 CPU 需要用于传输数据等),那么必须以某种方式模拟异步性 - 通过使用另一个 CPU,通过撒谎(同步执行并假装“异步完成”立即发生),或者使用单个 CPU 在不同工作之间快速切换(一个请求异步操作,另一个正在执行异步操作)来制造异步错觉。

下一层是操作系统的内核和设备驱动程序。对于几乎所有现代操作系统,所有需要足够时间才值得关心的事情都是(内部)完全异步的(即使底层硬件不支持它,也会模拟异步操作);主要是因为多处理和性能(例如,确保来自不同进程的单独请求能够让所有硬件设备在可能的情况下并行执行有用的工作,通常具有 IO 优先级以尝试确保更重要的工作比不重要的工作更快地发生,通常与后台发生的各种预取策略相结合,以增加在任何进程请求之前完成工作的机会)。然而; 提供给用户空间的 API 可能不会暴露内核' ■ 支持对用户空间进程的异步操作。例如,最初的 POSIX API 根本不支持异步操作(它在为时已晚后被追溯入侵,这就是为什么你甚至不能做一些基本的事情,比如异步打开文件)。

下一层是语言的运行时。这介于语言标准(可能会或可能不会迎合异步操作,可能会或可能不会有某种“用户空间线程/光纤/任何东西”)和内核的 API(可能会或可能不会提供一些/任何异步操作)。对于语言的运行时支持异步操作但底层内核 API 不支持的情况,必须以某种方式模拟它 - 例如使用(一个,更多?)“内核线程”(并且可能将其隐藏在任何类型的“用户-space threading/fibers/whatever”),或者通过撒谎(同步进行并假装“异步完成”立即发生)。

最后一层是第三方库/模块/包,用于在最初不支持它们的语言之上添加异步操作。这总是涉及某种仿真。

请注意,如果您查看整个层集,整个系统可能会在 2 个不同级别(在内核/设备驱动程序中,以及在语言运行时的用户空间中或在其之上)模拟异步操作。它); 因此可以同时使用 2 种不同的模拟异步性的方法(即使在这种情况下,也可能不会使用线程/任务来模拟/提供异步性)。

当然,如果有 100 种语言,每种语言平均有 4 个实现;那么将有 400 种排列,它们对于异步操作的实际实现方式可能都有不同的答案;因此,您可以期望(可能)针对 C#、Javascript 等给出不同的答案……尽管是错误的(对于该语言的其他实现或其他语言),这可能是正确的(对于该语言的实现)。


推荐阅读