async-await的新的C#功能代替异步方法 VR资源

manew_JR 2017-09-27 16:58:01
在Unity中使用协同程序通常是解决某些问题的好方法,但它也带来了一些缺陷:
 
 
协程不能返回值。 这鼓励程序员创建巨大的整体协同程序方法,而不是从许多较小的方法中构建它们。
 
协调程序使错误处理变得困难。 您不能将yield放在try-catch中,因此不可能处理异常。 另外,当异常确实发生时,堆栈跟踪只会告诉您在何处抛出异常,所以你必须猜测它可能调用了哪些其他协程。
 
 
随着Unity 2017的发布,现在可以使用一个称为async-await的新的C#功能来代替我们的异步方法。 与协同程序相比,它具有很多不错的功能。
 
要启用此功能,所有您需要做的是打开播放器设置(编辑 - >项目设置 - >播放器),并将“脚本运行时版本”更改为“实验(.NET 4.6等效)”。
 
我们来看一个简单的例子。 给定以下协程:
 
1
2
3
4
5
6
7
8
public class AsyncExample : MonoBehaviour
{
    IEnumerator Start()
    {
        Debug.Log("Waiting 1 second...");
        yield return new WaitForSeconds(1.0f);
        Debug.Log("Done!");
    }
}
 
使用async-await等效的方法将是以下内容:
 
 
1
2
3
4
5
6
7
8
public class AsyncExample : MonoBehaviour
{
    async void Start()
    {
        Debug.Log("Waiting 1 second...");
        await Task.Delay(TimeSpan.FromSeconds(1));
        Debug.Log("Done!");
    }
}
 
 
在这两种情况下,有意识地看到发生了什么事情是有帮助的。
 
 
简而言之,Unity协同程序使用C#对迭代器块的内置支持来实现。 您提供给StartCoroutine方法的IEnumerator迭代器对象由Unity保存,每个框架都会向前转发此迭代器对象,以获取由协调程序返回的新值。 然后,您可以通过Unity读取不同的值,以触发特殊情况行为,例如执行嵌套协同程序(返回另一个IEnumerator)时,延迟几秒钟(返回类型为WaitForSeconds的实例) 等到下一帧(返回null时)。
 
 
不幸的是,由于在Unity中异步等待是相当新的,如上所述,对协同程序的内置支持对于async-await不是以类似的方式存在。 这意味着我们必须添加很多这样的支持。
 
 
Unity确实为我们提供了一件重要的事情。 正如你在上面的例子中看到的,我们的异步方法默认情况下将在主unity线程上运行。 在非Unity的C#应用程序中,异步方法通常会在单独的线程中自动运行,这在Unity中将是一个大问题,因为在这些情况下,我们并不总是能够与Unity API进行交互。 没有Unity引擎的支持,我们对Unity方法/对象的调用有时会失败,因为它们将在单独的线程上执行。 在引擎框架下,它的工作原理是因为Unity提供了一个名为UnitySynchronizationContext的默认SynchronizationContext,它会自动收集每个帧排队的任何异步代码,并在主要的Unity线程上继续运行它们。
 
 
不过事实证明,这足以令我们开始。 我们只需要一些帮助代码,让我们做一些更有趣的事情,而不仅仅是简单的时间延迟。
 

自定义 Awaiters
目前,我们可以编写很多有趣的异步代码。 我们可以调用其他异步方法,我们可以使用Task.Delay,就像上面的例子中的那样,但不是很多。
 
 
作为一个简单的例子,让我们添加直接在TimeSpan上等待的能力,而不是每次像上面的例子一样总是要调用Task.Delay。 如下:
 
 
1
2
3
4
5
6
public class AsyncExample : MonoBehaviour
{
    async void Start()
    {
        await TimeSpan.FromSeconds(1);
    }
}
 
我们需要做的只是为TimeSpan类添加一个自定义GetAwaiter扩展方法:
 
1
2
3
4
5
6
public static class AwaitExtensions
{
    public static TaskAwaiter GetAwaiter(this TimeSpan timeSpan)
    {
        return Task.Delay(timeSpan).GetAwaiter();
    }
}
 
这是因为为了支持在较新版本的C#中“等待”一个给定的对象,所需要的只是对象有一个名为GetAwaiter的方法返回Awaiter对象。 这是伟大的,因为它允许我们等待我们想要的任何东西,像上面的TimeSpan对象,而不需要改变实际的TimeSpan类。
 
 
我们可以使用同样的方法来支持等待其他类型的对象,包括Unity用于协程指令的所有类; 我们可以使WaitForSeconds,WaitForFixedUpdate,WWW等都等同于在协同程序中可以实现的方式。 我们还可以向IEnumerator添加一个GetAwaiter方法,以支持等待协程来允许使用旧的IEnumerator代码来互换异步代码。
 
 
使所有这些发生的代码可以从github repo的发行版中下载。 这样,您可以执行以下操作:
 
01
02
03
04
05
06
07
08
09
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
public class AsyncExample : MonoBehaviour
{
    public async void Start()
    {
        // Wait one second
        await new WaitForSeconds(1.0f);
  
        // Wait for IEnumerator to complete
        await CustomCoroutineAsync();
  
        await LoadModelAsync();
  
        // You can also get the final yielded value from the coroutine
        var value = (string)(await CustomCoroutineWithReturnValue());
        // value is equal to "asdf" here
  
        // Open notepad and wait for the user to exit
        var returnCode = await Process.Start("notepad.exe");
  
        // Load another scene and wait for it to finish loading
        await SceneManager.LoadSceneAsync("scene2");
    }
  
    async Task LoadModelAsync()
    {
        var assetBundle = await GetAssetBundle("www.my-server.com/myfile");
        var prefab = await assetBundle.LoadAssetAsync<GameObject>("myasset");
        GameObject.Instantiate(prefab);
        assetBundle.Unload(false);
    }
  
    async Task<AssetBundle> GetAssetBundle(string url)
    {
        return (await new WWW(url)).assetBundle
    }
  
    IEnumerator CustomCoroutineAsync()
    {
        yield return new WaitForSeconds(1.0f);
    }
  
    IEnumerator CustomCoroutineWithReturnValue()
    {
        yield return new WaitForSeconds(1.0f);
        yield return "asdf";
    }
}
 
正如你所看到的,使用异步等待这样可以非常强大,特别是当您开始组合多个异步方法时,如上面的LoadModelAsync方法。 请注意,对于返回值的异步方法,我们使用通用版本的Task,并将我们的返回值作为通用参数传递,就像上面的GetAssetBundle一样。
 
 
注意,在大多数情况下,使用WaitForSeconds实际上比我们的TimeSpan扩展方法更好,因为WaitForSeconds将使用Unity游戏时间,而我们的TimeSpan扩展方法将始终使用实时时间(因此它不会受到Time.timeScale的更改的影响)
 
 
异常处理
 
您可能已经注意到我们上面的代码有一些方法被定义为“async void”,一些方法被定义为“异步任务”或有时是“异步任务”。 那么你什么时候应该使用一个呢?
 
 
这里的主要区别是,定义为“async void”的方法不能被其他异步方法调用。 因此,这将表明我们应该始终使用返回类型的Task或Task <T>来定义我们的异步方法,以允许它们被其他异步方法调用。
 
但是,这会导致一个问题,因为异常仅在标记为“async void”的方法中发生时才记录到统一控制台。 这种行为存在很好的理由 - 允许异步代码与try-catch块正常工作。 采取以下代码:
 
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
async Task RunAsync()
{
    var task = NestedRunAsync();
  
    try
    {
        await task;
    }
    catch (Exception e)
    {
        // do something
    }
}
  
async Task NestedRunAsync()
{
    throw new Exception();
}
 
在这里,异常被NestedRunAsync方法返回的Task捕获,只有当它被等待时才被再次触发。 正如你所看到的,调用异步方法不同于等待它,这就是为什么有必要让Task对象捕获异常。
 
但是,这让我们有一个关于如何处理我们调用的根级异步方法的问题? 例如:
 
 
01
02
03
04
05
06
07
08
09
10
11
public class AsyncExample : MonoBehaviour
{
    public void Start()
    {
        RunAsync();
    }
  
    async Task RunAsync()
    {
        throw new Exception();
    }
}
 
在这种情况下,异常将被Task对象捕获,不会被记录到unity控制台。
 
我建议的解决方案来避免这个问题是为了使用一个简单的扩展方法:
 
1
2
3
4
5
6
public static class AwaitExtensions
{
    public static async void WrapErrors(this Task task)
    {
        await task;
    }
}
 
而使用情况就是这样的:
 
 
01
02
03
04
05
06
07
08
09
10
11
public class AsyncExample : MonoBehaviour
{
    public void Start()
    {
        RunAsync().WrapErrors();
    }
  
    async Task RunAsync()
    {
        throw new Exception();
    }
}
这样做非常好,并正确输出我们的异步代码中出现的任何未处理的异常(使用完整异步链的堆栈跟踪!)。 所以我们可以采用我们期望的约定,总是为所有的异步方法返回Task或Task <T>,没有任何问题。
 
另外,如果您正在使用visual studio进行编译,那么在调用不返回任务的代码时,应该收到一个警告 - 记住使用WrapErrors()方法非常有帮助。
 
从协同程序调用异步
 
对于一些代码库,从协同程序迁移到使用异步等待似乎是一项艰巨的任务。 我们可以通过允许异步等待逐步采用来简化此过程。 为了做到这一点,我们不仅需要从异步代码调用IEnumerator代码的能力,而且还需要能够从IEnumerator代码调用异步代码。 幸运的是,我们可以非常容易地添加另一个扩展方法:
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
public static class TaskExtensions
{
    public static IEnumerator AsIEnumerator(this Task task)
    {
        while (!task.IsCompleted)
        {
            yield return null;
        }
  
        if (task.IsFaulted)
        {
            throw task.Exception;
        }
    }
}
现在我们可以从这样的协程中调用异步方法:
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class AsyncExample : MonoBehaviour
{
    public void Start()
    {
        StartCoroutine(Run());
    }
  
    IEnumerator Run()
    {
        yield return RunAsync().AsIEnumerator();
    }
  
    async Task RunAsync()
    {
        // run async code
    }
}
多线程
我们也可以使用async-await来执行多个线程。 你可以通过两种方法来实现。 第一种方法是使用ConfigureAwait方法,如下所示:
 
01
02
03
04
05
06
07
08
09
10
11
public class AsyncExample : MonoBehaviour
{
    async void Start()
    {
        // Here we are on the unity thread
  
        await Task.Delay(TimeSpan.FromSeconds(1.0f)).ConfigureAwait(false);
  
        // Here we may or may not be on the unity thread depending on how the task that we
        // execute before the ConfigureAwait is implemented
    }
}
 
如上所述,Unity提供了一个名为默认SynchronizationContext的东西,默认情况下它将在主Unity线程上执行异步代码。 ConfigureAwait方法允许我们重写这个行为,所以结果将是不再保证在await下面的代码在主Unity线程上运行,而是从我们正在执行的任务继承上下文, 有些情况可能是我们想要的。
 
如果要在后台线程上显式执行代码,还可以执行以下操作:
 
01
02
03
04
05
06
07
08
09
10
11
public class AsyncExample : MonoBehaviour
{
    async void Start()
    {
        // We are on the unity thread here
  
        await new WaitForBackgroundThread();
  
        // We are now on a background thread
        // NOTE: Do not call any unity objects here or anything in the unity api!
    }
}
 
WaitForBackgroundThread是这个帖子的源代码中包含的一个类,并且将执行启动一个新线程的工作,并确保Unity的默认SynchronizationContext行为被覆盖。
 
 
返回Unity线程怎么办?
 
您可以通过等待上述我们创建的任何Unity特定对象来做到这一点。 例如:
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class AsyncExample : MonoBehaviour
{
    async void Start()
    {
        // Unity thread
  
        await new WaitForBackgroundThread();
  
        // Background thread
  
        await new WaitForSeconds(1.0f);
  
        // Unity thread again
    }
}
 
包含的源代码还提供了一个类WaitForUpdate(),您可以使用它,如果您只想返回到unity线程没有任何延迟:
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class AsyncExample : MonoBehaviour
{
    async void Start()
    {
        // Unity thread
  
        await new WaitForBackgroundThread();
  
        // Background thread
  
        await new WaitForUpdate();
  
        // Unity thread again
    }