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