在前面章节中,我们已经看到了并行编程的工作方式:创建称为工作单元(Unit of Work)的小任务,这些任务可以由一个或多个线程同时执行。
本章将从介绍同步代码和异步代码之间的区别开始,然后讨论何时适合使用异步代码,以及何时应避免异步代码。最后我们将讨论并行编程中的新功能以解决异步代码复杂性的帮助。
本教程对应学习工程:魔术师Dix / HandsOnParallelProgramming · GitCode
本章相当于阶段性总结,新知识不多,总体较为简单。
书上这里讲了同步操作和异步操作的执行顺序,我觉得是属于偏新手向的,不需要额外说明。同步执行就是代码只能一行一行、从上往下执行;异步的话就会同时执行多个代码块。相信这点基础知识不需要赘述大家都能明白,理解同步与异步并不难。
这里直接跳过,下一部分。
何时适合使用异步编程,微软有个说明文档:
同步和异步操作 - WCF | Microsoft Learn了解实现和调用异步服务操作。 WCF 服务和客户端可以在应用程序的两个级别使用异步操作。https://learn.microsoft.com/zh-cn/dotnet/framework/wcf/synchronous-and-asynchronous-operations
里面建议了以下情形:
如果从中间层应用程序调用操作。
果在 ASP.NET 页中调用操作,可使用异步页。
如果从任何单线程的应用程序(如 Windows 窗体或 Windows Presentation Foundation (WPF))调用操作。 使用基于事件的异步调用模型时,将在 UI 线程上引发结果事件,从而向应用程序添加响应性而无需您自己处理多个线程。
粗粗看了一下,跟我们 Unity 开发一点关系都没有。而且对于 Unity 开发,何时使用异步多线程一直有较为明确的场景:网络、IO、大量数据的处理等。总之,出于对性能的考虑,自然是当某任务放在主线程会造成卡顿时,就要考虑是否要异步多线程。
有以下实现异步的方式:
使用 Delegate.BeginInvoke 方法;
使用 Task 类;
使用 IAsyncResult 接口;
使用 async 和 await 关键字。
这个是对于委托有这个方法,我们直接看一段代码:
private void RunWithBegionInvoke()
{
Action logAction = AsyncLogAction;
logAction.BeginInvoke(null, null);
Debug.Log("RunWithBegionInvoke !!!");
}
public static async void AsyncLogAction()
{
await Task.Delay(2500);
Debug.Log("AsyncLogAction Finish !!!");
}
我们直接执行,结果如下:
可以看到,BeginInvoke 并没有阻塞主线程,而实现了异步的效果。同时,这个函数还支持回调和参数,可以说是非常好用了。他这个底层其实也是用 IAsyncResult 实现的,当然自动实现的会有一定的额外开销。
这之前用太多了,不赘述了,可以参考第二章。略过。
IAsyncResult 接口 (System) | Microsoft Learn表示异步操作的状态。 https://learn.microsoft.com/zh-cn/dotnet/api/system.iasyncresult?view=netstandard-2.1 这种接口语法的知识,直接上代码比较好理解。这里我们随便定义一个自己的 IAsyncResult,当然里面的参数我就随便写写:
///
/// IAsyncResult 的示例代码;
///
public class MyAsyncResult : IAsyncResult
{
public MyAsyncResult(MyAsyncState state)
{
m_AutoResetEvent = new AutoResetEvent(false);
IsCompleted = false;
m_AsyncState = state;
CompletedSynchronously = false;
}
private MyAsyncState m_AsyncState;
public object AsyncState => m_AsyncState;
private AutoResetEvent m_AutoResetEvent;
public WaitHandle AsyncWaitHandle => m_AutoResetEvent;
public bool CompletedSynchronously { get; private set; }
public bool IsCompleted { get; private set; }
}
///
/// 自定义的数据结构
///
public class MyAsyncState
{
public Vector3 Positon;
public Quaternion Rotation;
}
之后我们尝试调用他:
private void RunWtihMyAsyncResult()
{
MyAsyncState state = new MyAsyncState();
state.Positon = new Vector3(50, 811, 55);
state.Rotation = Quaternion.Euler(7, 8, 9);
MyAsyncResult result = new MyAsyncResult(state);
Action logAction = AsyncLogAction;
// OnActionCallBack 的回调仍然在子线程
logAction.BeginInvoke(OnActionCallBack, result);
Debug.Log($"RunWtihMyAsyncResult : {Task.CurrentId}");
}
public static void OnActionCallBack(IAsyncResult result)
{
Debug.Log($"OnActionCallBack : {result.GetType()} | {Task.CurrentId} !");
MyAsyncResult myResult = result.AsyncState as MyAsyncResult;
MyAsyncState myState = myResult.AsyncState as MyAsyncState;
Debug.Log($"Positon : {myState.Positon}");
Debug.Log($"EulerAngles : {myState.Rotation.eulerAngles}");
Debug.Log($"IsCompleted : {result.IsCompleted}");
Debug.Log($"CompletedSynchronously : {result.CompletedSynchronously}");
Debug.Log($"OnActionCallBack : End !");
}
可以看到,这里的回调 MyAsyncResult 是返回的 IAsyncResult 中的 AsyncState ,属于层层套娃了。系统返回的这个接口实现是 System.Runtime.Remoting.Messaging.AsyncResult 这个类,应该是自动封装的。
注意:BeginInvoke 返回调用的 IAsyncResult 接口仍然是在子线程 !
当然,上述写法发现,我们自己写的这个 IAsyncResult 几乎没有什么用……不过 C# 提供了一个通用的简便写法:
private void RunWtihAsyncCallBack()
{
MyAsyncState state = new MyAsyncState();
state.Positon = new Vector3(888, 831, 255);
state.Rotation = Quaternion.Euler(76, 38,329);
Action logAction = TestFunction.AsyncLogAction;
logAction.BeginInvoke(TestFunction.OnActionCallBackSimple, state);
}
这里返回的同样也是 System.Runtime.Remoting.Messaging.AsyncResult 。和我预期不一样的是,回调的调用是比委托先执行的,所以暂时不知道这个接口有啥用。
这里作者提供了4种情形:
没有连接池的单个数据库中
更注重代码的易于阅读和维护
操作简单且运行时间短
应用程序使用了大量的共享资源
其实对于我们 Untiy 开发来说,这些都不是问题。Unity 本来就推荐在主线程做完所有事情,多线程就是高级用法。写多线程的程序员一般都知道何时该使用多线程。Unity 由于禁用了很多接口在子线程调用,实际上大部分和 Untiy 相关的代码都只能在主线程,使用子线程的大部分是 IO 或者纯数据处理等。
本章其实算是总结吧,介绍了异步和同步的概念,还讨论了异步的各种实现。但我个人感觉干货其实并不多,有用的东西不如前面几章讲的,有点水。
本教程对应学习工程:魔术师Dix / HandsOnParallelProgramming · GitCode