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

6

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

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会创建一个新线程。


2
“async”并不总是意味着涉及其他线程-请参见没有线程 - Charles Mager
你的 SyncData 方法缺少 await 关键字,这会告诉编译器继续进行其他处理。目前你的 SyncData 方法应该是阻塞的,这是正确的。 - user1269016
3个回答

10
不应该相同吗?我认为不使用await调用异步方法会创建一个新线程。 不,async并不会为其方法调用自动分配新线程。async-await主要是利用自然异步的API,例如对数据库或远程Web服务的网络调用。当您使用Task.Run时,您明确使用线程池线程来执行委托。如果您使用async关键字标记方法,但在内部没有任何await,它将同步执行。我不确定您的SyncContacts()方法实际上做了什么(因为您没有提供其实现),但仅标记async将不会带来任何好处。编辑:现在您已经添加了实现,我看到两件事情:
  1. I'm not sure how CPU intensive is your synchronous data analysis, but it may be enough for the UI to get unresponsive.
  2. You're not awaiting your asynchronous operation. It needs to look like this:

    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);
    }
    

你确定它明确使用了线程池吗?据我所知,它明确使用默认的TaskScheduler,而这个调度器恰好包装了线程池。我们可能知道它使用了线程池,但是抽象层旨在隐藏此细节,并且不保证在下一个 .net 版本中不会更改。 - Gusdor
4
Task.Run默认使用TaskScheduler.Default作为任务调度器,该调度器是线程池任务调度器。 - Yuval Itzchakov
@Gusdor,Yuval是正确的。根据MSDN:“将指定的工作排队运行在线程池上,并返回该工作的任务或Task<TResult>句柄。” - shay__
谢谢Yuval,我已经更新了关于SyncContacts实现的更多细节的问题。 - nimatra
1
return await 不在最后一行的唯一原因是因为我们正在返回,所以没有必要等待,对吧? - Sinjai
1
@Sinjai,这是因为我想避免额外的状态机分配。由于某人已经在调用堆栈的更高位置等待,而方法中没有其他异步操作,所以我们可以这样做。 - Yuval Itzchakov

0

你的代码行 Task.Run(() => this.SyncContacts()); 实际上是创建了一个新任务并启动它,然后将其返回给调用者(在你的情况下没有进一步使用)。这就是为什么它会在后台执行工作而UI仍然可以继续工作的原因。如果你需要等待任务完成,你可以使用 await Task.Run(() => this.SyncContacts());。如果你只想确保 SyncContacts 在返回 SyncData 方法时已经完成,你可以使用返回的任务并在 SyncData 方法末尾等待它。正如评论中建议的那样:如果你不关心任务是否完成,你可以直接返回它。

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


那是不正确的。await 与某些东西是否在后台运行无关。它与如何处理等待任务完成以及如何处理其响应有关。微软也不建议在所有情况下使用 await,因为它会引入开销。如果返回一个 Task 的方法不需要等待任务完成,那么没有理由使用 async/await,只需返回 Task 即可。 - Panagiotis Kanavos
1
我并没有说await会在后台运行任务。我说的是Task.Run()会这样做。关于第二件事,你是对的,我应该更好地说明MS建议不要混合阻塞代码和异步代码https://msdn.microsoft.com/en-us/magazine/jj991977.aspx。无论如何,返回一个Task而不是void肯定是必须的。 - Daniel Fink

0

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

因此,无论是否await了从SyncContacts返回的Task,在调用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);
}

(回复: 关于SyncContacts为何不使用async/return await- 参见Yuval的观点 - 在此情况下,将方法设为异步并等待结果会浪费资源)

对于WPF项目,可以使用Task.Run来使CPU密集型工作脱离调用线程(但对于MVC或WebAPI Asp.Net项目,则不行)。

同时,假设contactsToUpload映射工作是线程安全的,并且您的应用程序可以充分利用用户的资源,您也可以考虑并行映射以减少总体执行时间:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接