Как сделать удаление ManualResetEventSlim из ConcurentDictionary

Как сделать грамотное удаление ManualResetEventSlim из ConcurentDictionary? Как гарантировать, что ManualResetEventSlim не кем не используется? Перед удалением мне нужно будет ещё вызвать Dispose

public class Worker
{
    private readonly ConcurrentDictionary<string, ManualResetEventSlim> _etpIdToReadyEventSlim = new();
    private readonly DefaultRtsMarketWaiterSettings _settings = new();
    private readonly object _syncRoot = new();

    public Task WaitProcedureAsync(Procedure procedure,
        CancellationToken cancellationToken = default)
    {
        ManualResetEventSlim readyEventSlim;
        
        lock (_syncRoot)
        { 
            readyEventSlim = _etpIdToReadyEventSlim.GetOrAdd(procedure.EtpId, 
                (_) => new ManualResetEventSlim());
        }

        Task.Run(() => DoWaitAsync(procedure.EtpId, readyEventSlim, cancellationToken), cancellationToken);

        readyEventSlim.Wait(cancellationToken);

        //Здесь следует сделать удаление

        return Task.CompletedTask;
    }

    private async Task DoWaitAsync(string etpId,
        ManualResetEventSlim readyEventSlim,
        CancellationToken cancellationToken = default)
    {
        Guid id = Guid.NewGuid();
        
        DateTime startsAtUtc = DateTime.UtcNow;

        try
        {
            while (DateTime.UtcNow - startsAtUtc < _settings.MaxWaitingTimeSpan)
            {
                try
                {
                    cancellationToken.ThrowIfCancellationRequested();

                    Console.WriteLine($"Id работы: {id}");

                    await Task.Delay(_settings.BetweenChecksDelay, cancellationToken);
                }
                catch (Exception e)
                {
                    //Ignore
                }
            }
        }
        finally
        {
            readyEventSlim.Set();
        }
    }
}


public class Procedure
{
    public string EtpId { get; set; }
}

public class DefaultRtsMarketWaiterSettings
{
    public TimeSpan MaxWaitingTimeSpan { get; set; } = TimeSpan.FromMinutes(1);

    public TimeSpan BetweenChecksDelay { get; set; } = TimeSpan.FromSeconds(5);
}

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

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

Проще всего просто не вызывать Dispose у ManualResetEventSlim.

Этот метод делает две вещи:

  1. освобождает WaitHandle если он был создан;
  2. запрещает дальнейшую работу с объектом.

Если вы замените все вызовы readyEventSlim.Wait на readyEventSlim.WaitAsync - WaitHandle никогда создан не будет, а значит и освобождать его будет необязательно.


Однако самое правильное решение тут - вовсе не использовать этот класс: для синхронизации вы можете использовать непосредственно объект Task.

Для начала, в текущем виде вы можете просто избавиться от ConcurrentDictionary и писать return Task.Run(() => DoWaitAsync(...);, поскольку ваш ManualResetEventSlim ничего не делает кроме создания паразитных связей.

Поэтому я предположу, что ваша задача - избежать запуска нескольких одновременных циклов. В таком случае можно воспользоваться подсчётом ссылок. Начнём со вспомогательного класса:

    private class Worker
    {
        public readonly CancellationTokenSource cts = new();
        public Task waitTask = null!;
        public int refCount;
    }

    private readonly ConcurrentDictionary<string, Worker> workers = new();

Дальше всё просто, надо лишь аккуратно обработать все возможные случаи:

    public async Task WaitFor(string etpId, CancellationToken token)
    {
    again:
        var worker = workers.GetOrAdd(etpId, new Worker());

        Task waitTask;

        lock (worker)
        {
            if (worker.refCount < 0)
            {
                // Редкий случай, когда мы попали точно на момент остановки цикла.
                // Сейчас воркера уже нет в словаре, надо лишь попробовать ещё раз.
                goto again;
            }

            if (worker.refCount++ == 0)
            {
                // Это первое обращение к воркеру, запускаем цикл ожидания
                worker.waitTask = Task.Run(() => DoWaitAsync(etpId, worker, worker.cts.Token));
            }

            waitTask = worker.waitTask;
        }

        try
        {
            await waitTask.WaitAsync(token);
        }
        finally
        {
            lock(worker)
            {
                if (--worker.refCount == 0)
                {
                    // Это было последнее обращение к воркеру, останавливаем цикл ожидания и удаляем воркера из словаря
                    worker.refCount = -1;
                    worker.cts.Cancel();

                    workers.TryRemove(new(etpId, worker));

                    // прямо сейчас метод DoWaitAsync может ещё выполняться, но по завершению waitTask
                    // на воркера не останется никаких ссылок, самое время освободить ресурсы
                    worker.waitTask.ContinueWith(_ => worker.cts.Dispose(), TaskContinuationOptions.ExecuteSynchronously);
                }
            }
        }
    }
→ Ссылка