Если вы хотите разрешить одновременное выполнение нескольких задач с одним и тем же taskId
, но с другим типом данных, вы можете переместить универсальный тип <T>
из метода в класс.Таким образом, каждый тип <T>
имеет свой собственный словарь.Это также позволяет вам хранить словарь Task<T>
и избегать приведения типов.
Вы сказали, что у вас будет только один экземпляр класса TaskManager
, но если вы можете изменить его на наличие толькоотдельный экземпляр каждого класса, который вызывает TaskManager
, вы можете избежать необходимости в словаре (и, следовательно, хэшировать имя задачи), так как каждый класс имеет экземпляр
public class TaskManager<T>
{
private Task<T> _currentTask;
private object _lock = new object();
public Task<T> ExecuteOnceAsync(string taskId, Func<Task<T>> taskFactory)
{
if (_currentTask == null)
{
lock (_lock)
{
if (_currentTask == null)
{
Task<T> concreteTask = taskFactory();
concreteTask.ContinueWith(RemoveTask);
_currentTask = concreteTask;
}
}
}
return _currentTask;
}
private void RemoveTask()
{
_currentTask = null;
}
}
Если вы хотите быть супер-корректным,тогда вы можете использовать Interlocked.Exchange
вместо присвоения значений непосредственно _currentTask
, что более важно в анонимной функции, передаваемой в ContinueWith
, так как она работает вне блокировки.Возможно, вы захотите использовать какой-нибудь метод Interlocked
, чтобы проверить, является ли значение нулевым или нет.Кстати, блокировка не нужна, если вы знаете, что вызовы ExecuteOnceAsync не будут выполняться параллельно, например, если она вызывается обработчиком события нажатия кнопки в приложении WPF или WinForms (даже если пользователь нажимает несколькораз, каждый должен происходить последовательно).Веб-приложение не может дать такие гарантии, хотя.
Вы можете видеть, что я делаю проверку if (_currentTask == null)
дважды, один раз внутри замка и один раз за пределами замка, что позволяет в случае уже существующей задачи избежать попадания в ловушку необходимости получить замок.Я не верю, что вы можете использовать тот же трюк с кодом в вашем вопросе, потому что если RemoveTask
вызывается одновременно с TryGetValue
, вы можете попасть в плохое состояние.Вы можете переключиться на использование ConcurrentDictionary, и это, вероятно, позволит вам избежать использования блокировки внутри RemoveTask
и блокировать ExecuteOnceAsync
только тогда, когда TryGetValue
возвращает false.Но нужно ли это измерять для повышения производительности или нет.
Другая проблема как с вашим кодом, так и с моим, заключается в том, что когда taskFactory
медленный, он долго удерживает блокировку (помните, когда вызывается асинхронный вызов).метод выполняется синхронно до тех пор, пока первое ожидание, когда задача, которую он ожидает, еще не завершено ).Это может быть уменьшено с помощью Task<T> concreteTask = Task.Run(()=> taskFactory());
, но Task.Run
имеет свои собственные издержки, поэтому, если taskFactory
действительно очень быстро набирает await
, вероятно, лучше сохранить производительность, как есть.Опять же, если перфоманс имеет значение, вам нужно измерить.
И хотя я ни в коем случае не эксперт по перфекту, в зависимости от ваших перфектных требований мое предложение имеет несколько улучшений:
- Хотя
Dictionary
поисков - это O (1), равно как и Thread.Sleep(1000)
, другими словами, он может быть постоянно медленнее, чем альтернатива, даже если альтернативой является O (n) (для малых значений n).Поскольку ваш ключ словаря является строкой, string.GetHashCode
- это O (n) в зависимости от длины строки, и словарь perf может быть поврежден коллизиями хешей.Мое предложение имеет только один объект задачи, так что это также O (1), и он не требует хеширования чего-либо или поиска в массиве, или, тем не менее, Dictionary
. - Это супер минор, но так как мойRemoveTask не нужно захватывать какие-либо переменные, я могу передать его непосредственно в
ContinueWith
как делегат (так как типы совпадают), тогда как вы используете анонимную функцию, которая означает, что компилятор должен создать анонимный класс для захвата переменной taskId,и создайте экземпляр этого класса в куче при каждом запуске кода, что увеличивает нагрузку на память.Это само по себе не окажет значительного перфоментного эффекта (если только не будет выполняться в очень узком цикле), но оно может способствовать «смерти от 1000 сокращений», если у вас есть много других битов кода, которые создают небольшие объекты в куче. - Как уже упоминалось выше, мой код не блокируется, когда задача уже существует.
- Ваш код эффективно использует глобальную блокировку приложения. Это самый важный убийца перфектов .Если ваш
TaskManager
используется каждый раз, когда делается запрос к базе данных, ваша реализация может запускать только один запрос к базе данных за раз, даже если две задачи с разными taskId
никогда не смогут вернуть одну и ту же задачу.И RemoveTask
, независимо от того, какая задача завершена, блокирует все вызовы на ExecuteOnceAsync
.Используя другой экземпляр TaskManager
для каждого типа запроса, вы можете избежать GetProductsAsync
блокирования GetCustomerInformation
.
Написав все это, если ваша цель - не использовать один TaskManager
для каждого приложения, но вместо этого используйте один TaskManager
для каждого метода запроса к базе данных, что означает, что taskId
на самом деле представляет параметры запроса GetProductsAsync
, тогда вам следует игнорировать все, что я написал :) Ну, почти все.Вы по-прежнему можете перемещать <T>
в определения классов и словарей и избегать выполнения типов в своем методе.