Как написать свой аналог Task.Delay?
Я занимаюсь изучением асинхронного кода, идет очень туго. Я хочу написать свой собственный Task.Delay, потому что я не понимаю, как он устроен. Предположительно, у меня должен быть генератор (или что-то в таком духе), обозначающий, закончено ли ожидание, или пока рано.
Ответы (1 шт):
Написать свой Task.Delay можно, но сразу предупреждаю: при большом числе потоков то, что я напишу ниже, будет работать не так быстро как хотелось бы; в реальном коде есть ещё куча тонких оптимизаций.
Начнём с того, что потребуется контейнер для всех запланированных задач и объект для синхронизации:
private static PriorityQueue<TaskCompletionSource, long> queue = new();
private static object _lock = new();
Здесь используется приоритетная очередь на основе пирамиды (кучи, heap), которая упорядочит все задачи в порядке их выполнения. В реальном коде используется более сложная структура данных, которая всё ещё остаётся приоритетной очередью.
Теперь можно представить как будет выглядеть аналог метода Task.Delay:
public static async Task MyDelay(TimeSpan delay, CancellationToken token = default)
=> MyDelay((long)delay.TotalMilliseconds, token);
public static async Task MyDelay(int delay, CancellationToken token = default)
=> MyDelay((long)delay, token);
private static async Task MyDelay(long delay, CancellationToken token) {
var task = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var wakeupTime = Environment.TickCount64 + delay;
lock(_lock) {
queue.Enqueue(task, wakeupTime);
// Если новая задача оказалась самой приоритетной - уведомим ожидающий поток
if (queue.Peek() == task) Monitor.Pulse(_lock);
}
// тут бы удалить задачу из очереди, да вот PriorityQueue не поддерживает удаление,
// поэтому просто отменяем задачу при отмене токена
using (token.Register(() => task.TrySetCanceled())) {
return await task.Task;
}
}
Вроде все задачи сложены в очередь. Осталось создать поток который бы её "разгребал":
static MyTask() {
new Thread(ProcessQueue) {
Name = "MyTask.MyDelay process thread",
IsBackground = true,
}.Start();
}
const int MaxDelay = int.MaxValue / 2; // есть один баг...
private static void ProcessQueue() {
while(true) {
TaskCompletionSource task;
lock(_lock) {
if (!queue.TryPeek(out task, out wakeupTime)) {
// Очередь пуста, ждём новых задач
Monitor.Wait(_lock);
continue;
}
else {
var remaining = wakeupTime - Environment.TickCount64;
if (remaining > MaxDelay)
remaining = MaxDelay;
if (remaining > 0) {
// Очередь не пуста, но время очередной задачи ещё не наступило, надо подождать
Monitor.Wait(_lock, TimeSpan.FromMilliseconds(remaining));
continue;
}
}
// Очередь не пуста, и время очередной задачи наступило
queue.Dequeue();
}
task.TrySetResult();
}
}