Возникла сегодня идея написать асинхронную версию ManualResetEvent, которая в задаче будет «ожидать» через await и при этом не занимать никакой поток.
В теории все просто, для стейтмашины нужен объект, который имеет метод GetAwaiter, который вернет awaiter, в котором реализован INotifyCompletion с OnCompleted, поле IsCompleted и метод GetResult. Начнем с awaiter:
public class ManualResetEventAwaiter : INotifyCompletion
{
Action _continuation;
public void OnCompleted(Action continuation)
{
Volatile.Write(ref _continuation, continuation);
}
public bool GetResult()
{
return true;
}
public bool IsCompleted { get; private set; } = false;
public void Set()
{
IsCompleted = true;
Action continuation = Interlocked.Exchange(ref _continuation, null);
Task.Run(() =>
{
continuation?.Invoke();
});
}
}
В
OnCompleted стейтмашина передаст действие которое продолжит нашу логику выполнения. Для нас это воспринимается как логика после await.
В
Set будет вызываться это продолжение работы. Я обернул его в Task для того что бы «ожидающие таски»(это условность) так же продолжились асинхронно. Хотя, возможно, continuation(там у нас IAsyncStateMachine.MoveNext) итак это сделает, но я до конца не уверен. Если кто в курсе напишите.
Объект с GetAwaiter:
public class ManualResetEventAwaitable
{
ManualResetEventAwaiter _manualResetEventAwaiter;
public ManualResetEventAwaitable()
{
_manualResetEventAwaiter = new ManualResetEventAwaiter();
}
public ManualResetEventAwaiter GetAwaiter()
{
return _manualResetEventAwaiter;
}
public void Set()
{
_manualResetEventAwaiter.Set();
}
}
И сам
ManualResetEventAsync:
public class ManualResetEventAsync
{
ConcurrentQueue<ManualResetEventAwaitable> awaiteQueue
= new ConcurrentQueue<ManualResetEventAwaitable>();
public bool State { get; private set; }
public ManualResetEventAsync(bool initialState)
{
State = initialState;
}
public ManualResetEventAwaitable WaitOneAsync()
{
var awaitable = new ManualResetEventAwaitable();
if (State)
awaitable.Set();
else
awaiteQueue.Enqueue(awaitable);
return awaitable;
}
public void Set()
{
lock (awaiteQueue)
{
while (awaiteQueue.TryDequeue(out var awaitable))
awaitable.Set();
State = true;
}
}
public void Reset()
{
State = false;
}
}
Метод
WaitOneAsync будет использоваться с await.
Set вызовет все продолжения и установит сигнальное состояние.
Reset установит не сигнальное состояние.
И свойство
State позволит узнать текущее состояние. Его можно использовать в условии до WaitOneAsync:
if (!mre.State)
await mre.WaitOneAsync();
Но и без условия работа не нарушится.
В оригинале метод WaitOne имеет 5 перегрузок, но тут я ограничусь пока одним методом.
Ссылка на проект
комментарии (24)