Как заменить контекст / планировщик задач синхронизации на другой внутри TaskCompletionSource.Task для ConfigureAwait(false)?

Предположим, я создал библиотеку, содержащую такой метод:

Task MyLibraryMethodAsync()
{
    var taskCompletionSource = new TaskCompletionSource<object>();

    Action myWorkItem =
        () =>
        {
            // Simulate some work.
            // Actual work items depend on input params.
            Thread.Sleep(TimeSpan.FromSeconds(1));

            taskCompletionSource.SetResult(null);
        };

    // The next two lines is simplification for demonstration.
    // I do not have access to the workerThread - it is created
    // and managed for me by another lib.
    // All I can do - is to post some short work items to it.
    var workerThread = new Thread(new ThreadStart(myWorkItem));
    workerThread.Start();

    return taskCompletionSource.Task;
}

Любой пользователь моей библиотеки может позвонить MyLibraryMethodAsync как это

await MyLibraryMethodAsync().ConfigureAwait(false);
VeryLongRunningMethod();
void VeryLongRunningMethod()
{
    Thread.Sleep(TimeSpan.FromHours(1));
}

И тут возникает проблема - VeryLongRunningMethod будет выполнен внутри taskCompletionSource.SetResult(null) позвонить и, таким образом, он заблокирует workerThread в течение длительного периода времени, что нежелательное поведение, потому что workerThread предназначен для запуска небольших частей кода (рабочих элементов).

Как заменить контекст / планировщик на пул потоков внутри возвращенной задачи? await x.ConfigureAwait(false) продолжить на пуле потоков, но не на workerThread?

Текущее решение, которое я нашел

Task MyLibraryMethodAsync()
{
    // ...

    return taskCompletionSource.Task
        .ContinueWith(x => x.Result, TaskScheduler.Default);
}

Однако, мне не нравится это из-за накладных расходов, которые это создает. Может быть, есть более элегантное решение?

2 ответа

Решение

Начиная с.NET 4.6 есть опция в TaskCreationOptions называется RunContinuationsAsynchronously, что делает именно то, что вы хотите, это гарантирует, что все продолжения будут выполняться асинхронно, а не синхронно при установке результата. TaskCompletionSource имеет дополнительный TaskCreationOption Параметр в своем конструкторе для вас, чтобы предоставить эту опцию.

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

Не уверен, правильно ли я вас понимаю, но вы можете явно создать фоновую задачу, чтобы избежать блокировки:

await MyLibraryMethodAsync().ConfigureAwait(false);
await Task.Run(() => VeryLongRunningMethod());

Вы даже можете пропустить ConfigureAwait тогда:

await MyLibraryMethodAsync()
await Task.Run(() => VeryLongRunningMethod());

РЕДАКТИРОВАТЬ: на основе вашего комментария: Если вы, как автор библиотеки, хотите предотвратить блокировку потока, вы можете использовать:

Task.Run(() => taskCompletionSource.SetResult(null));
Другие вопросы по тегам