Почему мой второй фрагмент кода асинхронного кода F # работает, но первый не работает?

NB: Я не профессиональный разработчик программного обеспечения, но я пишу много кода, который не использует асинхронное что-либо, поэтому я приношу свои извинения, если этот вопрос действительно прост.

Я взаимодействую с библиотекой, написанной на С#. Существует определенная функция (позволяет называть ее "func" ), которая возвращает "Threading.Tasks.Task > "

Я создаю библиотеку в F #, которая использует func. Я тестировал следующий фрагмент кода в консольном приложении, и он работал нормально.

let result = 
        func()
        |> Async.AwaitTask
        |> Async.RunSynchronously
        |> Array.ofSeq

Однако, когда я запускаю его из приложения WinForms (в конечном итоге, это то, что я хочу сделать), исполняемые блоки в коде формы и никогда не возвращаются.

Итак, я испортил код и пробовал следующее, которое сработало.

let result = 
        async{
            let! temp = 
                func()
                |> Async.AwaitTask

            return temp
        } |> Async.RunSynchronously |> Array.ofSeq

Почему первый фрагмент не работал? Почему второй фрагмент работы? Есть ли что-нибудь на на этой странице, которая ответит на любой из этих вопросов? Если это так, это не кажется очевидным. Если нет, можете ли вы указать мне куда-нибудь, что делает?

Ответ 1

Разница между вашим первым и вторым фрагментами заключается в том, что вызов AwaitTask происходит в разных потоках.

Попробуйте это, чтобы проверить:

let printThread() = printfn "%d" System.Threading.Thread.CurrentThread.ManagedThreadId

let result = 
    printThread()
    func()
    |> Async.AwaitTask
    |> Async.RunSynchronously
    |> Array.ofSeq

let res2 = 
    printThread()
    async {
        printThread()
        let! temp = func() |> Async.AwaitTask
        return temp
    } |> Async.RunSynchronously |> Array.ofSeq

Когда вы запустите res2, вы получите две строки вывода с двумя разными номерами. Нить, на которой работает async, - это не тот поток, на котором выполняется сама res2. Погружение в async помещает вас в другой поток.

Теперь это взаимодействует с тем, как работают задачи .NET TPL. Когда вы ходите на задание, вы не просто получаете обратный вызов в какой-то случайной нити, о нет! Вместо этого ваш обратный вызов назначается через "текущий" SynchronizationContext. Это особый вид зверя, из которого всегда есть один "текущий" (доступный через статическое свойство - говорить о глобальном состоянии!), И вы можете попросить его запланировать материал "в том же контексте", где концепция "тот же контекст" определяется реализацией.

WinForms, конечно, имеет свою собственную реализацию, точно названную WindowsFormsSynchronizationContext. Когда вы работаете в обработчике событий WinForms и запрашиваете текущий контекст для планирования чего-то, он будет запланирован с использованием собственного цикла событий WinForms - a la Control.Invoke.

Но, конечно, поскольку вы блокируете поток цикла событий с помощью Async.RunSynchronously, задача не будет иметь шансов на успех. Вы ждёте, что это произойдет, и он ждет вас, чтобы выпустить поток. Ака "тупик".

Чтобы исправить это, вам нужно запустить ожидание в другом потоке, чтобы контекст синхронизации WinForms не использовался - решение, на которое вы случайно наткнулись.

Альтернативным рекомендуемым решением является указание TPL явно не использовать "текущий" контекст через Task.ConfigureAwait:

let result = 
    func().ConfigureAwait( continueOnCapturedContext = false )
    |> Async.AwaitTask
    |> Async.RunSynchronously
    |> Array.ofSeq

К сожалению, это не скомпилируется, потому что Async.AwaitTask ожидает Task, а ConfigureAwait возвращает ConfiguredTaskAwaitable.