跳转至

Async

AsyncVapor 3的重要特性,但这个特性刚开始容易令人困惑。

假设一个服务器只有一个线程处理客户请求,四个客户端请求顺序发起:

  • 第一个请求需要从服务器获取股票报价,但股票报价需要服务器从另外的服务器获取后才能返回给客户端。

  • 第二个请求获取CSS样式表,这个样式表可以从服务器自己的存储器中直接获取。

  • 第三个请求需要服务器从数据库中查询用户信息,返回给客户端

  • 第四个请求获取HTML内容,这些HTML内容也可以在服务器自己的存储器中直接获取。

如果用一个同步服务器处理这四个请求,因为是单线程,所有当第一个请求发起后,第二个请求需要在第一个请求处理完成后才能被处理到,所以四个请求是顺序执行,前一个处理完成才能处理后一个。

如果用一个异步服务器处理这四个请求,同样也是单线程,当第一个请求发起后,服务器初始化一个调用去处理请求,但是不能马上处理完成返回结果,此时就会把第一个请求先放在一边,马上去处理第二个请求。当被放在一边的请求处理完成后会恢复线程执行,把结果返回给客户端。

虽然服务器可以有多个线程处理请求,但是服务器同时开启的线程数量是有上限的,在多个线程间切换环境是很耗费资源的操作,并且数据在多线程间访问时处理不当很容易发生错误,保证线程安全的访问很耗时并且也很容易出错。多线程服务器使用线程池的方式,效率也不高,不是一个好的解决方案。

以异步的方式处理请求,把请求放在一边这种操作可以通过promise的方式把处理过程封装起来,一旦处理完成就返回给客户端。把一个不确定啥时候返回的结果封装成一个Future结构。

这块有点不太好理解,需要有点耐心。

假设有一个函数是这样的:

1
2
3
func getAllUsers() -> [User] {
    // do some database queries
}
在异步环境下,这个函数返回时,可能数据库的查询操作还没有完成,所以是不能正常工作的。这种情况下,只知道函数会返回一个数组[User],却不清楚返回的具体时刻,所以需要改造一下返回类型,用Future这个范型结构承诺在将来的某个时刻返回对应的类型的数据。

1
2
3
func getAllUsers() -> Future<[User]> {
    // do some database queries
}

使用Future可能一开始会有点困惑,因为这个概念还不是很熟悉,不过使用一段时间就会适应,毕竟在Vapor中会有大量场景使用它。

当我们从一个函数获得一个Future返回时,实际上是想在这个Future有实际结果时执行一些操作,但这个Future在被获取时还没有产生实际值,所以我们需要给Future提供它在产生实际值时需要进行的操作对应的回调函数,让Future自己在产生实际值时调用相应的处理回调。

// TODO: 这里需要一个更加清晰的解释Future机制

和Future搭配使用的操作有以下几种:

  • flatMap:CollectionType -> AnotherElemType
  • map: CollectionType -> CollectionType
  • transform: 与map类似,不处理具体元素,直接变换为指定值
  • flatten: 等所有Future都返回时执行
  • do/catch: 用来捕获错误,但不是恢复错误
  • catchMap/catchFlatMap: 捕获并修复错误
  • always: 不管结果如果总会执行
  • wait: 不能在主线程上使用
  • request.future(_:)可以创建在同一个请求线程上使用的Future

关于FlatMap和Map的理解

假如一个数组Array(0,1,2,3), 要把它的每个元素变成原来的2倍,可以使用

1
2
3
Array(0,1,2,3).map { elem in
    return elem * 2
}
这样得到的结果是:

1
Array(0,2,4,6)

但如果要把每个元素都映射成为自己和比自己大一的数组时就是:

1
2
3
Array(0,1,2,3).map { elem in
    return Array(elem, elem + 1);
}
这时的结果就是:

1
Array(Array(0,1), Array(2,3), Array(4,5), Array(6, 7))

可见这时就是一个二层嵌套数组。如果我们此时想得到的结果是: Array(0,1,2,3,4,5,6,7)

就可以使用flatMap这种操作:

1
2
3
Array(0,1,2,3,).flatMap { elem in 
    return Array(elem, elem + 1)
}
可以把flatMap看作是去掉一层嵌套的壳之后,再把元素组合在一起。

对于Future来说,它的位置就和Array的位置一样,我们只需要把上面例子中的Array换成Future就可以,道理类似。flatMap相当于在map操作的基础上剥去一层外壳,再把各个元素的值整合在一起。

在Vapor中一个Request就是一个Worker,相当于一个线程。

全局操作支持最多五个Future结果返回后执行。

对Future可以链式操作,用来避免过度嵌套。

SwiftNIO

是苹果的一个开源跨平台异步网络库,它用来管理连接和处理数据传输,管理着事件循环(EventLoop),每一个事件循环对应一个线程。

如果一个线程写入一个变量的同时有另外一个线程同时的对这个变量进行读或者写操作,那么就会产生竞争关系,有可能会使你的应用发生崩溃。传统的处理方式是给多个线程同时访问的变量各自加一把锁来使对变量的访问变的有序,从而消除竞争关系。线程在访问一个变量前先给这个变量上锁,表示此刻该变量正在使用,其它线程不能访问,等访问完成后对该变量解锁,以示其它线程可以继续对其进行访问。这是一种解决办法,但是存在缺点,就是使用起来很复杂,同时也会影响到程序的执行效率。

另一种思路是把对同一个变量的访问都放在同一个线程里来进行。但这就要求这个处理读写操作的线程要能够把读写的结果返回给发起读写请求的线程中去。其实每一个事件循环(EventLoop)都可以看作是一个线程。 如果变量的读写操作所在的线程把读写结果返回给没有发起读写请求的其它线程,那么SwiftNIO就会用崩溃来避免程序发生不确定问题。要把读写操作的结果返回给发起读写请求的相关线程中去的功能就需要用到FuturePromise这两个概念了。

Future是用来描述目前还不存在,但未来会存在的信息的一种数据结构。写异步代码时用Future来表示一个处理结果是成功还是失败,这两种结果确定会发生在未来,但是目前不知道会是哪一种。但是如果不知道未来结果到底是什么样子(可能成功,可能失败,也可能是其它的),那就需要创建Promise了。

PromiseFuture都必须在事件循环(EventLoop)中创建,Future会被返回给产生它的事件循环中,一次只能代表一个结果,要么成功,要么失败,都算是处理完成状态。然后Promise创建时一定是完成状态的。