Коли слід використовувати TaskCompletionSource <T>?


199

Все, що він знає, AFAIK - це те, що в якийсь момент його SetResultабо SetExceptionметод викликають для завершення Task<T>викритого через його Taskвластивість.

Іншими словами, він виступає як виробник для Task<TResult>та його завершення.

Я бачив тут приклад:

Якщо мені потрібен спосіб виконати функцію асинхронно і мати завдання для представлення цієї операції.

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 за допомогою завдання без створення нових тем.
Арвіс

Відповіді:


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 .


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

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();

1
Чому б Task.Factory.StartNew не працював тут?
Tola Odejayi

23
@Tola Як би це створило нове завдання, що працює на потоці нитки пулу, але наведений вище код використовує потік завершення вводу / виводу, запущений BeginAccept, iow: він не запускає новий потік.
Frans Bouma

4
Спасибі, @ Frans-Bouma. Отже TaskCompletionSource - це зручний спосіб перетворення коду, який використовує операції Початок ... Кінець ... у завдання?
Tola Odejayi

3
@TolaOdejayi Біт пізньої відповіді, але так, це один з основних випадків використання, які я знайшов для цього. Це чудово працює для цього переходу коду.
Ерік

4
Подивіться на TaskFactory <TResult> .FromAsync, щоб завершити операціїBegin.. End... .
Мікбіг

37

Для мене класичним сценарієм використання TaskCompletionSourceє те, коли можливо, що моєму методу не обов’язково доведеться робити багато часу. Що дозволяє нам робити - це вибрати конкретні випадки, коли ми хотіли б використовувати новий потік.

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

Приклад коду можна побачити тут: Як умовно запустити код асинхронно за допомогою завдань


Я бачив ваше запитання, а також відповідь. (подивіться на мій коментар до відповіді) .... :-) і справді це навчальне питання та відповідь.
Рой Намір

11
Це насправді не ситуація, коли потрібен ТКС. Ви можете просто використовувати Task.FromResultдля цього. Звичайно, якщо ви використовуєте 4.0 і не маєте для Task.FromResultтого, для чого використовували б TCS, - це написати свій власний FromResult .
Сервіс

@Servy Task.FromResultдоступний лише з .NET 4.5. До цього таким способом було досягти такої поведінки.
Аді Лестер

@AdiLester Ви відповідаєте на посилання Task.Run, вказуючи, що це 4,5+. І мій попередній коментар спеціально стосувався .NET 4.0.
Сервіс

@ Сервіс Не всі, хто читає цю відповідь, націлені на .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, який не виконує код".


11

TaskCompletionSource використовується для створення об’єктів Task , які не виконують код. У сценаріях реального світу TaskCompletionSource ідеально підходить для операцій, пов'язаних з введенням / виведенням. Таким чином, ви отримуєте всі переваги завдань (наприклад, значення повернення, продовження тощо), не блокуючи нитку протягом тривалості операції. Якщо ваша "функція" - операція, пов'язана з входом / виводом, не рекомендується блокувати потік за допомогою нового завдання . Натомість, використовуючи TaskCompletionSource , ви можете створити ведене завдання, щоб просто вказати, коли ваша операція, пов'язана з входом / виводом , закінчується або не працює.


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();
    });
}

було б краще скористатись функцією wait на tcs.Task, а потім скористатися action () після
Royi Namir

5
тому що ви повернулися до контексту, куди ви поїхали, де Continuewith не зберігає контекст. (не за замовчуванням) також, якщо наступне твердження в дії () спричиняє виняток, важко буде його зафіксувати там, де використання await покаже вам як звичайний виняток.
Royi Namir

3
Чому б не просто await Task.Delay(millisecondsDelay); action(); return;або (in .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


1
Будь ласка, розмістіть тут відповідний код або документацію, оскільки посилання можуть змінюватися з часом і зробити цю відповідь нерелевантною.
rfornal

3

Я справжній сценарій, де я використовувався, TaskCompletionSourceце коли реалізувати чергу завантаження. У моєму випадку, якщо користувач запускає 100 завантажень, я не хочу запускати їх усі одразу, і тому замість повернення страйкованого завдання я повертаю завдання, додане до нього TaskCompletionSource. Після завершення завантаження нитка, яка працює в черзі, виконує завдання.

Ключова концепція тут полягає в тому, що я розв'язую, коли клієнт просить розпочати завдання з того моменту, коли він насправді починається. У цьому випадку тому, що я не хочу, щоб клієнт мав справу з управлінням ресурсами.

зауважте, що ви можете використовувати async / wait в .net 4 до тих пір, поки ви використовуєте компілятор C # 5 (VS 2012+), див. тут для отримання більш детальної інформації.


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
Не потрібно використовувати "async" разом із "TaskCompletionSource", оскільки це вже створило завдання
Mandeep Janjua

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.