Могу ли я удалить разные элементы из списка / словаря одновременно без блокировки? - PullRequest
1 голос
/ 06 апреля 2020

Допустим, у меня есть следующий код:

class Test
{
    Dictionary<int, Task> tasks;

    void Run()
    {
        tasks.Add(1, DoStuff(1));
        tasks.Add(2, DoStuff(2));
        tasks.Add(3, DoStuff(3));
    }

    async Task DoStuff(int id)
    {
        try
        {
            // Do stuff that takes a lot of time and can end unsuccessfully.
        }
        finally
        {
            tasks.Remove(id);
        }
    }
}

Если я точно знаю, что задача может удалить только себя, и все идентификаторы будут другими (например, недавно созданный guid) - могу ли я оставить этот код как есть? Или мне нужна какая-то блокировка или использование ConcurrentDictionary здесь, потому что внутренние объекты обычного словаря или списка могут выйти из строя при одновременном доступе? (скажем, есть 10000 таких задач, и все они заканчиваются одновременно)

Кроме того: никто никогда не будет читать этот словарь / список или перебирать его. Он используется исключительно для хранения ссылок на фоновые задачи (поэтому G C не собирает их). Если у вас есть лучшее предложение для этого варианта использования, пожалуйста, предложения приветствуются:)

Ответы [ 2 ]

1 голос
/ 07 апреля 2020

Класс Dictionary не является поточно-ориентированным, поэтому вам обязательно нужно защитить его с помощью lock, если вы собираетесь манипулировать им несколькими потоками параллельно. В противном случае внутреннее состояние класса может быть повреждено.

Что касается производительности, это не должно быть проблемой. Удаление элемента из Dictionary выполняется очень быстро, поэтому, если вы удерживаете lock только на время выполнения этой операции, состязание за блокировку должно быть почти несуществующим. Вы должны начать беспокоиться, только если вы ожидаете частоту около 100 000 словарных операций в секунду или более. В этом случае было бы разумно переключиться на ConcurrentDictionary, который будет лучше справляться с растущим конфликтом из-за его реализации гранулярной блокировки.

Хотя в вашей реализации есть условие гонки. Task добавляется в словарь после его создания, поэтому теоретически возможно, что он попытается удалить себя еще до того, как он там появится. Если это произойдет, вы можете закончить с заданиями, которые останутся в словаре навсегда. Эту проблему нелегко решить элегантным способом.

1 голос
/ 06 апреля 2020

Здесь вам определенно нужен какой-то механизм блокировки. Лично я бы использовал ConcurrentDictionary для этого. Или, в зависимости от того, что вы пытаетесь выполнить sh здесь, вы можете просто сделать это:

var list = new List<Task>();

list.Add(DoStuff(1));
list.Add(DoStuff(2));
list.Add(DoStuff(3));

await Task.WhenAll(list);

Это обеспечит выполнение всех задач, прежде чем продолжить.

*** Редактировать - обновить

Если вы хотите, чтобы они постоянно удалялись из списка - в зависимости от того, как вы просматриваете их, вы можете просто сделать это вместо:

private List<Task> _list = new List<Task>();

...

_list.Add(DoStuff(1));
_list.Add(DoStuff(2));
_list.Add(DoStuff(3));

Тогда, когда вам нужно увидеть, сколько осталось, вы можете просто сделать это:

list.Count(a => !a.IsCompleted);

По сути, IsCompleted имеет значение true, только если этот поток завершен до завершения. Однако вам также может понадобиться проверить IsFapted или IsCanceled, в зависимости от того, что делают эти задачи.

*** Редактировать обновление 2

В потоке, который добавляет в этот список, вы затем можете удалить их, не беспокоясь:

_list.Add(DoStuff(4));
_list.Add(DoStuff(5));

_list = _list.Where(a => !a.IsCompleted).ToList();

Если вы не хотите связываться с фактической ссылкой на переменную, вы можете сделать задний ход для l oop и удалить вручную, пока другой Тема не добавляется в этот список.

...