Async/Await为何出现多线程?如何限定仅在主线程运行?
问题描述
原本以为使用Async/Await的程序仅会占用1个线程:编译器遇到await关键字时,会在同一线程处理其他任务,直至等待的操作完成,仅等待的目标操作可能在其他线程执行,但程序本身不会创建新线程。但实际VB代码中出现主线程和工作线程卡在同一位置,疑惑是否是Task.WhenAll和Task.WhenAny导致了多线程,想知道如何让所有逻辑仅在主线程运行。
日志记录代码
Public Sub LogEvents(ByVal whatToLog As String, Optional ByVal canRandom As Boolean = True) Static logNumber As Integer Dim timeStamp As String timeStamp = CStr(Microsoft.VisualBasic.Now) whatToLog = timeStamp & " " & whatToLog & Microsoft.VisualBasic.vbNewLine Try Debug.Print(whatToLog) System.IO.File.AppendAllText("log.txt", whatToLog, defaultEncoding) ' ... 省略后续代码 End Sub
任务创建代码
For Each account In uniqueAccounts().Values Dim newtask = account.getMarketInfoAsync().ContinueWith(Sub() account.LogFinishTask("Geting Market Info", starttime)) LogEvents("marketinfo account " + account.Exchange + " is being done by task " + newtask.Id.ToString + " " + newtask.ToString) tasklist.Add(newtask) 'newtask.ContinueWith(Sub() LogEvents(account.ToString)) Next
后续调用代码
LogEvents("Really Start Getting Market Detail of All") Try Await jsonHelper.whenAllWithTimeout(tasklist.ToArray, 500000) Catch ex As Exception Dim b = 1 End Try
whenAllWithTimeout函数代码
Public Shared Async Function whenAllWithTimeout(taskar As Task(), timeout As Integer) As Task Dim timeoutTask = Task.Delay(timeout) Dim maintask = Task.WhenAll(taskar) Await Task.WhenAny({timeoutTask, maintask}) If maintask.IsCompleted Then Dim b = 1 For Each tsk In taskar LogEvents("Not Time Out. Status of task " + tsk.Id.ToString + " is " + tsk.IsCompleted.ToString) Next End If If timeoutTask.IsCompleted Then Dim b = 1 For Each tsk In taskar LogEvents("status of task " + tsk.Id.ToString + " is " + tsk.IsCompleted.ToString) Next End If End Function
问题分析与解决方案
为什么会出现多线程?
- ContinueWith的默认调度逻辑:
ContinueWith默认会把后续任务放到线程池线程执行,不会自动回到原上下文(比如主线程),这是工作线程出现的核心原因。Async/Await本身会在await恢复时回到原同步上下文,但ContinueWith不遵循这个规则。 - Task.WhenAll/WhenAny不创建线程:这两个方法只是等待任务完成,不会主动生成新线程,但如果传入的任务本身在多线程执行(比如
getMarketInfoAsync内部用了线程池,或者ContinueWith导致的),自然会出现多线程运行的情况。 - LogEvents的竞态风险:
LogEvents里的Static logNumber是线程不安全的,加上多线程同时调用文件写入操作,可能导致线程阻塞或冲突。
如何让所有逻辑在主线程运行?
用Await替换ContinueWith:这是最简洁可靠的方式,Async/Await会自动处理上下文切换,确保后续代码回到主线程:
' 串行执行的写法 For Each account In uniqueAccounts().Values Await account.getMarketInfoAsync() account.LogFinishTask("Geting Market Info", starttime) LogEvents($"marketinfo account {account.Exchange} completed") Next ' 如果需要并行执行所有任务 Dim taskList As New List(Of Task)() For Each account In uniqueAccounts().Values ' 先收集所有异步任务 taskList.Add(account.getMarketInfoAsync()) Next ' 等待所有任务完成,恢复时回到主线程 Await Task.WhenAll(taskList) ' 统一执行后续逻辑 For Each account In uniqueAccounts().Values account.LogFinishTask("Geting Market Info", starttime) Next给ContinueWith指定主线程调度器:如果一定要用
ContinueWith,需要显式指定调度器,强制后续任务在主线程执行:Dim newtask = account.getMarketInfoAsync().ContinueWith( Sub() account.LogFinishTask("Geting Market Info", starttime), TaskScheduler.FromCurrentSynchronizationContext() )修复LogEvents的线程安全问题:即使在单线程运行,也建议优化方法的安全性,避免后续引入多线程时出问题:
Private Shared logLock As New Object() Public Sub LogEvents(ByVal whatToLog As String, Optional ByVal canRandom As Boolean = True) Static logNumber As Integer Dim timeStamp As String = CStr(Microsoft.VisualBasic.Now) whatToLog = timeStamp & " " & whatToLog & Microsoft.VisualBasic.vbNewLine SyncLock logLock Debug.Print(whatToLog) System.IO.File.AppendAllText("log.txt", whatToLog, defaultEncoding) End SyncLock ' ... 省略后续代码 End Sub检查getMarketInfoAsync内部实现:如果这个方法内部用了
Task.Run包裹同步操作,会主动切换到线程池线程。要完全在主线程运行,需要确保它使用真正的异步IO API(比如HttpClient的异步方法),而不是模拟异步。
内容的提问来源于stack exchange,提问作者user4951
相关产品推荐
相关产品推荐

