Day2:非同步执行与 Callback 的问题

在前一篇文章中,我们知道依据程序的执行顺序分成两种执行方式,一种是同步(Synchronous) 、另一种则是非同步(Asynchronous)

同步

同步的执行方式会依照程序码的顺序,依序执行。如下:

fun sayHello(){
	println("Hello")
	println("Nice to meet you!")
}

执行 sayHello() 我们就可以得到如我们程序码顺序的结果。

Hello
Nice to Meet you!

执行顺序图

https://imgur.com/U1ZiJ98

好的,同步程序的执行方式是如此直觉:按照顺序。


假如,有一个方法 showContents() 为我们登入 FB 之後,系统会根据登入的使用者,把最近的内容列出来。

以这个例子来说,我们会有三个步骤,

  1. 登入 FB,取得使用者资讯。
  2. 根据使用者资讯,来取得最近的内容
  3. 将最近的内容显示在画面上

Pseudo code 如下:

fun showContents() {
	val token = login(userName, password)
	val contents = fetchLatestContents(token)
	showContents(contents)
}

login() 会回传 TokenfetchLatestContents() 会使用 login() 所回传的 Token 来呼叫另一个 API 取得最新的内容,最後则是把 fetchLatestContentes() 回传的结果传给 showContents() ,让它来显示内容。

其中三个函式的定义如下:

fun login(userName: String, password: String): Token{...}
fun fetchLatestContent(token: Token): List<Contents> {...}
fun showContents(contents: List<Contents>){...}

这种写法依然是我们所熟悉的同步式,但是可能会有一个问题,假如每一段函式时,都需要一段时间才能够取得资料,当我们直接呼叫,也就是使用主执行绪 (Main Thread) 来执行时,那我们就会发现画面被卡住。

例如 fetchLatestContent() 需要花费较长的时间,执行时间图如下:

有一个任务执行很久

解决方案

这个情况是不是跟我们昨天所提到的情况很类似呢?有一个耗时的任务(I/O任务)在主执行绪上执行,那麽其他在主执行绪上的任务将无法执行,因为一次只能执行一项任务。还记得我们可以怎麽处理吗?

.

.

.

.

没错,答案就是「非同步」,那麽实务上要怎麽实作非同步的程序呢?

建立新的执行绪

最直觉的想法是,由於在主执行绪上执行耗时任务会造成主执行绪卡住,所以建立新的执行绪来处理这些耗时的工作,避免占用主执行绪。

fun thread01(){
	thread {
		// a work in new thread
	}
}

在 Kotlin 中,我们可以使用 thread{} 来新建一个执行绪,并且在这个括弧中 {} 执行我们所需要执行的任务。

如果我们尝试将前面的 pseudo code 改成使用 thread{},会发生什麽事呢?

fun login(userName: String, password: String): Token{
    thread {
        // ...
        return@thread token // <- 没有办法在这边直接回传值
    }
}

我们会发现,在 return@thread token 这一行会出现, Type mismatch

原因在 thread{} 中,是不允许回传值的,因为 thread{} 只接受 Unit,也就是没有回传值。

那,建立新的执行绪这招难道真的没有办法使用吗?

/images/emoticon/emoticon06.gif

使用 Callback

重新检视上述的情境,一个函式的结果会当作下一个函式的输入传递进去,我们要怎麽解决这个问题呢?

在 Kotlin 中,我们可以将 lambda function 当作一个参数传进函数中,所以解决这个问题的另一个思路是,将一个 lambda function 传进去函式内,当执行完成之後,就呼叫 lambda function 通知下一个函式。

fun login(userName: String, password: String): Token{ ... }
fun fetchLatestContent(token: Token): List<Contents> { ... }
fun showContents(contents: List<Contents>){ ... }

改成

fun loginAsync(userName: String, password: String, callback: (Token) -> Unit) {
  thread {
      //....
      callback.invoke(token)
  }
}

fun fetchLatestContentAsync(token: Token, callback: (List<Contents>) -> Unit) {
    thread {
        val content = service.fetchContent(token)
        callback.invoke(contents)
    }
}

fun showContents(contents: List<Contents>){ ... }

如果 Lambda 函式是参数的最後一项,那麽我们可以将 Lambda 函式从括弧中搬出来,让程序的结构更为清晰。

那麽我们就可以将原本的直叙式的写法,改成 Callback 的写法。如下

fun showContents(){
    loginAsync(userName, password){ token -> 
        fetchLatestContentAsync(token){ contents -> 
            showContents(contents)
        }	
    }
}

Callback 的问题

但是, 用 Callback 的写法有两个缺点,第一个是如果有多个函式都需要使用 Callback,那麽在观看程序码的时候,就会变得很不好看、不容易被维护。这就是 Callback hell (回呼地狱)。

请自行 Google Callback hell

使用 Callback 的另一个问题就是会发生控制权转移 (Inversion of Control) 的情况,什麽是控制权转移呢?看下面的范例:

假如有两个函式,它们都各有两个参数,第一个参数为输入的值,另一个参数则为一个 Lambda 函式,作为 Callback 使用。由下方的程序码可以得知,呼叫 doA 会将输入的数值利用 callback 传出去,同样的,如果呼叫 doB 也会将输入的数值利用 callback 传递出去。

fun doA(value: Int, callback: (Int) -> Unit) {
    callback(value)
}

fun doB(value: Int, callback: (String) -> Unit) {
    callback(value.toString())
}

假如我们将这两个函式串在一起。

doA(1){ valueA -> 
	doB(valueA){ valueB ->
		println(valueB)
	}
}

当我们呼叫上面的函式时, doA 会在它里面将输入的值透过 Lambda 函式传给 doB 。以上面的范例来说,我们最後就会列印出 1

如果 doA 的内部不小心呼叫两次 callback

fun doA(value: Int, callback: (Int) -> Unit) {
    callback(value)
		callback(value)
}

那麽原本的结果就会出现连续两个 1 。这明显不是我们想要的。

doB 将自己输入数值的控制权转交给 doA 来呼叫,当 doA 错误呼叫时,後面的呼叫就有可能出错。


那麽要怎麽避免 Callback hell、不在主执行绪上执行,又能让程序码是以非同步的方式执行呢?

能解决这个问题的方法有很多种: Futures , Promise, RxJava 以及本系列文章的主角 Coroutine

心智图

非同步心智图

特别感谢

Kotlin Taiwan User Group
Kotlin 读书会


<<:  [Day6] 呼吸灯制作

>>:  Day01 序 -- 大致安好

IOS、Python自学心得30天 Day-29 连接Firebase辨识

前言: 衔接完成 程序码: // // ViewController.swift // Dog Br...

Day14-旧网站重写成Vue_5_多图片切换

昨天PO完文重看一下旧文才发现前天说要讲json做轮播,结果昨天先讲了tab…. 希望今天能讲完轮播...

Day4|【Git】用户名称与信箱- Git的初始设定与 config

💡 开始使用 Git 之前,我们需要先设定使用者名称及电子邮件地址。 为什麽需要设定用户名称及 E-...

Day08:【TypeScript 学起来】物件型别 Object Types : object

https://bit.ly/2XuVqBJ (这篇必看,不分享对不起自己) //原来南无观世音菩...

Day5 Type

Background 对於变数的Type,能够依据他们的特性分为两种,分别为不可变的Static t...