Библиотека параллельных задач (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!