基于协程的编程方式在移动端研发的思考及最佳实践

1. 超级App的性能和代码痛点

 

在 iOS 开发中线程使用特别方便,但是多线程使用不当引发的崩溃问题很多。

 

  • 多线程访问引发野指针问题
  • 多线程访问引发容器类崩溃问题
  • 多线程访问引发过渡释放问题

以手机淘宝为例,整个生命周期大量使用线程,多线程使用不当引发的崩溃问题占比达到了60%以上。

为了解决多线程崩溃问题或者为了让代码可读性更强可能会严重牺牲应用性能。

  • iOS 系统 API 设计很不友好,绝大部分 IO、跨进程调用等耗时接口提供的都是同步方法,主线程调用会产生严重性能问题。
  • 为了解决多线程崩溃加的锁、信号量等,由于设计不合理,很容易引发卡顿甚至死锁
  • iOS 系统 API 缺乏统一的异步编程模型,Delegate、Callback、同步等杂揉在一起,要写出高性能代码需要付出极大的努力。

手机淘宝卡顿问题分布:

系统 API、IO 等接口在异步编程上支持并不友好,极易产生性能问题。

 

2. iOS异步编程现状

 

 

基于 Block 回调的异步编程方式是目前 iOS 开发使用最广泛的异步编程方式,下面是使用 block 回调的异步编程的一个例子:

基于Block回调的异步编程方式有以下缺点:

 

  • 容易进入”嵌套地狱”
  • 错误处理复杂和冗长
  • 容易忘记调用completion handler
  • 条件执行变得很困难
  • 从互相独立的调用中组合返回结果变得极其困难
  • 在错误的线程中继续执行(如子线程操作UI)

3. 其他语言的异步编程方式

 

 

kotlin中通过协程实现的异步编程方式,代码简洁,逻辑清晰。

 

node.js中通过协程async/await方式实现的可重试异步网络请求。

协程不仅打开了异步编程的大门,还提供了大量其他的可能性。

4. 协程是什么?

 

协程具有以下特征:

协程的概念在60年代就已经提出,目前在服务端中应用比较广泛,在高并发场景下使用极其合适,可以极大降低单机的线程数,提升单机的连接和处理能力,但是在iOS移动开发中并没有框架支持。

从其他语言的发展来看,基于协程的全新的异步编程方式,是我们解决现有异步编程问题的有效的方式,但是无奈苹果对于 Objective-C 的支持基本已经停滞,也不指望苹果能够让 Objective-C 的开发者用上协程,基于我们团队长期对系统底层库和汇编的研究,我们通过汇编和C语言实现了支持 Objective-C 和 Swift 协程的完美解决方案coobjc。

5. iOS协程开发框架——coobjc

coobjc 是由手机淘宝架构团队推出的能在 iOS 上使用的协程开发框架,目前支持 Objective-C 和 Swift 中使用,我们底层使用汇编和C语言进行开发,上层进行提供了 Objective-C 和 Swift 的接口,目前以Apache开源协议进行了开源。

 

开源项目地址:https://github.com/alibaba/coobjc

coobjc 框架设计

  • 最底层是协程内核,包含了栈切换的管理、协程调度器的实现、协程间通信 channel 的实现等
  • 中间层是基于协程的操作符的包装,目前支持 async/await、Generator、Actor 等编程模型
  • 最上层是对系统库的协程化扩展,目前基本上覆盖了 Foundation 和 UIKit 的所有 IO和耗时方法

async/await操作符

(引用自:https://dkandalov.github.io/async-await)

上图介绍了 await 的作用,在协程函数中通过 await 调用异步方法,当前线程的执行流会立即返回,不会阻塞当前线程的执行,当异步方法执行结束后,await会恢复当前协程函数的执行,这样在协程内部是顺序执行,但是协程不会阻塞当前线程其他代码的执行。

创建协程

 

我们使用 co_launch 方法创建协程

co_launch 创建的协程默认在当前线程进行调度

await异步方法

 

在协程中我们使用 await 方法等待异步方法执行结束,得到异步执行结果

上述代码将原本需要 dispatch_async 两次的代码变成了顺序执行,代码更加简洁。

使用场景

 

协程最重要的使用场景就是异步计算(在C#等语言中通过async/await进行处理)。我们先看一个通过传统的 callback 进行异步 I/O 的场景:

上面是 iOS 开发中常见的异步调用方式,我们经常需要在 callback 中嵌套 callback,代码的缩进和逻辑变得越来越复杂,代码可读性和可维护性会随着回调的嵌套层级增长变得越来越差,进入所谓的 “callback hell” (嵌套地狱)

同样的异步计算,使用协程可以很直接的表达出来(需要有库提供了满足协程需要的 I/O 接口):

 

Generator

 

协程的另一个经典的使用场景就是懒计算序列(在C#、Python等语言中通过yield来处理)。这个懒计算序列可以通过顺序执行的代码生成,只有在需要的时候才进行计算:

这个代码创建了懒加载的斐波拉契序列,我们可以获取序列的值,通过take或者next:

传统容器类与Generator的区别

创建Generator

我们使用co_sequence创建Generator。

在其他协程中,我们可以调用next方法,获取生成器中的数据。


使用场景

生成器可以在很多场景中进行使用,比如消息队列、批量下载文件、批量加载缓存等:


通过生成器,我们可以把传统的生产者加载数据-》通知消费者模式,变成消费者需要数据-》告诉生产者加载模式,避免了在多线程计算中,需要使用很多共享变量进行状态同步,消除了在某些场景下对于锁和信号量的使用。

示例

我们接下来演示一下如何使用 Generator 进行 XML 的解析,传统的 XML 解析方式如下:

我们需要设置 Delegate,在Delegate 中处理所有的解析逻辑。

使用 Generator 解析后,我们的代码逻辑变成了如下方式:

我们可以在一个循环里遍历解析了,更加简单方便,尤其对于大的 XML 文件,我们可以只解析一部分,然后就cancel,这样可以更加节省内存。

Actor

 

Actor 的概念来自于 Erlang,在 AKKA 中,可以认为一个 Actor 就是一个容器,用以存储状态、行为、Mailbox 以及子 Actor 与 Supervisor 策略。Actor 之间并不直接通信,而是通过 Mail 来互通有无。

引用自:

https://cwiki.apache.org/confluence/display/FLINK/Akka+and+Actors

mailbox: 存储 message 的队列

Isolated State: actor 的状态,内部变量等

message: 类似于 OOP 的方法调用的参数

Actor模型的执行方式有两个特点:

 

1. 每个 Actor,单线程地依次执行发送给它的消息。

2. 不同的 Actor 可以同时执行它们的消息。

创建Actor

我们可以使用 co_actor_onqueue 在指定线程创建 actor


给actor发送消息

actor 的 send 方法可以给 actor 发送消息

使用场景

现有的面向对象编程设计的思路,很容易在多线程引发的崩溃问题,因为类的方法和属性都是暴露给调用方,调用方可以在任何线程进行调用,但是该线程往往并不是库的提供者设想的线程,就很容易出现崩溃。

 

从理论上来说,我们可以通过合理的设计来让多线程任务执行变得趋于合理,同时通过丰富的文档和示例告诉使用方该如何正确的调用我们设计的接口,但是这种通过依赖人工设计和文档来解决问题并不彻底,终究会因为疏忽而引发新的问题。

 

使用 Actor 编程模型可以帮助我们很好的设计出线程安全的模块,

示例

使用传统的方式,如果我们要实现一个计数器,可以按照如下方式实现:

传统的方式通过锁来确保线程安全,那使用 Actor,我们可以使用如下方式实现:

对于上述 actor 实现的计数器,可以按照如下方式使用:


在两个不同线程中进行调用,完全不用担心线程安全

Swift的支持

 

通过上层的封装,coobjc 完全支持 Swift,让我们可以在 Swift 中提前享用协程。

由于 Swift 更丰富、更高级的语法支持,coobjc 在 Swift 中使用更优雅,例如:

6. 使用coobjc的实际案例

 

我们以 GCDFetchFeed 开源项目中 Feeds 流更新的代码为例,演示一下协程的实际使用场景和优势,下面是原始的不使用协程的实现:


下面是 viewDidLoad 中对上述方法的调用:

上述代码无论从可读性还是简洁性上都比较差,下面我们看一下使用协程改造以后的代码:


下面是 viewDidLoad 中使用协程调用该接口的地方:


协程化改造之后的代码,变得更加简单易懂,不易出错。

7. 协程的优势

 

  • 简明
    • 概念少:只有很少的几个操作符,相比响应式几十个操作符,简直不能再简单了。
    • 原理简单: 协程的实现原理很简单,整个协程库只有几千行代码。
  • 易用
    • 使用简单:它的使用方式比GCD还要简单,接口很少。
    • 改造方便:现有代码只需要进行很少的改动就可以协程化,同时我们针对系统库提供了大量协程化接口。
  • 清晰
    • 同步写异步逻辑:同步顺序方式写代码是人类最容易接受的方式,这可以极大的减少出错的概率。
    • 可读性高: 使用协程方式编写的代码比block嵌套写出来的代码可读性要高很多。
  • 性能
    • 调度性能更快:协程本身不需要进行内核级线程的切换,调度性能快,即使创建上万个协程也毫无压力。
    • 减少卡顿卡死: 协程的使用以帮助开发减少锁、信号量的滥用,通过封装会引起阻塞的IO等协程接口,可以从根源上减少卡顿、卡死,提升应用整体的性能。

8. 总结

程序是写来给人读的,只会偶尔让机器执行一下

——Abelson and Sussman

  • 基于协程实现的编程范式能够帮助开发者编写出更加优美、健壮、可读性更强的代码
  • 协程可以帮助我们在编写并发代码的过程中减少线程和锁的使用,提升应用的性能和稳定性

未经允许不得转载:大自然的搬运工 » 基于协程的编程方式在移动端研发的思考及最佳实践

赞 (0)

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址