Страницы

Поиск по вопросам

суббота, 30 ноября 2019 г.

В чем смысл TaskCompletionSource и когда его лучше использовать?

#c_sharp #net #асинхронность #tpl


Немного не понял смысла класса TaskCompletionSource.
В некоторых источниках пишут, что лучше его возвращать из метода вместо обычного
Task.Run().

Разве есть какой-то смысл? Что так, что так я смогу вызвать await на вызывающей стороне.
    


Ответы

Ответ 1



TaskCompletionSource — это тот самый крайний случай, когда вы не можете создать «базовый» Task стандартными средствами. Давайте я поясню, что я имею в виду. Если вы создаёте Task, обычных путей для этого два. Во-первых, если ваш код не производит ожидания, а активно работает, например, проводит вычисления (CPU-bound), вы отправляете его на пул потоков при помощи Task.Run или его аналогов. Во-вторых, если вы пользуетесь другими асинхронными операциями, вы создаёте async-метод, в котором производите await на другие асинхронные операции. .NET предоставляет множество готовых асинхронных операций, например, NetworkStream.ReadAsync или там Dispatcher.InvokeAsync. Но что делать, если вам нужно самому создать примитивную асинхронную операцию, которая не выражается в терминах других, уже готовых асинхронных операций? Как созданы самые внутренние Task-методы? В этом месте вам как раз и пригодится TaskCompletionSource. Например, вы хотите асинхронно дождаться события. Для этого вам нужно превратить событие в Task. Это делается как-то так: мы подписываемся на событие, и по его приходу завершаем Task. Task WaitInput() { var tcs = new TaskCompletionSource(); source.InputReceived += (o, args) => tcs.SetResult(args.Input); return tcs.Task; } Более строгий вариант с отпиской, в которой TaskCompletionSource используется как внутренний Task, чтобы успеть отписаться после его окончания: async Task WaitInput() { var tcs = new TaskCompletionSource(); SourceInputHandler handler = (o, args) => tcs.SetResult(args.Input); source.InputReceived += handler; try { return await tcs.Task; } finally { source.InputReceived -= handler; } } Ещё один пример из реального кода: запустить и дождаться окончания процесса: Task ExecuteProcess(string path) { var p = new Process() { EnableRaisingEvents = true, StartInfo = { FileName = path } }; var tcs = new TaskCompletionSource(); p.Exited += (sender, args) => { tcs.SetResult(true); p.Dispose(); }; // запуск выгружаем на пул потоков, потому что он медленный Task.Run(() => p.Start()); return tcs.Task; } Ещё один пример взят из класса DispatcherThread. Нам нужно дождаться, пока поток стартует, и придёт в «рабочее» состояние. Обычно для этого используют AutoResetEvent, но блокироваться в ожидании его неохота, и намного проще использовать TaskCompletionSource: static public Task CreateAsync() { var waitCompletionSource = new TaskCompletionSource(); var thread = new Thread(() => { // тут могут быть любые настройки waitCompletionSource.SetResult(new DispatcherThread()); Dispatcher.Run(); }); thread.SetApartmentState(ApartmentState.STA); thread.Start(); return waitCompletionSource.Task; } Мы видим, что таким образом можно превратить в Task по сути любую операцию. Дополнительное чтение по теме: TPL and Traditional .NET Framework Asynchronous Programming. Резюме: TaskCompletionSource позволяет превратить в Task даже ту асинхронную операцию, которая не даёт await-абельного интерфейса.

Ответ 2



На SO был такой вопрос. Почитай здесь. Перевод одного из лучших ответов: В моих опытах TaskCompletionSource отлично подходит для переноса старых асинхронных шаблонов в современный шаблон async/await. Самый полезный пример, о котором я могу думать, - это работать с Socket. Он имеет старые шаблоны APM и EAP, но не методы awaitable Task, которые имеют TcpListener и TcpClient. У меня лично есть несколько проблем с классом NetworkStream и предпочитаю raw Socket. Будучи тем, что мне также нравится шаблон async/await, я создал класс расширения SocketExtender, который создает несколько методов расширения для Socket. Все эти методы используют TaskCompletionSource для обертывания асинхронных вызовов следующим образом: public static Task AcceptAsync(this Socket socket) { if (socket == null) throw new ArgumentNullException("socket"); var tcs = new TaskCompletionSource(); 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();

Комментариев нет:

Отправить комментарий