Async.StartImmediate против Async.RunSynchronously

Как мое ограниченное (или даже неправильное) понимание, как Async.StartImmediate, так и Async.RunSynchronously запускают асинхронное вычисление на текущем потоке. Тогда в чем же разница между этими двумя функциями? Может кто-нибудь помочь объяснить?

Update:

Изучив исходный код F # на https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/control.fs, я думаю, что я понимаю, что происходит. Async.StartImmediate запускает асинхронный поток текущего потока. После того, как он ударит асинхронную привязку, будет ли она продолжать работать в текущем потоке, зависит от самой привязки async. Например, если асинхронная привязка вызывает Async.SwitchToThreadPool, она будет работать на ThreadPool вместо текущего потока. В этом случае вам нужно будет вызвать Async.SwitchToContext, если вы хотите вернуться к текущему потоку. В противном случае, если привязка async не делает никакого переключения на другие потоки, Async.StartImmediate будет продолжать выполнять асинхронную привязку к текущему потоку. В этом случае нет необходимости вызывать Async.SwitchToContext, если вы просто хотите остаться в текущем потоке.

Причина, по которой пример Dax Fohls работает в потоке графического интерфейса, заключается в том, что Async.Sleep тщательно захватывает SynchronizationContext.Current и убедитесь, что продолжение выполняется в захваченном контексте, используя SynchronizationContext.Post(). См. https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/control.fs#L1631, где unprotectedPrimitiveWithResync обертка изменяет "args.cont" (продолжение) для публикации в захваченный контекст (см.: https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/control.fs#L1008 - trampolineHolder.Post - это в основном SynchronizationContext.Post). Это будет работать только когда SynchronizationContext.Current не является нулевым, что всегда имеет место для потока GUI. Особенно, если вы запустите консольное приложение с StartImmediate, вы обнаружите, что Async.Sleep действительно отправится в ThreadPool, потому что основной поток в консольном приложении не имеет SynchronizationContext.Current.

Итак, чтобы подвести итог, это действительно работает с потоком GUI, потому что некоторые функции, такие как Async.Sleep, Async.AwaitWaitHandle и т.д., тщательно захватывают и не забудьте вернуться к предыдущему контексту. Похоже, это преднамеренное поведение, однако это, похоже, не документировано нигде в MSDN.

Ответ 1

Async.RunSynchronously ожидает завершения всего вычисления. Поэтому используйте это в любое время, когда вам нужно выполнить асинхронное вычисление из обычного кода и ждать результата. Достаточно просто.

Async.StartImmediate гарантирует, что вычисление выполняется в текущем контексте, но не дожидается завершения всего выражения. Наиболее частое использование этого (для меня, по крайней мере) - это то, когда вы хотите асинхронно запускать вычисление в потоке графического интерфейса. Например, если вы хотите сделать три вещи в потоке графического интерфейса с интервалом в 1 секунду, вы можете написать

async {
  do! Async.Sleep 1000
  doThing1()
  do! Async.Sleep 1000
  doThing2()
  do! Async.Sleep 1000
  doThing3()
} |> Async.StartImmediate

Это обеспечит, чтобы все вызывалось в потоке GUI (при условии, что вы вызываете это из потока GUI), но не будет блокировать поток GUI в течение целых 3 секунд. Если вы используете RunSynchronously там, он будет блокировать поток GUI на время, и ваш экран перестанет реагировать.

(Если вы еще не разработали GUI-программирование, просто обратите внимание, что обновления для элементов управления графическим интерфейсом должны выполняться из одного и того же потока, что может быть сложно скоординировать вручную, а это лишний раз снимает боль).

Чтобы привести еще один пример, здесь:

// Async.StartImmediate
async {
  printfn "Running"
  do! Async.Sleep 1000
  printfn "Finished"
} |> Async.StartImmediate
printfn "Next"

> Running
> Next
// 1 sec later
> Finished

// Async.RunSynchronously
async {
  printfn "Running"
  do! Async.Sleep 1000
  printfn "Finished"
} |> Async.RunSynchronously
printfn "Next"

> Running
// 1 sec later
> Finished
> Next

// Async.Start just for completion:
async {
  printfn "Running"
  do! Async.Sleep 1000
  printfn "Finished"
} |> Async.Start
printfn "Next"

> Next
> Running // With possible race condition since they're two different threads.
// 1 sec later
> Finished

Также обратите внимание, что Async.StartImmediate не может вернуть значение (так как оно не заканчивается до продолжения), тогда как RunSynchronously может.