游戏逻辑思想
上QQ阅读APP看书,第一时间看更新

14. 同步与异步

同步与异步经常被其他人混淆,以为是和阻塞非阻塞一样的概念。但是实际上同步异步的概念要更加广一下。

我们来看一下客户端发起一个资源(不一定在本地或者可能有更新)的整个过程。

客户端调用底层接口加载资源,底层接口判断这个资源不存在或者有更新,向服务器发起下载资源的请求,请求完成后回调客户端接口,客户端接口加载资源。

客户端接口加载资源的这个过程我们可以使用阻塞或者非阻塞。但是纵观这整个过程,它是异步的。因为它需要跟网络交互,这个时间是不确定的,不是马上完成的。

如果客户端的请求是:

客户端调用底层接口加载资源,底层接口判断这个资源存在,回调客户端接口,客户端接口加载资源。

这样整个过程是一气呵成的,都在同一句代码里面发生。从资源加载到回调,我们说这样的一个行为是同步的。

最后我们再来看客户端加载资源的这个过程,因为它可能发生同步也可能发生异步的情况,所以总体上我们称它是异步的过程(只要有一环是异步的,那么它就是异步的)。

同步的操作是经常存在在我们的代码中,我们写的大量代码都是同步的,所以我们能很好的维护它的时序。而异步的代码经常伴随着回调,协程等技术。那看起来我们只需要了解它们的概念即可,然而我们要更加关注的东西是,我们可以通过机制的设计,来保证一个异步的行为在某些时候可以同步的调用。这是非常重要的,当我们在考虑设计的时候考虑到这一点,意味着我们可以通过转同步的方式来降低代码的复杂度。因为异步的代码无论是回调还是协程还是其他,都多多少少会破坏代码的可读性。在一些多个异步请求发生的过程中,处理起来需要加多个变量去记录状态做多重判断。这时候,如果我们可以将其中的某些异步操作在机制上转为同步,那么我们就可以避免代码上面的灾难。

我们来举个例子,看看怎么把异步转同步来降低代码的复杂度。SDK的提供产生有一个广告接口,它是有调用次数限制的。我们的客户端界面要求在没有广告的时候要隐藏界面的广告按钮。从前面的限制条件我们可以得出,我们要判断是否展现广告按钮,它是一个异步请求网络的流程。所以我们的接口原来是:

function canLoadAd(){

}

变成了

function canLoadAd(fCanLoad){

//调用SDK的接口,在它的回调中调用fCanLoad

}

我们原来希望canLoadAd是一个同步操作,现在不得已变成了一个异步操作,而且它还很奇怪传入了一个回调用于在判断完成后做操作,比如隐藏按钮等。首先这个接口违背了我们前面的接口设计原则,canLoadAd就应该是个简单接口,返回true或者false,任何多余的东西都不该传入。那么这时候我们就需要把它变成一个同步接口了。

我们在项目启动的时候手动调用SDK的接口获取到广告剩余次数,那么我们的接口就可以变成:

function canLoadAd(){

return 广告剩余次数

}

这就完成了一个异步转同步的过程。当然,我们这边转同步的方式是通过优化逻辑来减少代码复杂度。

事实上,我们在语言层面有个概念,叫做协程。它可以把一些异步的操作通过同步的写法写出来。注意这边和我们这节说的概念是不同的,我们这节的同步是真实的在逻辑上是同步的过程。而协议的同步化写法本质上还是异步的。我们来看几个例子来感受一下协程的同步化:

unity中的协程:

private IEnumerator Test()

{

WWW www = new WWW("www.baidu.com");

yield return www;

yield return new WaitForSeconds(8.0f);

}

本应该是WWW的回调之后进行 WaitForSeconds,现在挨着写了。

typescript:

private async loginAndLoad(loadingView){

await platform.login();

await RES.loadGroup("preload", 0, loadingView);

MyApp.instance.createScene(GameScene);

}

登录与加载资源都是异步的,也有同步的写法写了。