调用异步方法和 Task.Run 异步方法的区别

Difference between calling an async method and Task.Run an async method

我的视图模型中有一个方法

private async void SyncData(SyncMessage syncMessage)
{
    if (syncMessage.State == SyncState.SyncContacts)
    {
        this.SyncContacts(); 
    }
}

private async Task SyncContacts()
{
    foreach(var contact in this.AllContacts)
    {
       // do synchronous data analysis
    }

    // ...

    // AddContacts is an async method
    CloudInstance.AddContacts(contactsToUpload);
}

当我从 UI 命令调用 SyncData 并且我正在同步大量数据时 UI 冻结。但是当我用这种方法调用 SyncContacts

private void SyncData(SyncMessage syncMessage)
{
    if (syncMessage.State == SyncState.SyncContacts)
    {
        Task.Run(() => this.SyncContacts()); 
    }
}

一切都很好。他们不应该是一样的吗? 我在想不使用 await 来调用异步方法会创建一个新线程。

Should not they be the same? I was thinking that not using await for calling an async method creates a new thread.

不,async 不会神奇地为其方法调用分配新线程。 async-await 主要是关于利用自然异步 API,例如对数据库或远程 Web 服务的网络调用。

当您使用 Task.Run 时,您明确使用线程池线程来执行您的委托。如果您使用 async 关键字标记一个方法,但在内部不 await 任何东西,它将同步执行。

我不确定你的 SyncContacts() 方法实际上做了什么(因为你没有提供它的实现),但是标记它 async 本身不会给你任何好处。

编辑:

现在您已经添加了实现,我看到两件事:

  1. 我不确定您的同步数据分析 CPU 有多密集,但这可能足以让 UI 无响应。
  2. 您没有在等待异步操作。它需要看起来像这样:

    private async Task SyncDataAsync(SyncMessage syncMessage)
    {
        if (syncMessage.State == SyncState.SyncContacts)
        {
            await this.SyncContactsAsync(); 
        }
    }
    
    private Task SyncContactsAsync()
    {
        foreach(var contact in this.AllContacts)
        {
           // do synchronous data analysis
        }
    
        // ...
    
        // AddContacts is an async method
        return CloudInstance.AddContactsAsync(contactsToUpload);
    }
    

你的行 Task.Run(() => this.SyncContacts()); 真正做的是创建一个新任务来启动它,然后 return 将它发送给调用者(在你的情况下不会用于任何进一步的目的)。这就是为什么它会在后台工作并且 UI 会继续工作的原因。如果您需要 (a) 等待任务完成,您可以使用 await Task.Run(() => this.SyncContacts());。如果您只想确保 SyncContacts 在您 return 您的 SyncData 方法时完成,您可以使用 returning 任务并在 SyncData 方法结束时等待它。正如评论中所建议的那样:如果您对任务是否已完成不感兴趣,您可以 return 它。

但是,Microsoft 建议不要混合使用阻塞代码和异步代码,并且异步方法以 Async (https://msdn.microsoft.com/en-us/magazine/jj991977.aspx) 结尾。因此,当您不使用 await 关键字时,您应该考虑重命名您的方法并且不要将方法标记为 async。

只是为了澄清 UI 冻结的原因 - 在紧密的 foreach 循环中完成的工作很可能 CPU-bound 并且会阻塞原始调用者的线程直到循环完成。

因此,无论 SyncContacts 返回的 Task 是否被 await 编辑,调用 AddContactsAsync 之前的 CPU 绑定工作将仍然同步发生并阻塞调用者的线程。

private Task SyncContacts()
{
    foreach(var contact in this.AllContacts)
    {
       // ** CPU intensive work here.
    }

    // Will return immediately with a Task which will complete asynchronously
    return CloudInstance.AddContactsAsync(contactsToUpload);
}

(回复:没有为什么 async / return awaitSyncContacts- 参见 Yuval 的观点 - 使方法异步并等待结果本来是 wasteful in this instance

对于 WPF 项目,使用 Task.Run 在调用线程之外执行 CPU 绑定工作应该没问题(但 not so for MVC or WebAPI Asp.Net 项目)。

此外,假设 contactsToUpload 映射工作是线程安全的,并且您的应用充分利用了用户的资源,您还可以考虑并行化映射以减少整体执行时间:

var contactsToUpload = this.AllContacts
    .AsParallel()
    .Select(contact => MapToUploadContact(contact)); 
    // or simpler, .Select(MapToUploadContact);