热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Golang异步API全面解析与应用实例

本文深入解析了Golang中的异步API,并通过具体的应用实例展示了其强大功能。文章不仅探讨了Golang和Erlang在并行处理方面的核心理念,还详细介绍了如何利用通道(channel)和goroutine实现高效的并发编程。例如,通过`ch:=make(chanint)`创建通道,并使用`gofunc(chchanint)`启动goroutine来处理异步任务。此外,文章还提供了多个实际案例,帮助读者更好地理解和应用这些概念。

首先是剧透。这篇文章所讲的东西,其实就是golang和erlang里的并行精髓。文中的问题在golang里可以这样解决:

ch := make(chan int);

go fun(ch chan int) {
  DoSomething();
  ch <- result;
}(ch);

OtherWork();
MoreOtherWork();

result := <-ch;

Herb Sutter

当设计并发APIs的时候,要分离“要做啥”和“如何做”。

Herb Sutter写过不少畅销书,也是软件开发方面的顾问,同时还在Microsoft任软件架构师一职。可以通过www.gotw.ca联系到他。

介绍

让我们从一个已有的同步API函数说起:

例子1:原始的同步API,可能执行时间很长(需要计算,需要等待磁盘或者网络,等等)

RetType DoSomething(InParameters ins, OutParameters outs );

因为DoSomething可能会花很长时间执行(不管CPU是不是因此很忙),而且可能与调用者的其他工作互相独立,自然的,调用者就希望能异步执行DoSomething。比如说,像这样调用代码:

例子1,后续:需要调用的样例代码

void CallerMethod() {
  result = DoSomething( this, that, outTheOther );

  // 这些可能也很花时间
  OtherWork();
  MoreOtherWork();

  // 现在可以查看使用result和outOther(应该是outTheOther)
}

如果OtherWork和MoreOtherWork并不依赖返回值result或者其他DoSomething产生的副作用,并且他们并不与DoSomething使用相同的数据或资源,因此可以正确的并行运转,那么让DoSomething与OtherWork/MoreOtherWork并行执行会有很有用。

问题是,我们如何让他们并行化?其实有个简单正确的答案。但是因为很多接口已经选择了更复杂的答案,我们还是先来看看这些复杂的做法。

选择1:明确指定开始/结束(不好)

对API来说,一种选择是提供异步版本的函数。这种做法太流行了,已经形成了许多固定的模式。每个模式都需要将相关变量改造了多次。

一种在.NET里常用的模式(这种改造同样用在其他地方,比如Microsoft Office的C++代码)被称作框架异步模式(Frameworks Async Pattern)。这个模式的思想是,把原始方法分离成一组方法,一个用来开始(启动)工作,另一个用来结束(结合)工作。我们一会儿将看到这个模式的细节,包括引入中间结构和需要明确注册回调函数。先从简单的版本说起:

例子2:应用最基本的“异步模式” “开始”部分接受输入参数。

IAsyncResult BeginDoSomething(
  InParameters ins
);

// “结束”部分产生返回值和输出参数。注意:调用者需要显式调用EndDoSomething。
//
RetType EndDoSomething(
IAsyncResult asyncResult,
OutParameters outs
);

这里展示的是调用方的代码,使用这种BeginXxx/EndXxx(开始/结束)模式来异步调用DoSomething:

例子2,后续:调用示例代码:

void CallerMethod() {
  // 启动,传入输入参数
  IAsyncResult ar = BeginDoSomething( this, that );
  result = DoSomething( this, that, outTheOther );

  // 这些可能也很花时间
  // 但是会和DoSomething并行执行
  OtherWork();
  MoreOtherWork();

  // 结合,然后调用End得到返回值和输出参数
  ar.AsyncWaitHandle.WaitOne();
  result = EndDoSomething( ar, outTheOther );

  // 现在可以使用result和outTheOther
}

这种方法确实能工作,但是,有些明显的缺点。

第一,这种模式侵入了原有API,并增加了API数量。API接口数量为了实现异步,变成了原来的三倍,原来的一个函数变成了需要互相维持同步关系的三个函数。这里还有一致性的问题:由于每个API手册上的约束不同,API作者每次在应用这个模式时会有很大变化(应该是指函数接口上的变化)。而且,API设计者必须提前知道哪些方法需要支持异步调用。

我们可以提供不需要关联特定API的,通用形式的开始/结束模式,来消除上面提到的问题。比如,.NET框架提供泛型的BeginInvoke/EndInvoke来异步调用任意方法。[1]可惜,这个方法并不能解决下面的缺点。

第二,这种方法对调用者来说比最早的同步方案复杂很多,原来只用调一个函数,而现在必须调用三个:一个用来启动,一个用来等待,一个用来结束。这也增加了潜在失败的调用点,这些点上调用代码可能返回错误。比如说,由于你取消了工作或者只是试探性的调用或者其他什么原因,你并不关心结束后的结果那你还需要调用EndXxx方法么?通常来说,是的(对.NET来说,永远是的),因为BeginXxx调用将会分配资源来启动和跟踪工作,而EndXxx调用会释放这些资源。因此,.NET设计指导文档这样写道:“当操作已经结束后,必须保证总是调用EndMethodName方法。 这样可以接受到在异步操作中产生的异常,并释放异步操作相关的资源。”即便对有经验的程序员来说,这也是常见的会引起错误的地方。而有些大师写的书,甚至错误的认为EndXxx只是个可选的调用,或者只是忘了在所有的路径里调用EndXxx方法。

第三,沿着这个方向,这个方法会变得更加复杂和“华丽”。特别是,如果你看看当前的开始/结束模式的实现,你会发现已经变成了这样:

例子3:更完整的“异步模式”

//
// “开始”部分接受输入参数
// 并且可以接受一个回调函数在任务结束后被调用,
// 和一个标识这次调用的COOKIE(就是指这次调用需要用到的特殊变量)
//
IAsyncResult BeginDoSomething(
  InParameters ins,
  // + 调用者可以提供一个用于通知的回调函数
  AsnycCallback callback,
  // + 用来标识这次调用的额外状态
  Object COOKIE
  }
  // “结束”和例子2里一样
  RetType EndDoSomething(
  IAsyncResult asyncResult,
  OutParameters outs
);

需要我们这样做的原因是,调用者可以提供一种“最终,根据结果做X”的代理器,这样万一调用者不需要/不想等待异步调用完成的时候,调用者可以有方法返回。比如说:

例子3,后续:另一种不需要等待的调用代码

//
void CallerMethod() {
  //

  // 启动,传入输入参数
  // 但是不等待,只是在最后直接使用result。
  IAsyncResult ar = BeginDoSomething( this, that,
  (Object myExtraState) => {
    result = EndDoSomething( ar, outTheOther );
    // 需要根据result来处理的事情
    // (写磁盘,更新GUI文字框)
  },
    new MyExtraState( /**/ )
  );
  result = DoSomething( this, that, outTheOther);

  OtherWork();
  MoreOtherWork();

  // 不等待DoSomething的完成,直接返回
}

最后第四点,开始/结束方法用一堆变量来连接这种特定的异步调用方法。调用者可以选择让工作异步执行,但不能选择如何执行——是让工作跑在一个线程池的线程?在一个全新的线程?一个运行时自动负载均衡的任务?一个特定的处理器核心上?还是其他什么的。

我们确实想把“让这个工作异步执行”的想法(“如何执行”调用)和某个具体的API本身(调用“做什么”)解耦。我们通过将开始/结束模式泛型化,部分的实现了这个想法。但是我们想做的更好:我们想把调用代码变得更简单健壮,并让调用者可以灵活选择用哪种方式启动工作。

好消息是,我们确实可以做得更好。

选择2:将“做什么”和“怎么做”解耦

通过抽象来解决:

使用一个单独的更加通用的任务启动器,来启动一个工作。根据你的使用环境,你可能已经有很多可用的选择了,比如可以用pool.run( /任务/ )在Java或者.NET线程池里运行任务,或者在C++ 0x里调用async( /任务/ )。

使用future来管理异步结果。future是一个异步值——把它想成类似“在未来可赎回一个值的票据”。[2]这个抽象在Java中已经存在Future,在即将到来的C++ 0x里则叫做future,在下一个.NET发布版本中则叫做Task

举个例子,这里有个简单的同步调用CallSomeFunc:

// 同步调用(会阻塞直到执行完成)
int result = CallSomeFunc(x, y, z);

// 直到上面的调用完成,result已经返回,才能执行到这段代码

// 使用已经返回好的result
DoSomethingWith( result );

这是对应的异步调用(混合了C++ 0xC#的语法):

// 异步调用
future result =
async( ()=<{ return CallSomeFunc(x, y, z); } );

// 这里的代码与CallSomeFunc并发执行

// 当result准备好后使用它(这里可能会阻塞)
DoSomethingWith( result.value() );

future允许我们把调用(启动)和接收结果(结合)解耦。这让我们可以灵活指定如何启动工作,即不用入侵修改任何已存在的同步API,也不需要入侵表述起来很简单健壮的future任务句柄。同时,我们也不用必须记住要显式调用EndXxx方法来做收尾工作,这些收尾工作已经封装到future抽象里了(一般来说,是future对象的析构函数dtor或者清扫方法disposer method)。

下面是对文章最早那个例子的改进。注意这里并没有修改原始的API:

例子4:依旧是原始的同步API

RetType DoSomething(
  InParameters ins,
  OutParameters outs
);
// 异步调用代码的例子
//
void CallerMethod() {
  // 异步启动工作(随便什么流行的启动方式;比如用线程池)
  // 注意result和outTheOther现在是future类型
  result = pool.run( ()=>{
  DoSomething( this, that, outTheOther ) } );

  // 这些可能也会花很长时间执行
  // 但是现在和DoSomething是并发执行的
  OtherWork();
  MoreOtherWork();

  // 现在可以使用result.wait()(可能会阻塞)和outTheOther
}

注意:我用C#的lambda语法来展示代码只是为了写起来方便。如果你的环境无法提供C++ 0x或者C#的lambda语法,你依旧可以这样做:将“()=>”改成不同的Runnable对象(Java),delegate(C#)或者仿函数(C++)。lambda只是一个语法糖,可以更方便的写runnable或者仿函数。

如果我们不想等待result但当工作结束还需要做些收尾工作,就像例子3那样,应该怎么做?简单:只要完成异步调用工作那块就行,不需要回调函数:

例子5(与例子3对比):另一种不需等待的调用代码

void CallerMethod() {
  //

  // 启动,传入输入参数。
  // 但是不等待,只是最终使用result
  async( ()=>{
  DoSomething( this, that, outTheOther );
  // 处理那些需要result的工作
  // (写磁盘,更新GUI文字框)
  } );

  OtherWork();
  MoreOtherWork();

  // 现在直接返回,不需要等待DoSomething
}

总结

我们如何提供异步版本的API?通常最好的答案是什么也不要做。因为调用者可以使用future特性结合“async”或者任务启动特性,从API外面将一个调用变成异步执行调用。

如果你需要提供框架或者程序库或者其他什么API接口,那么你会更喜欢将异步启动(“如何做”)与完成任务(“做什么”)分离。这条路会得到更简单的API,更简单且更健壮的调用代码,和更灵活的指定在何时何地执行工作的能力。

注释

  • 异步调用同步方法
  • 在过去,人们有时用“future”这个词同时指代异步工作和工作的结果,但这其实将两个不同的事情混为一谈。总是将future堪称异步的值或者对象。

推荐阅读
  • Flutter 核心技术与混合开发模式深入解析
    本文深入探讨了 Flutter 的核心技术,特别是其混合开发模式,包括统一管理模式和三端分离模式,以及混合栈原理。通过对比不同模式的优缺点,帮助开发者选择最适合项目的混合开发策略。 ... [详细]
  • JUC并发编程——线程的基本方法使用
    目录一、线程名称设置和获取二、线程的sleep()三、线程的interrupt四、join()五、yield()六、wait(),notify(),notifyAll( ... [详细]
  • pypy 真的能让 Python 比 C 还快么?
    作者:肖恩顿来源:游戏不存在最近“pypy为什么能让python比c还快”刷屏了,原文讲的内容偏理论,干货比较少。我们可以再深入一点点,了解pypy的真相。正式开始之前,多唠叨两句 ... [详细]
  • 关于进程的复习:#管道#数据的共享Managerdictlist#进程池#cpu个数1#retmap(func,iterable)#异步自带close和join#所有 ... [详细]
  • RocketMQ在秒杀时的应用
    目录一、RocketMQ是什么二、broker和nameserver2.1Broker2.2NameServer三、MQ在秒杀场景下的应用3.1利用MQ进行异步操作3. ... [详细]
  • 作为140字符的开创者,Twitter看似简单却异常复杂。其简洁之处在于仅用140个字符就能实现信息的高效传播,甚至在多次全球性事件中超越传统媒体的速度。然而,为了支持2亿用户的高效使用,其背后的技术架构和系统设计则极为复杂,涉及高并发处理、数据存储和实时传输等多个技术挑战。 ... [详细]
  • Java 中的十进制样式 getZeroDigit()方法,示例 ... [详细]
  • 深入理解Java SE 8新特性:Lambda表达式与函数式编程
    本文作为‘Java SE 8新特性概览’系列的一部分,将详细探讨Lambda表达式。通过多种示例,我们将展示Lambda表达式的不同应用场景,并解释编译器如何处理这些表达式。 ... [详细]
  • 流处理中的计数挑战与解决方案
    本文探讨了在流处理中进行计数的各种技术和挑战,并基于作者在2016年圣何塞举行的Hadoop World大会上的演讲进行了深入分析。文章不仅介绍了传统批处理和Lambda架构的局限性,还详细探讨了流处理架构的优势及其在现代大数据应用中的重要作用。 ... [详细]
  • 汇编语言:编程世界的始祖,连C语言都敬畏三分!
    当C语言还在萌芽阶段时,它首次接触到了汇编语言,并对其简洁性感到震惊。尽管汇编语言的指令极其简单,但它却是所有现代编程语言的基础,其重要性不言而喻。 ... [详细]
  • 在将 Android Studio 从 3.0 升级到 3.1 版本后,遇到项目无法正常编译的问题,具体错误信息为:org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':app:processDemoProductDebugResources'。 ... [详细]
  • 本文详细介绍了Elasticsearch中的分页查询机制,包括基本的分页查询流程、'from-size'浅分页与'scroll'深分页的区别及应用场景,以及两者在性能上的对比。 ... [详细]
  • 本文探讨了Java中线程的多种终止方式及其状态转换,提供了关于如何安全有效地终止线程的指导。 ... [详细]
  • 小米路由器AX6000与小米11同步推出,不仅在硬件配置上达到了旗舰级水准,其独特的4K QAM技术更是引领了行业新标准。本文将深入探讨这款路由器的性能表现及其实际应用。 ... [详细]
  • 深入探讨:Actor模型如何解决并发与分布式计算难题
    在现代软件开发中,高并发和分布式系统的设计面临着诸多挑战。本文基于Akka最新文档,详细探讨了Actor模型如何有效地解决这些挑战,并提供了对并发和分布式计算的新视角。 ... [详细]
author-avatar
他像强盗霸占了d我的心
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有