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

Рейтинг: 0Ответов: 1Опубликовано: 04.05.2023

Как сделать грамотное удаление 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);
}

Ответы

▲ 0

Проще всего просто не вызывать 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);
                }
            }
        }
    }