Когда следует использовать TaskCompletionSource <T>?

199

AFAIK, все, что он знает, - это то, что в какой-то момент вызывается его метод SetResultили SetExceptionметод для завершения Task<T>экспонирования через его Taskсвойство.

Другими словами, он выступает в качестве производителя для Task<TResult>и его завершения.

Я видел здесь пример:

Если мне нужен способ выполнить Func асинхронно и иметь задачу для представления этой операции.

public static Task<T> RunAsync<T>(Func<T> function) 
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ => 
    { 
        try 
        {  
            T result = function(); 
            tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
    }); 
    return tcs.Task; 
}

Который может быть использован *, если у меня не было Task.Factory.StartNew- но у меня есть Task.Factory.StartNew.

Вопрос:

Может кто - то пожалуйста , объясните на примере сценарий , связанный непосредственно с , TaskCompletionSource а не к гипотетической ситуации , в которой у меня нет Task.Factory.StartNew?

Ройи Намир
источник
5
TaskCompletionSource в основном используется для переноса асинхронного API на основе событий в Task без создания новых потоков.
Арвис

Ответы:

230

Я в основном использую его, когда доступен только API, основанный на событиях ( например, сокеты Windows Phone 8 ):

public Task<Args> SomeApiWrapper()
{
    TaskCompletionSource<Args> tcs = new TaskCompletionSource<Args>(); 

    var obj = new SomeApi();

    // will get raised, when the work is done
    obj.Done += (args) => 
    {
        // this will notify the caller 
        // of the SomeApiWrapper that 
        // the task just completed
        tcs.SetResult(args);
    }

    // start the work
    obj.Do();

    return tcs.Task;
}

Так что это особенно полезно, когда используется вместе с asyncключевым словом C # 5 .

GameScripting
источник
4
Вы можете написать словами, что мы видим здесь? это как то SomeApiWrapper, чего ждут где-нибудь, пока издатель не поднимет событие, которое заставит эту задачу завершиться?
Ройи Намир
посмотрите на ссылку, которую я только что добавил
GameScripting
6
Просто для обновления Microsoft выпустила Microsoft.Bcl.Asyncпакет для NuGet, который позволяет использовать async/awaitключевые слова в проектах .NET 4.0 (рекомендуется VS2012 и выше).
Эрик
1
@ Fran_gg7 вы можете использовать CancellationToken, см. Msdn.microsoft.com/en-us/library/dd997396(v=vs.110).aspx или как новый вопрос здесь о stackoverflow
GameScripting
1
Проблема с этой реализацией состоит в том, что это приводит к утечке памяти, поскольку событие никогда не освобождается от obj.Done
Уолтер Веховен,
78

По моему опыту, TaskCompletionSourceотлично подходит для упаковки старых асинхронных шаблонов в современныеasync/await шаблон.

Самый полезный пример, который я могу вспомнить, - это работа с Socket. У него есть старые шаблоны APM и EAP, но нет awaitable Taskметодов, которые TcpListenerи TcpClientесть.

У меня лично есть несколько проблем с NetworkStreamклассом, и я предпочитаю сырье Socket. Поскольку я также люблю async/awaitшаблон, я создал класс расширения, SocketExtenderкоторый создает несколько методов расширения Socket.

Все эти методы используют, TaskCompletionSource<T>чтобы обернуть асинхронные вызовы следующим образом:

    public static Task<Socket> AcceptAsync(this Socket socket)
    {
        if (socket == null)
            throw new ArgumentNullException("socket");

        var tcs = new TaskCompletionSource<Socket>();

        socket.BeginAccept(asyncResult =>
        {
            try
            {
                var s = asyncResult.AsyncState as Socket;
                var client = s.EndAccept(asyncResult);

                tcs.SetResult(client);
            }
            catch (Exception ex)
            {
                tcs.SetException(ex);
            }

        }, socket);

        return tcs.Task;
    }

Я передаю socketвBeginAccept методы, так что я получаю небольшое повышение производительности от компилятора, которому не нужно поднимать локальный параметр.

Тогда красота всего этого:

 var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
 listener.Bind(new IPEndPoint(IPAddress.Loopback, 2610));
 listener.Listen(10);

 var client = await listener.AcceptAsync();
Erik
источник
1
Почему бы Task.Factory.StartNew не работал здесь?
Тола Одежаи,
23
@Tola Так как это создало бы новую задачу, выполняющуюся в потоке потоков, но код выше использует поток завершения ввода / вывода, запущенный BeginAccept, iow: он не запускает новый поток.
Франс Боума,
4
Спасибо, @ Франс-Бума. Таким образом, TaskCompletionSource - это удобный способ преобразования кода, который использует операторы Begin ... End ... в задачу?
Тола Одежаи
3
@TolaOdejayi Немного поздний ответ, но да, это один из основных вариантов использования, которые я нашел для него. Это прекрасно работает для этого перехода кода.
Эрик
4
Посмотрите на TaskFactory <TResult> .FromAsync, чтобы обернуть Begin.. End...операторы.
MicBig
37

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

Хороший пример для этого, когда вы используете кеш. У вас может быть GetResourceAsyncметод, который ищет в кэше запрошенный ресурс и сразу же возвращает (не используя новый поток, используя TaskCompletionSource), если ресурс был найден. Только если ресурс не найден, мы хотели бы использовать новый поток и получить его с помощью Task.Run().

Пример кода можно посмотреть здесь: Как условно выполнить код асинхронно с помощью задач

Ади Лестер
источник
Я видел ваш вопрос, а также ответ. (посмотрите на мой комментарий к ответу) .... :-) и это действительно познавательный вопрос и ответ.
Рой Намир
11
Это на самом деле не та ситуация, в которой нужен TCS. Вы можете просто использовать Task.FromResultдля этого. Конечно, если вы используете 4.0 и у вас нет того, для Task.FromResultчего вы должны использовать TCS, это написать свой собственный FromResult .
Serv
@Servy Task.FromResultдоступен только с .NET 4.5. До этого это был способ добиться такого поведения.
Ади Лестер
@AdiLester Ваш ответ ссылается Task.Run, указывая, что это 4.5+. И мой предыдущий комментарий специально адресован .NET 4.0.
Serv
@Servy Не все, кто читает этот ответ, ориентированы на .NET 4.5+. Я считаю, что это хороший и правильный ответ, который помогает людям задавать вопрос ОП (который, кстати, помечен как .NET-4.0). В любом случае, мне кажется, что понижение голоса немного, но если вы действительно верите, что оно заслуживает понижения, тогда продолжайте.
Ади Лестер
25

В этом сообщении в блоге Леви Ботелхо описывает, как использовать TaskCompletionSourceдля написания асинхронной оболочки для Процесса, чтобы вы могли запустить его и дождаться его завершения.

public static Task RunProcessAsync(string processPath)
{
    var tcs = new TaskCompletionSource<object>();
    var process = new Process
    {
        EnableRaisingEvents = true,
        StartInfo = new ProcessStartInfo(processPath)
        {
            RedirectStandardError = true,
            UseShellExecute = false
        }
    };
    process.Exited += (sender, args) =>
    {
        if (process.ExitCode != 0)
        {
            var errorMessage = process.StandardError.ReadToEnd();
            tcs.SetException(new InvalidOperationException("The process did not exit correctly. " +
                "The corresponding error message was: " + errorMessage));
        }
        else
        {
            tcs.SetResult(null);
        }
        process.Dispose();
    };
    process.Start();
    return tcs.Task;
}

и его использование

await RunProcessAsync("myexecutable.exe");
Зарин
источник
14

Похоже, никто не упомянул, но я думаю, что юнит-тесты тоже можно считать реальной жизнью .

Я считаю TaskCompletionSourceполезным при насмешке зависимости с помощью асинхронного метода.

В реальной тестируемой программе:

public interface IEntityFacade
{
  Task<Entity> GetByIdAsync(string id);
}

В модульных тестах:

// set up mock dependency (here with NSubstitute)

TaskCompletionSource<Entity> queryTaskDriver = new TaskCompletionSource<Entity>();

IEntityFacade entityFacade = Substitute.For<IEntityFacade>();

entityFacade.GetByIdAsync(Arg.Any<string>()).Returns(queryTaskDriver.Task);

// later on, in the "Act" phase

private void When_Task_Completes_Successfully()
{
  queryTaskDriver.SetResult(someExpectedEntity);
  // ...
}

private void When_Task_Gives_Error()
{
  queryTaskDriver.SetException(someExpectedException);
  // ...
}

В конце концов, такое использование TaskCompletionSource кажется еще одним случаем «объекта Task, который не выполняет код».

superjos
источник
11

TaskCompletionSource используется для создания объектов Task, которые не выполняют код. В реальных сценариях TaskCompletionSource идеально подходит для операций ввода-вывода. Таким образом, вы получаете все преимущества задач (например, возвращаемые значения, продолжения и т. Д.), Не блокируя поток на время выполнения операции. Если ваша «функция» связана с операциями ввода-вывода, не рекомендуется блокировать поток с помощью новой задачи . Вместо этого, используя TaskCompletionSource , вы можете создать подчиненную задачу, чтобы просто указать, когда ваша связанная операция ввода-вывода завершается или происходит сбой.

v1p3r
источник
5

В этом посте приведен реальный пример с достойным объяснением из блога «Параллельное программирование с .NET» . Вы действительно должны прочитать это, но вот резюме в любом случае.

В сообщении блога показаны две реализации:

«фабричный метод для создания« отложенных »задач, которые на самом деле не будут запланированы, пока не произойдет какой-то заданный пользователем тайм-аут».

Первая показанная реализация основана на Task<>двух основных недостатках и имеет их. Во втором посте о внедрении они смягчаются с помощью TaskCompletionSource<>.

Вот эта вторая реализация:

public static Task StartNewDelayed(int millisecondsDelay, Action action)
{
    // Validate arguments
    if (millisecondsDelay < 0)
        throw new ArgumentOutOfRangeException("millisecondsDelay");
    if (action == null) throw new ArgumentNullException("action");

    // Create a trigger used to start the task
    var tcs = new TaskCompletionSource<object>();

    // Start a timer that will trigger it
    var timer = new Timer(
        _ => tcs.SetResult(null), null, millisecondsDelay, Timeout.Infinite);

    // Create and return a task that will be scheduled when the trigger fires.
    return tcs.Task.ContinueWith(_ =>
    {
        timer.Dispose();
        action();
    });
}
Urig
источник
было бы лучше использовать await на tcs.Task, а затем использовать action () после
Royi Namir
5
потому что вы вернулись в контекст, в котором вы оставили, где Continuewith не сохраняет контекст. (не по умолчанию) также, если следующий оператор в действии () вызывает исключение, было бы трудно отследить его, когда использование await покажет вас как обычное исключение.
Рой Намир
3
Почему бы просто await Task.Delay(millisecondsDelay); action(); return;или (в .Net 4.0)return Task.Delay(millisecondsDelay).ContinueWith( _ => action() );
sgnsajgon
@sgnsajgon, который будет, конечно, легче читать и поддерживать
JwJosefy
@JwJosefy На самом деле, метод Task.Delay может быть реализован с помощью TaskCompletionSource , аналогично приведенному выше коду. Реальная реализация здесь: Task.cs
sgnsajgon
4

Это может быть упрощением, но источник TaskCompletion позволяет ожидать события. Поскольку tcs.SetResult устанавливается только при возникновении события, вызывающая сторона может ожидать выполнения задачи.

Посмотрите это видео, чтобы узнать больше:

http://channel9.msdn.com/Series/Three-Essential-Tips-for-Async/Lucian03-TipsForAsyncThreadsAndDatabinding

nmishr
источник
1
Пожалуйста, разместите соответствующий код или документацию здесь, так как ссылки могут со временем меняться и делать этот ответ неактуальным.
rfornal
3

Я в реальном мире сценарий, где я использовал TaskCompletionSourceпри реализации очереди загрузки. В моем случае, если пользователь запускает 100 загрузок, я не хочу запускать их все сразу, и вместо того, чтобы возвращать объявленную задачу, я возвращаю задачу, к которой прикреплен TaskCompletionSource. После завершения загрузки поток, работающий в очереди, завершает задачу.

Ключевой концепцией здесь является то, что я отделяюсь, когда клиент запрашивает запуск задачи с момента ее фактического запуска. В этом случае, потому что я не хочу, чтобы клиенту приходилось иметь дело с управлением ресурсами.

обратите внимание, что вы можете использовать async / await в .net 4, если вы используете компилятор C # 5 (VS 2012+), смотрите здесь для получения дополнительной информации.

Yaur
источник
0

Я использовал TaskCompletionSourceдля запуска задачи, пока она не будет отменена. В этом случае это подписчик ServiceBus, который я обычно хочу запускать до тех пор, пока приложение работает.

public async Task RunUntilCancellation(
    CancellationToken cancellationToken,
    Func<Task> onCancel)
{
    var doneReceiving = new TaskCompletionSource<bool>();

    cancellationToken.Register(
        async () =>
        {
            await onCancel();
            doneReceiving.SetResult(true); // Signal to quit message listener
        });

    await doneReceiving.Task.ConfigureAwait(false); // Listen until quit signal is received.
}
Йохан Гов
источник
1
Нет необходимости использования «асинхронной» с «TaskCompletionSource» , как он уже создал задачу
Mandeep ДЖАНЖУА