1. 简单百科
  2. 异步调用

异步调用

异步调用(asynchronous call),是指一种不需要等待被调用函数返回值就能够继续执行的方法。

举例

异步调用的例子可以理解为你喊你的朋友一起吃饭,即使你的朋友表示稍后会找你,你也可以先去做其他事情。相比之下,同步调用则意味着你需要一直等待直到你的朋友完成工作并与你一同前往。

实战用法

随着操作系统的不断发展,线程已成为其重要组成部分。操作系统能够将CPU处理时间划分为多个短暂的时间片,在T1时间执行一个线程的指令,随后在T2时间执行下一个线程的指令,使得各个线程似乎是在并行地执行。在这种情况下,程序员可以创建多个线程并在同一时间段内执行,每个线程都可以“并行”地完成不同的任务。

在单线程模式下,计算机遵循严格的约翰·冯·诺依曼架构,当一段代码调用另一段代码时,必须采用同步调用,即必须等待这段代码执行完毕并返回结果后,调用方才能够继续执行后续代码。然而,得益于多线程支持,我们可以采用异步调用,调用方和被调用方可以属于不同的线程。调用方启动被调用方线程后,不必等待对方返回结果即可继续执行后续代码。被调用方执行完毕后,可以通过某种方式通知调用方:结果已经得出,请适当处理。

在计算机中,某些处理可能较为耗时。当调用此类处理代码时,如果调用方一直处于等待状态,将会显著影响程序性能。例如,某个程序启动后需要打开文件读取其中的数据,然后再根据这些数据进行一系列初始化处理,那么程序的主窗口可能会延迟显示,导致用户体验不佳。通过异步调用,我们能够将整个初始化处理放在一个单独的线程中,主线程启动此线程后继续向下执行,从而使主窗口快速显示。当用户专注于窗口时,初始化处理已经在后台悄然完成。程序稳定运行后,我们还可以继续使用异步调用来提高人机交互的即时响应性。当用户点击鼠标触发操作时,如果该操作相对耗时,再次点击鼠标时可能不会立即响应,这会使整个程序显得缓慢。通过异步调用来处理耗时的操作,可以让主线程随时准备处理下一条消息,从而实现快速响应用户的点击,提升用户对软件的好感度。

异步调用外部数据处理

异步调用对于处理来自外部的输入数据非常有效。假设计算机需要从一台低速设备获取数据,然后进行一段较长的数据处理过程,采用同步调用显然是不合理的:计算机首先向外部设备发出请求,然后等待数据输入;而外部设备向计算机发送数据后,也需要等待计算机完成数据处理后再发出下一条数据请求。在这段时间里,双方都有等待期,延长了整个处理过程。实际上,计算机可以在处理数据之前先发出下一条数据请求,然后立即处理接收到的数据。如果数据处理的速度较快,那么只需要等待的是计算机,外部设备可以连续不断地采集数据。如果计算机同时连接有多台输入设备,它可以依次向各台设备发出数据请求,并随时处理每台设备发送的数据,整个系统可以保持连续高效运转。关键在于将数据采集工具代码和数据处理代码分别分配给两个不同的线程。数据处理代码调用一个数据请求异步函数,然后直接处理当前的数据。当下一组数据到达后,数据处理线程将收到通知,结束等待状态,发出下一条数据请求,然后继续处理数据。

在异步调用过程中,调用方不必等待被调用方返回结果,因此必须有一种机制能够让被调用方在得到结果后通知调用方。在同一进程中,有许多可用的机制,包括回调、互斥对象和消息。

回调是一种简单的机制:在调用异步函数时,在参数中提供一个函数地址,异步函数将其保存下来,当有结果时回调此函数,以便向调用方发出通知。如果将异步函数封装在一个对象中,可以使用事件代替回调函数地址,通过事件处理例程向调用方发送通知。

Mutex是Windows系统提供的常用同步对象,可用于在异步处理中协调不同线程之间的步骤。如果调用方暂时没有其他任务,可以调用wait函数在此处等待,此时Mutex处于非信号状态。当被调用方获得结果后,将Mutex对象设置为信号状态,wait函数就会自动结束等待,使调用方恢复活动,从被调用方获取处理结果。这种方法相对于回调来说更为复杂,速度也可能较慢,但它具有更大的灵活性,可以应对更加复杂的处理系统。

借助Windows消息传递通知是一种不错的选择,因为它既简单又安全。程序中定义了一个用户消息,并且调用方已经准备好了消息处理例程。被调用方获得结果后立即向调用方发送此消息,并通过WParam和LParam这两个参数传输结果。消息始终与窗口handle相关联,因此调用方必须依赖一个窗口才能接收消息,这也是它的不便之处。此外,通过消息联系会影响速度,需要高速处理时回调方式更具优势。

如果调用方和被调用方属于不同的进程,由于内存空间的隔离,通常采用Windows消息传递通知更为简单可靠。被调用方可以借助消息本身向调用方传输数据。Event对象也可以通过名称在不同进程中共享,但仅限于发送通知,本身无法传输数据,需要借助Windows消息和FileMapping等内存共享手段,或者MailSlot和Pipe等通信手段。

异步调用原理

异步调用的原理并不复杂,但是在实际使用中容易出现意想不到的问题,尤其是当不同线程共享代码或共享数据时更容易出现问题,编程时需要注意是否存在这样的共享,并通过各种状态标志避免冲突。Windows系统提供的Mutex对象在这方面特别有用。Mutex在同一时间内只有一个管理者。一个线程放弃管理权限后,另一个线程才能接管。当某一线程执行到敏感区域之前先接管Mutex,使其他线程被wait函数阻挡在其后面;离开敏感区域后立即释放管理权限,使wait函数结束等待,另一个线程就有机会访问此敏感区域。这样就可以有效地防止多个线程同时进入同一敏感区域。

由于异步调用容易出现问题,要设计一个安全高效的编程方案需要较多的设计经验,因此最好不过多地使用异步调用。同步调用虽然会让编写代码的人感觉更舒适,因为无论程序在哪里,只要关注移动点就能了解情况,而不像异步调用那样,总有一种四面楚歌、心神不定的感觉。必要时甚至可以将异步函数转换为同步函数。方法很简单:调用异步函数后立即调用wait函数等待,直到异步函数返回结果后再继续执行。

异步调用使用方法

测试方法和异步委托

所有的示例均使用相同的长时间运行测试方法TestMethod。该方法会在控制台上显示一条表示已经开始处理的信息,然后睡眠几秒,最终结束。TestMethod有一个out参数(在Visual Basic中为ByRef),用于演示如何将这些参数添加到BeginInvoke和EndInvoke的签名中。你可以以同样的方式处理ref参数(在Visual Basic中为ByRef)。下面的代码示例展示了TestMethod及其对应的委托;如果你想使用任何一个示例,请将示例代码附加到这段代码中。请注意,为了简化这些示例,TestMethod在独立于Main()的类中声明。或者,TestMethod可以是包含Main()的同一类中的静态方法(在Visual Basic中为Shared)。

使用EndInvoke等待异步调用

异步执行方法的最简单方式是使用BeginInvoke开始,对主线程执行一些操作,然后调用EndInvoke。EndInvoke直到异步调用完成后才会返回。这种技术适用于文件或网络操作,但由于它阻塞了EndInvoke,因此请勿从用户界面的服务线程中使用它。

使用WaitHandle等待异步调用

等待WaitHandle是一项常见的线程同步技术。你可以使用由BeginInvoke返回的IAsyncResult的AsyncWaitHandle属性来获取WaitHandle。异步调用完成后,WaitHandle会被发出信号,而你可以通过调用它的WaitOne来等待它。如果你使用WaitHandle,则在异步调用完成后,但在通过调用EndInvoke检索结果之前,可以执行其他处理。

轮询异步调用完成

你可以使用由BeginInvoke返回的IAsyncResult的IsCompleted属性来确定异步调用何时完成。可以从用户界面的服务线程中进行异步调用时可以执行此操作。轮询完成允许用户界面线程继续处理用户输入。

参考资料

异步调用.博客.2024-10-27

JavaScript 异步编程.菜鸟教程.2024-10-27

异步调用.知乎.2024-10-27