轻松无痛实现异步操作串行

提起异步操作的序列执行,指的是有一系列的异步操作(比如网络请求)的执行有前后的依赖关系,前一个请求执行完毕后,才能执行下一个请求。

异步操作的定义

我们定义一般异步操作都是如下形式:

1

2

3

4

5

func asyncOperation(complete : ()-> Void){

//..do something

complete()

}

常规的异步操作都会接受一个闭包作为参数,用于操作执行完毕后的回调。

那异步操作的序列化会有什么问题呢? 看如下的伪代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

func asyncOperation(complete : ()-> Void){

//..do something

print("fst executed")

complete()

}

func asyncOperation1(complete : ()-> Void){

//..do something

print("snd executed")

complete()

}

func asyncOperation2(complete : ()-> Void){

//..do something

print("third executed")

complete()

}

我们定义了三个操作asyncOperation,asyncOperation1 和 asyncOperation2,现在我们想序列执行三个操作,然后在执行完后输出 all executed。 按照常规,我们就写下了如下的代码:

1

2

3

4

5

6

7

asyncOperation {

asyncOperation1({

asyncOperation2({

print("all executed")

})

})

}

可以看到,明明才三层,代码似乎就有点复杂了,而我们真正关心的代码却只有 print("all executed") 这一行。但为了遵从前后依赖的时许关系,我们不得不小心的处理回调,以防搞错层级。如果层级多了就有可能像这样:

1

2

3

4

5

6

7

8

9

10

11

12

13

asyncOperation {

asyncOperation1({

asyncOperation2({

asyncOperation3{

asyncOperation4{

asyncOperation5{

print("all executed")

}

}

}

})

})

}

这就是传说中的callback hell, 而且这还只是最clean的情况,实际情况中还会耦合很多的逻辑代码,更加无法维护。

用reduce来实现异步操作的串行

那是否有解决办法呢? 答案是有的。很多FRP的框架都提供了类似的实现,有兴趣的读者可以自行查看Promise、 ReactiveCocoa 和 RxSwift中的实现。

然后正如本节的标题所说,Swift提供了两个函数式的特性:

  • 函数是一等公民(可以像变量一样传来传去,可以做函数参数、返回值
  • 高阶函数,比如 map 和 reduce

接下来我们就用这两个特性,实现一个更加优雅的方式来做异步操作串行。

1. 定义类型

为了方便书写,我们先定义一下异步操作的类型:

1

typealias AsyncFunc = (()->Void) -> Void

AsyncFunc 代表了一个函数类型,这样的函数有一个闭包参数(其实就是上面 asyncOperation 的类型)

2. 从串行两个操作开始

我们先化简问题,假设我们只需要串行两个异步操作呢? 有没有办法把两个异步操作串行成一个异步操作呢? 想到这里,我们可以YY出这样一个函数:

1

2

3

func concat(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{

}

concat函数,顾名思义,是连接的意思。指的是将两个异步操作:leftright串行起来,并返回一个新的异步操作。

那现在,我们来思考如何实现concat函数,既然返回的是AsyncFunc 也就是一个函数,那我们可以先YY出这样的结构:

1

2

3

4

5

func concat(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{

return { complete in

}

}

仔细回忆 AsyncFunc 的类型: (()->Void) -> Void,所以闭包参数complete就对应前面的参数。

架子已经写好了,我们来思考要实现如何实现最终返回这个函数。根据concat的定义我们可以知道,我们最终返回的是一个 接受一个闭包作为参数, 先执行left,成功后执行right,成功后再执行传入的闭包

你看,这样一分析,逻辑就非常清晰了,闭包参数就是complete. 我们抽丝剥茧,找到了问题的本质,于是很容易可以写出:

1

2

3

4

5

6

7

8

9

func concat(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{

return { complete in

left{

right{

complete()

}

}

}

}

核心逻辑和我们最原始的版本其实并没有区别,区别就是不论再多个串行,我们都不需要写更多的嵌套了。

基于最开始的例子,我们测试一下:

1

2

3

4

5

let concatedFunction = concat(asyncOperation,

right: asyncOperation1)

concatedFunction {

print("all executed")

}

至此,我们以及成功的实现了把两个异步操作合并成一个串行的异步操作。

3. 定义一个运算符

让我们回过头去,再审视一个我们concact的签名:

1

func concat(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{

我们忘记什么函数,什么闭包,什么异步。就来看签名:他接收两个相同类型的参数,最后返回一个结果,结果的类型和参数一致。

像什么?像雾像雨又像风? 还是像加法像减法又像乘法?总之我们可以把他看做是某种运算,具备如下性质:

1

a -> b -> c =  concact(a,b) -> c = concat(  concat(a,b)  , c)       (-> 代表异步地串行执行)

既然是运算,我们干脆给他定义个运算符,修改我们的concat函数如下所示, + 代表这是一种用来表示结合的运算,>代表他有前后的依赖关系,不满足交换律。+> 就是我们自己定义的异步串行运算符。

1

2

3

4

5

6

7

8

9

10

infix operator +> {associativity left precedence 150}

func +> (left:AsyncFunc,right:AsyncFunc) -> AsyncFunc{

return { complete in

left{

right{

complete()

}

}

}

}

这样,我们最开始的,五个异步操作串行执行的代码就可以改为这样:

1

2

3

4

5

6

7

8

9

let concatedFunction = asyncOperation +>

asyncOperation1 +>

asyncOperation2 +>

asyncOperation3 +>

asyncOperation4 +>

asyncOperation5

concatedFunction {

print("all executed")

}

我们先把五个操作串行成一个,然后执行它。

4. 串行任意多个异步操作

那你会说,如果我们有更多的异步操作呢?比如我们有一组异步操作:[AsyncFunc], 难道只能展开来一个个用 +> 来合并吗?

其实,现在我们有了串行运算符,那就很容易想到我们可以拿我们刚才实现的+>运算符来reduce一组异步操作。继续用刚才的例子,我们先写下如下代码:

1

2

let reducedFunction = [asyncOperation,asyncOperation1,asyncOperation2].

reduce(【初始值】, combine: +>)

我们把刚才定义的三个异步函数扔到列表里,然后用我们的串行运算符+>来reduce他,combine 其实就是+>,但此时似乎又面临另外一个问题,【初始值】填什么?

每次思考reduce的初始值都是一个哲学问题,大多数情况下我们不希望他参与运算,但又不得不让他参与运算(因为combine是个二元函数),所以我们希望reduce的初始值(记为initial)具备如下性质:

  • combine(initial,x) = x

这种性质,大家应该能联想到一个类似的东西叫 CGAffineTransformIdentity,往深了讲,这其实是一个代数问题,不过这里暂时不讨论。

在本例,我们的initial可以定义为:

1

let identityFunc:AsyncFunc = {f in f()}

它是这样的一个函数,接受闭包作为参数,然后什么都不做,马上调用闭包。这里大家简单感受一下。_(:зゝ∠)

于是,我们完整的reduce版本可以定义为:

1

2

3

4

5

6

7

8

let identityFunc:AsyncFunc = {f in f()}

let reducedFunction = [asyncOperation,asyncOperation1,asyncOperation2,asyncOperation3,asyncOperation4,asyncOperation5].

reduce(identityFunc, combine: +>)

reducedFunction {

print("all executed")

}

首先定义了identityFunc作为初始值,然后把我们开头定义的几个异步操作reduce成一个:reducedFunction,然后调用了它,可以观察输出结果,和我们最开始写的嵌套版本是一样的。

引申的话题

带参数的串行

真实世界里,当我们需要串行异步操作的时候,一般后一个操作都需要前一个操作的执行结果。比如我们可能需要先请求新闻的列表,拿到新闻的id之后,再请求新闻的一些具体的信息,前后操作有数据上的依赖关系。(当然一般不这么搞,这里只是举个例子)

抽象的来看,我们要处理一组串行的操作,为了方便处理,我们希望函数的签名是一样的,偷懒的做法可以这样:

1

typealias AsyncFunc = (info : AnyObject,complete:(AnyObject)->Void) -> Void

定义闭包的类型为AnyObject->Void ,同时异步函数也接受一个AnyObject的参数,这样在各个异步函数中通过把参数cast成字典,提取信息,操作完毕后把结果的值传到回调的闭包中。具体实现见一下节

如果嫌AnyObject太丑的话也可以针对串行操作的场景设计一个protocol,然后用protocol作为参数的类型来传递信息。

错误处理

我们最终将一组异步操作,reduce成了一个异步操作,那如果中间某个操作出错了,我们该怎么知道呢? 其中一种实现,可以是:

1

typealias AsyncFunc = (info : AnyObject,complete:(AnyObject?,NSError?)->Void) -> Void

对比之前带参数的例子,唯一的区别就是在闭包的参数里加了一个NSError?,以及把AnyObject改成了optional,因为这里的AnyObject代表的是结果,如果失败了,结果自然就是nil.

于是,我们的核心,串行运算符可以变成这样:

1

2

3

4

5

6

7

8

9

10

11

12

13

func +>(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{

return { info , complete in

left(info: info){ result,error in

guard error == nil else{

complete(nil,error)

return

}

right(info: info){result,error in

complete(result,error)

}

}

}

}

逻辑也是很直接的,我们首先尝试执行left,在left的回调中查看error是否是nil,如果不是,说明有错误,则立刻执行complete,并且带上这个error。否则再执行right,并将right的结果调用complete。然后在用+>连接了一组异步操作的时候,一旦有错,这个逻辑就可以让错误一步步传播到最顶层,避免执行了冗余的代码。

一个稍微异步一点的例子

随便建一个single view application,在viewcontroller.swift的顶部(swift 要求 operator 定义在 file scope,所以不能写在类里),添加:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

typealias AsyncFunc = (info : AnyObject,complete:(AnyObject?,NSError?)->Void) -> Void

infix operator +> {}

func +> (left:AsyncFunc,right:AsyncFunc) -> AsyncFunc{

return { info , complete in

left(info: info){ result,error in

guard error == nil else{

complete(nil,error)

return

}

right(info: info){result,error in

complete(result,error)

}

}

}

}

然后修改viewDidLoad为如下代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

override func viewDidLoad() {

super.viewDidLoad()

// Do any additional setup after loading the view, typically from a nib.

let identity:AsyncFunc = {info,complete in complete(nil,nil)}

func dispatchSecond(afterSecond : Int, block:dispatch_block_t){

let time = dispatch_time(DISPATCH_TIME_NOW, Int64(afterSecond) * Int64(NSEC_PER_SEC))

dispatch_after(time, dispatch_get_main_queue(), block)

}

let async1: AsyncFunc = { info, complete in

dispatchSecond(2, block: {

print("oh, im first one")

complete(nil, nil)

})

}

let async2: AsyncFunc = { info, complete in

dispatchSecond(2, block: {

print("oh, im second one")

complete(nil, nil)

})

}

let async3: AsyncFunc = { info, complete in

dispatchSecond(2, block: {

print("shit, im third one")

complete(nil, nil)

})

}

let async4: AsyncFunc = { info, complete in

dispatchSecond(2, block: {

print("fuck, im fourth one")

complete(nil, nil)

})

}

let asyncDaddy = [async1,async2,async3,async4].reduce(identity, combine: +>)

asyncDaddy(info: 0) { (o, e) in

print("okay, im deadly a last one")

}

}

运行程序后,会每两秒有一个输出。:)

本文旨在抛砖引玉,其实swift的functional特性已经非常丰富,稍微探索一下是可以做出很多fancy的应用出来的。

在函数式编程的世界里,我们定义的 identity加上+> 就是一种monoid,常见的monoid还有:

加法: identity 就是 0 , +> 就对应 +
乘法:identity 就是 1 , +> 就对应 *

一点有趣的思考: 刚才我们已经解释了,我们的+> 运算符是不支持交换律的,因为是串行。那它是否支持结合律呢? 比如: (a +> b) +> c 是否等于 a +> (b +> c)

时间: 2024-12-28 01:27:31

轻松无痛实现异步操作串行的相关文章

[转载]轻松玩转LCD12864-基于AVR单片机的LCD12864串行显示

原文链接: http://bbs.elecfans.com/forum.php?mod=viewthread&tid=282698&extra=&highlight=12864&page=1 参考帖子:http://home.eeworld.com.cn/my/space-uid-159112-blogid-40752.html http://v.youku.com/v_show/id_XNDYwOTM2Njc2.html LCD12864是一种常用的图形液晶显示模块,顾名

IOS多线程知识总结/队列概念/GCD/串行/并行/同步/异步

进程:正在进行中的程序被称为进程,负责程序运行的内存分配;每一个进程都有自己独立的虚拟内存空间: 线程:线程是进程中一个独立的执行路径(控制单元);一个进程中至少包含一条线程,即主线程. 队列:dispatch_queue_t,一种先进先出的数据结构,线程的创建和回收不需要程序员操作,由队列负责. 串行队列:队列中的任务只会顺序执行(类似跑步) dispatch_queue_t q = dispatch_queue_create(“....”, dispatch_queue_serial); 并

IOS多线程知识总结/队列概念/GCD/主队列/并行队列/全局队列/主队列/串行队列/同步任务/异步任务区别(附代码)

进程:正在进行中的程序被称为进程,负责程序运行的内存分配;每一个进程都有自己独立的虚拟内存空间 线程:线程是进程中一个独立的执行路径(控制单元);一个进程中至少包含一条线程,即主线程 队列 dispatch_queue_t,队列名称在调试时辅助,无论什么队列和任务,线程的创建和回收不需要程序员操作,有队列负责. 串行队列:队列中的任务只会顺序执行(类似跑步) dispatch_queue_t q = dispatch_queue_create(“....”, DISPATCH_QUEUE_SER

如何直接串行电缆以及空调制解调器串行电缆之间的区别?

串行电缆可分为直链和交.一般为直线延伸PC装备,将2.3.5连接的2.3.5,因为PC普遍的男性.但设备大多女,因此,他们只是普通的,它也可以被用于扩展连接:交叉的将军PC与PC接,将2对3.3对2.5对5,一般两头都是母头! 计算机出现之前,为连接串口设备,EIA 制定了RS232 标准. PC 机出现后,已有的串口设备成为PC机外设.自然採用RS232 标准.眼下PC 机的串行通信接口採用EIA-RS-232C 标准.C 代表1969年最新一次的改动.EIA-RS-232C标准对电器特性.逻

Silverlight并行下载与串行下载

思路清晰后仅仅只需百来行代码便可轻松编写出一套完整的资源动态下载组件- SerialDownloader和ParallelDownloader,它们共用一个完成资源表,且串行下载集成了优先机制(DownloadPriority),并行下载也根据需要封装了并行队列模式(QueueParallelDownloader): DownloadBase /// <summary> /// 下载器基类 /// </summary> public class DownloadBase { pro

并行,并发,串行,同步,异步,阻塞,非阻塞,同步阻塞,同步非阻塞,异步阻塞,异步非阻塞

并行和并发 并发和并行从宏观上来讲都是同时处理多路请求的概念.但并发和并行又有区别,并行是指两个或者多个事件(多核线程)在同一时刻发生:而并发是指两个或多个事件(进程或者程序)在同一时间间隔内发生.计算机在宏观上并发,微观上并行. 在操作系统中,并发是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行. ①程序与计算不再一一对应,一个程序副本可以有多个计算 ②并发程序之间有相互制约关系,直接制约体现为一个程序需

串行队列、并行队列、同步、异步

进程:正在进行中的程序被称为进程,负责程序运行的内存分配;每一个进程都有自己独立的虚拟内存空间 线程:线程是进程中一个独立的执行路径(控制单元);一个进程中至少包含一条线程,即主线程 队列 dispatch_queue_t,队列名称在调试时辅助,无论什么队列和任务,线程的创建和回收不需要程序员操作,有队列负责. 串行队列:队列中的任务只会顺序执行(类似跑步) dispatch_queue_t q = dispatch_queue_create(“....”, dispatch_queue_ser

串行程序并行化

考虑这样一个问题:统计某个工程的代码行数.首先想到的思路便是,递归文件树,每层递归里,循环遍历父文件夹下的所有子文件,如果子文件是文件夹,那么再对这个文件夹进行递归调用.于是问题很轻松的解决了.这个方案可以优化吗? 了 再回想这个问题,可以发现,循环里的递归调用其实相互之间是独立的,互不干扰,各自统计自己路径下的代码文件的行数.于是,发现了这个方案的可优化点--利用线程池进行并行处理.于是一个串行的求解方案被改进成了并行方案. 不能光说不练,写了一个Demo,对串行方案和并行方案进行了量化对比.

控制异步回调利器 - async 串行series,并行parallel,智能控制auto简介

async 作为大名鼎鼎的异步控制流程包,在npmjs.org 排名稳居前五,目前已经逐渐形成node.js下控制异步流程的一个规范.async成为异步编码流程控制的老大哥绝非偶然,它不仅使用方便,文档完善,把你杂乱无章的代码结构化,生辰嵌套的回掉清晰化. async 提供的api包括三个部分: (1)流程控制 常见的几种流程控制. (2)集合处理 异步操作处理集合中的数据. (3)工具类 . github 开源地址: https://github.com/caolan/async 安装方法: