Как написать свой аналог Task.Delay?

Я занимаюсь изучением асинхронного кода, идет очень туго. Я хочу написать свой собственный Task.Delay, потому что я не понимаю, как он устроен. Предположительно, у меня должен быть генератор (или что-то в таком духе), обозначающий, закончено ли ожидание, или пока рано.


Ответы (1 шт):

Автор решения: Pavel Mayorov

Написать свой 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();
    }
}
→ Ссылка