注意async task返回 void 对象被释放问题 电脑版发表于:2021/6/23 13:23 `异步方法要尽量避免使用void返回值,就是不要写成async void这种写法`,就算不要返回值也要写成async task。因为一旦使用了async void这种写法,方法就不能被等待了,调用这个方法的地方就不会去等待这个方法的执行,就容易造成各种对象被释放的问题,常见的就是ef上下文对象在异步情况下被自动释放的问题。 C#的异步方法可以定义三种返回方式: `void` 、 `Task` 和 `Task<TResult>` 。其中, `Task<TResult>` 表示返回一个泛型任务,它可以在完成后提供一个T类型的结果值; Task 表示返回一个非泛型任务,它只表示一个异步操作的状态; void 表示不返回任何任务,也就是说,这个异步方法不能被调用者用类似"await"的关键字等待。 ### void类型的异步方法 ``` /// <summary> /// void类型的异步方法 /// </summary> public async void AddInquiryAndLoadPortAsync() { await DoAddInquiry(); } ``` 返回void的async方法不能被等待。这种方法只适用于异步事件处理器或其他不需要等待或错误处理的场景。返回void的async方法不能被等待,是因为它不返回任何对象,也就是说,它没有任何可被等待的任务。在C#中,要想一个对象能被等待,它必须有一个名为GetAwaiter的实例方法或扩展方法,这个方法可以返回一个awaiter对象,用于表示异步操作的状态和结果。而返回void的async方法不会产生任何对象,所以它没有GetAwaiter方法,也没有awaiter对象。因此,如果我们试图使用await关键字来等待一个返回void的async方法,编译器就会报错。这种方式又叫"发起并遗忘(Fire-and-forget)",只管发起调用,不管后续的执行状态和结果。 `异步方法要尽量避免使用void返回值,调用这个方法的地方就不会去等待这个方法的执行,就容易造成各种对象被释放的问题,常见的就是ef上下文对象在异步情况下被自动释放的问题。` ### 返回Task的异步方法 ``` /// <summary> /// 返回Task的异步方法 /// </summary> /// <returns></returns> public async Task AddInquiryAndLoadPortAsync() { await DoAddInquiry(); } ``` 返回`Task`或`Task<TResult>`的async方法可以被等待。它适用于大多数异步编程的场景,可以让调用方跟踪任务的执行状态。 这种方式也叫"等待并继续(Wait-and-continue)",发起调用后还继续跟踪任务的执行状态和结果。 ### 返回Task TResult的异步方法 ``` /// <summary> /// 返回Task<TResult>的异步方法 /// </summary> /// <returns></returns> public async Task<bool> AddInquiryAndLoadPortAsync() { bool isAdd = await DoAddInquiry(); return isAdd; } // 随便模拟一个异步方法 public Task<bool> DoAddInquiry() { return Task.Run<bool>(() => { return true; }); } ``` 和上面的方法原理一样,方法可以被等待,还可以在调用方通过`Task<TResult>`获得执行结果。能获取到方法的状态也能获取到方法的结果。 ## 相关原理解释 异步方法会被编译为一个状态机,状态机根据 await 将异步方法划分为若干片段,每执行完一个片段,就切出状态机,等待调度器重新调度。await 的原理就是调用 GetAwaiter() 方法获取 Awaiter 对象,状态机从而决定下一步动作。而 Awaiter 对象是从 Task 对象中获取的。如果异步方法的返回值是 void,也就意味着外部调用者无法获取 Awaiter 对象,其最严重的后果是,外部调用者无法捕获异步任务的异常。那么一旦异步任务发生异常,除非自行处理掉,否则该异常会直接沿着后台工作线程的调用堆栈直接传播到当前应用程序域 AppDomain,从而引发进程异常,导致整个进程崩溃。`所以,在实践中,异步方法尽量少用 void。不是说彻底禁止返回值为 void 的异步方法,而是要明白其工作原理。返回值为 void 的异步方法的最常见使用场景是异步事件`。但需要注意的是,由于外部调用者捕获不到异步事件里的异常,所以异步事件本身必须要通过 try-catch 处理掉自身的异常。