Страницы

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

пятница, 9 ноября 2018 г.

Синхронизация с использованием asynс/await

Есть такой код (взят у Шилдта и немного упрощен).
using System; using System.Threading; class ThreadTest { object lk = new object(); public void Tick() { lock (lk) { for (int x = 0; x < 12; x++) { Console.WriteLine("tick"); Thread.Sleep(200); Monitor.Pulse(lk); Monitor.Wait(lk); } Monitor.PulseAll(lk); } }
public void Tock() { lock (lk) { for (int x = 0; x < 12; x++) { Console.WriteLine("tock"); Thread.Sleep(200); Monitor.Pulse(lk); Monitor.Wait(lk); } Monitor.PulseAll(lk); } } } class demo { static void Main() { ThreadTest thrTest = new ThreadTest(); Thread t1 = new Thread(thrTest.Tick); t1.Start(); Thread t2 = new Thread(thrTest.Tock); t2.Start(); } }
Этот код в разных потоках запускает методы и синхронизируя их работу выводит на консоль Тик или Так. Но вот задумался, а как можно такой же функционал реализовать, но используя asynс/await ? И реально ли это ?


Ответ

Да, можно, хотя это немного сложнее.
Вот в этой статье расписана механика того, как сделать это самостоятельно. Но лучше не изобретать велосипед, а воспользоваться готовой библиотекой AsyncEx. С подключением этой библиотеки можно написать просто:
readonly AsyncLock mutex = new AsyncLock(); public async Task UseLockAsync() { using (await mutex.LockAsync()) { // тут вам принадлежит lock } }
(Оба автора — признанные специалисты в асинхронном программировании. Stephen Toub — один из разработчиков TPL в Microsoft, Stephen Cleary — автор хорошей книги Concurrency in C# Cookbook.)

По поводу конкретного кода с очерёдностью выполнения — такое можно сделать проще. Я использовал для синхронизации двух потоков AsyncBarrier из того же Nito.AsyncEx
class Program { static void Main(string[] args) { new Program().Run().Wait(); }
async Task Run() { pingBarrier = new AsyncBarrier(2); pongBarrier = new AsyncBarrier(2); var ping = Ping(); var pong = Pong(); await ping; await pong; }
AsyncBarrier pingBarrier, pongBarrier;
async Task Ping() { for (int i = 0; i < 12; i++) { await pingBarrier.SignalAndWaitAsync(); pingBarrier = new AsyncBarrier(2); Console.WriteLine($"ping #{i}, thread {Thread.CurrentThread.ManagedThreadId}"); await pongBarrier.SignalAndWaitAsync(); } }
async Task Pong() { for (int i = 0; i < 12; i++) { await pingBarrier.SignalAndWaitAsync(); await pongBarrier.SignalAndWaitAsync(); pongBarrier = new AsyncBarrier(2); Console.WriteLine($"pong #{i}, thread {Thread.CurrentThread.ManagedThreadId}"); } } }

Заметьте, что первоначальный пример Шилдта неправилен. Он использует Thread.Sleep для того, чтобы «гарантировать» наличие ожидающего потока, что является грубым просчётом. Из документации на Monitor.Pulse
The Monitor class does not maintain state indicating that the Pulse method has been called. Thus, if you call Pulse when no threads are waiting, the next thread that calls Wait blocks as if Pulse had never been called. If two threads are using Pulse and Wait to interact, this could result in a deadlock.
Впрочем, Шилдт знаменит неаккуратностью в своих книгах.

Уточнение: нет, пример Шилдта всё же правилен, Thread.Sleep в нём не играет решающей роли. Тем не менее, приведённая цитата из MSDN всё ещё в силе, и использование Wait/Pulse для синхронизации потоков опасно.

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

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