Библиотека параллельных задач (TPL) представляет собой набор открытых типов и API-интерфейсов в пространствах имен System.Threading и System.Threading.Tasks. Цель TPL — повышение производительности труда разработчиков за счет упрощения процедуры добавления параллелизма в приложения.
CancellationTokenSource (далее CTS) - объект для создания и управления токенами отмены (CancellationToken).
Обычный вариант использования выглядит так:
cts = new CancellationTokenSource(); var task = Task.Run(() => SomeWork(cts.Token), cts.Token); . . . . . cts.Cancel();
Я хочу рассказать о ситуации когда некоторый объект должен выполнять в единицу времени только одну задачу, например вывод результатов живого поиска, когда приходит запрос на вывод следующего результата предыдущий становится уже не актуальным. Простой пример решения такой задачи:
CancellationTokenSource сts = null; async void OnTextChanged(string text) { try { if (cts != null) cts.Cancel(); cts = new CancellationTokenSource(); var result = await DownloadSuggestionsAsync(text, cts.Token); ShowSuggestions(result); } catch(TaskCanceledException) { } }
Такой вариант хорошо работает при написании пользовательского интерфейса когда все выполняется в одном потоке и частота вызова OnTextChanged низкая. А что если метод аналогичный этому вызывается не на реакцию действия пользователя, а на результат какого либо сервиса, и вызовы могут быть из разных потоков. Самым просты решение это добавить lock. Но использование lock приведет к кратковременной блокировке вызывающих потоков и усложнению кода, мы же не хотим чтоб загрузка данных также попала под синхронизацию. Также хотелось бы иметь простой читабельный код.
Решение
Для решения этой проблемы хочу обратить внимание на unique_ptr из C++. Это умный указатель который получает единоличное владение объектом и разрушает его, когда unique_ptr выходит из области видимости или ему присваивается новый объект.
В результате сказанного выше получаем пару методов для работы с CTS:
public static CancellationTokenSource NewCts(ref CancellationTokenSource cts, params CancellationToken[] tokens) { var newCts = tokens.Length == 0 ? new CancellationTokenSource() : CancellationTokenSource.CreateLinkedTokenSource(tokens); var tmp = Interlocked.Exchange(ref cts, newCts); if (tmp != null) tmp.Cancel(); return newCts; } public static void DeleteCts(ref CancellationTokenSource cts) { var tmp = Interlocked.Exchange(ref cts, null); if (tmp != null) tmp.Cancel(); }
Метод Interlocked.Exchange - атомарная операция которая меняет значения двух переменных. Это позволяет дать гарантию что при одновременном вызове NewCts из разных потоков все созданные экземпляры будут в состоянии отмены кроме одного, последнего вызвавшего Interlocked.Exchange.
Для работы нужна переменная в которой хранится активный CTS (в примере поле класса _cts), она передается по ссылке. Важно! NewCts возвращает новый созданный им CTS объект в то время как переданная переменная (в примере _cts) будет содержать последний активный CTS. Они могут отличаться при одновременном вызове из разных потоков.
CancellationTokenSource _cts = null; async void OnTextChanged(string text) { try { var token = Utils.NewCts(ref _cts).Token; var result = await DownloadSuggestionsAsync(text, token); ShowSuggestions(result); } catch(TaskCanceledException) { } }
Метод NewCts также позволяет создавать связные CTS.
По окончанию работы или для принудительной отмены текущей задачи используется DeleteCts.
Достоинства:
- удобная запись;
- гарантия выполнения только одной операции в единицу времени при кратковременном спаме событий.
Недостатки:
- не подходит если события спамят постоянно, тогда результат мы просто не увидим.
Best wishes
And happy coding!