Отмена
В .NET Framework 4 введена новая универсальная модель совместной отмены асинхронных или долго выполняющихся синхронных операций. Эта модель построена на простом объекте, называемом токеном отмены. Объект, который вызывает отменяемую операцию, например путем создания нового потока или задачи, передает этот токен в операцию. Операция, в свою очередь, передает копии этого токена в другие операции. Некоторое время спустя объект, создавший токен, может использовать его для запроса остановки выполнения операции. Запрос на отмену может создавать только запрашивающий объект, и каждый прослушиватель должен обнаружить этот запрос и своевременно ответить на него. На следующем рисунке показана связь между источником токена и всеми копиями его токена.
Эта новая модель отмены упрощает создание приложений и библиотек, поддерживающих отмену. Она также поддерживает следующие возможности:
Отмена является совместной и не навязывается прослушивателю в принудительном порядке. Прослушиватель сам определяет порядок корректного завершения в ответ на запрос отмены.
Процесс запрашивания отличен от прослушивания. Объект, который вызывает отменяемую операцию, может управлять временем создания запроса отмены (а также самим фактом создания подобного запроса).
Запрашивающий объект создает запрос на отмену для всех копий токена, используя только один вызов метода.
Прослушиватель может одновременно прослушивать несколько токенов, объединяя их в один связанный токен.
Пользовательский код может отслеживать запросы отмены из кода библиотеки и реагировать на них, а код библиотеки, в свою очередь, может отслеживать запросы отмены из пользовательского кода и реагировать на них.
Прослушиватели могут обнаруживать запросы отмены путем выполнения опросов, регистрации обратных вызовов или ожидания дескрипторов ожидания.
Новые типы отмены
Новая инфраструктура отмены реализована в виде набора связанных типов, приведенных в следующей таблице.
Имя типа |
Описание |
---|---|
Объект, который создает токен отмены и запрос на отмену для всех копий этого токена. |
|
Простой тип значения передается в один или несколько прослушивателей, обычно в виде параметра метода. Прослушиватели отслеживают значение свойства IsCancellationRequested токена с помощью опросов, обратных вызовов или дескрипторов ожидания. |
|
Новые перегрузки этого исключения принимают токен отмены (CancellationToken) в качестве входного параметра. Прослушиватели могут также создавать это исключение для проверки источника отмены и уведомления остальных о выполненном ответе на запрос отмены. |
Новая модель отмены интегрирована в .NET Framework в нескольких типах. Наиболее важными являются System.Threading.Tasks.Parallel, System.Threading.Tasks.Task, System.Threading.Tasks.Task<TResult> и System.Linq.ParallelEnumerable. Рекомендуется использовать эту новую модель отмены во всем новом коде библиотек и приложений.
Пример кода
В следующем примере запрашивающий объект создает объект CancellationTokenSource, а затем передает его свойство Token в отменяемую операцию. Операция, получающая запрос, отслеживает значение свойства IsCancellationRequested токена с помощью опроса. Когда свойство принимает значение true, прослушиватель может завершить операцию соответствующим способом. В данном примере метод просто выполняет выход, что и требуется в большинстве случаев.
Примечание |
---|
В этом примере метод QueueUserWorkItem используется для демонстрации того, что новая инфраструктура отмены совместима с интерфейсами API прежних версий.Пример, в котором используется новый, предпочтительный тип System.Threading.Tasks.Task, расположен в разделе Практическое руководство. Отмена задачи и ее дочерних элементов. |
Shared Sub CancelWithThreadPoolMiniSnippet()
'Thread 1: The Requestor
' Create the token source.
Dim cts As New CancellationTokenSource()
' Pass the token to the cancelable operation.
ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf DoSomeWork), cts.Token)
' Request cancellation by setting a flag on the token.
cts.Cancel()
' end block
End Sub
'Thread 2: The Listener
Shared Sub DoSomeWork(ByVal obj As Object)
Dim token As CancellationToken = CType(obj, CancellationToken)
For i As Integer = 0 To 1000000
' Simulating work.
Thread.SpinWait(5000000)
If token.IsCancellationRequested Then
' Perform cleanup if necessary.
'...
' Terminate the operation.
Exit For
End If
Next
End Sub
static void CancelWithThreadPoolMiniSnippet()
{
//Thread 1: The Requestor
// Create the token source.
CancellationTokenSource cts = new CancellationTokenSource();
// Pass the token to the cancelable operation.
ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
// Request cancellation by setting a flag on the token.
cts.Cancel();
}
//Thread 2: The Listener
static void DoSomeWork(object obj)
{
CancellationToken token = (CancellationToken)obj;
for (int i = 0; i < 100000; i++)
{
// Simulating work.
Thread.SpinWait(5000000);
if (token.IsCancellationRequested)
{
// Perform cleanup if necessary.
//...
// Terminate the operation.
break;
}
}
}
Отмена операции и отмена объекта
В новой инфраструктуре отмены отменяются операции, а не объекты. Запрос отмены означает, что операция должна быть остановлена как можно быстрее после выполнения всех необходимых очисток. Один токен отмены должен относиться к одной отменяемой операции, однако эта операция может быть реализована в программе. После того как свойство IsCancellationRequested токена примет значение true, для него невозможно будет восстановить значение false. Таким образом, токены отмены невозможно использовать повторно после выполнения отмены.
Если требуется механизм отмены объекта, его можно построить на основе механизма отмены операции, как показано в следующем примере.
Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token
' User defined Class with its own method for cancellation
Dim obj1 As New MyCancelableObject()
Dim obj2 As New MyCancelableObject()
Dim obj3 As New MyCancelableObject()
' Register the object's cancel method with the token's
' cancellation request.
token.Register(Sub() obj1.Cancel())
token.Register(Sub() obj2.Cancel())
token.Register(Sub() obj3.Cancel())
' Request cancellation on the token.
cts.Cancel()
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// User defined Class with its own method for cancellation
var obj1 = new MyCancelableObject();
var obj2 = new MyCancelableObject();
var obj3 = new MyCancelableObject();
// Register the object's cancel method with the token's
// cancellation request.
token.Register(() => obj1.Cancel());
token.Register(() => obj2.Cancel());
token.Register(() => obj3.Cancel());
// Request cancellation on the token.
cts.Cancel();
Если объект поддерживает несколько параллельных отменяемых операций, в каждую отменяемую операцию следует передавать отдельный токен. Таким способом одну операцию можно отменить без воздействия на другие операции.
Прослушивание запросов отмены и ответ на них
Объект, реализующий отменяемую операцию, в пользовательском делегате определяет способ завершения операции в ответ на запрос отмены. В большинстве случаев пользовательский делегат может выполнить необходимую очистку, а затем немедленный возврат.
Однако в более сложных случаях может потребоваться, чтобы пользовательский делегат уведомлял код библиотеки об отмене. В таких случаях, чтобы правильно завершить операцию, следует вызвать объект ThrowIfCancellationRequested() делегатом, что приведет к созданию исключения OperationCanceledException. Новые перегрузки этого исключения в .NET Framework 4 принимают объект CancellationToken в качестве аргумента. Код библиотеки может перехватывать это исключение в потоке пользовательского делегата и проверять токен исключения, чтобы определить, указывает ли это исключение на совместную отмену или на какие-либо другие исключительные ситуации.
Класс Task обрабатывает исключение OperationCanceledException таким способом. Дополнительные сведения см. в разделе Отмена задач.
Прослушивание с помощью опросов
Для длительных циклических или рекурсивных вычислений можно ожидать передачи запроса отмены путем периодического выполнения опроса значения свойства CancellationToken.IsCancellationRequested. Если свойство примет значение true, то метод должен выполнить очистку и завершение как можно быстрее. Оптимальная частота выполнения опроса зависит от типа приложения. Разработчик должен определить оптимальную частоту выполнения опросов для конкретной программы. Выполнение опроса само по себе не оказывает существенного влияния на производительность. В следующем примере показан один из возможных способов выполнения опроса.
Shared Sub NestedLoops(ByVal rect As Rectangle, ByVal token As CancellationToken)
For x As Integer = 0 To rect.columns
For y As Integer = 0 To rect.rows
' Simulating work.
Thread.SpinWait(5000)
Console.Write("0' end block,1' end block ", x, y)
Next
' Assume that we know that the inner loop is very fast.
' Therefore, checking once per row is sufficient.
If token.IsCancellationRequested = True Then
' Cleanup or undo here if necessary...
Console.WriteLine("\r\nCancelling after row 0' end block.", x)
Console.WriteLine("Press any key to exit.")
' then...
Exit For
' ...or, if using Task:
' token.ThrowIfCancellationRequested()
End If
Next
End Sub
static void NestedLoops(Rectangle rect, CancellationToken token)
{
for (int x = 0; x < rect.columns && !token.IsCancellationRequested; x++)
{
for (int y = 0; y < rect.rows; y++)
{
// Simulating work.
Thread.SpinWait(5000);
Console.Write("{0},{1} ", x, y);
}
// Assume that we know that the inner loop is very fast.
// Therefore, checking once per row is sufficient.
if (token.IsCancellationRequested)
{
// Cleanup or undo here if necessary...
Console.WriteLine("\r\nCancelling after row {0}.", x);
Console.WriteLine("Press any key to exit.");
// then...
break;
// ...or, if using Task:
// token.ThrowIfCancellationRequested();
}
}
}
Более полный пример содержится в разделе Практическое руководство. Прослушивание запросов на отмену посредством опросов.
Прослушивание с помощью регистрации обратного вызова
Некоторые операции могут быть заблокированы таким образом, при котором невозможно своевременно проверить значение токена отмены. В таких случаях можно регистрировать метод обратного вызова, который разблокирует метод при получении запроса отмены.
Метод Register возвращает объект CancellationTokenRegistration, который используется именно в таких целях. В следующем примере показано использование метода Register для отмены асинхронного веб-запроса.
Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token
Dim wc As New WebClient()
' To request cancellation on the token
' will call CancelAsync on the WebClient.
token.Register(Sub() wc.CancelAsync())
Console.WriteLine("Starting request")
wc.DownloadStringAsync(New Uri("https://www.contoso.com"))
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
WebClient wc = new WebClient();
// To request cancellation on the token
// will call CancelAsync on the WebClient.
token.Register(() => wc.CancelAsync());
Console.WriteLine("Starting request");
wc.DownloadStringAsync(new Uri("https://www.contoso.com"));
Объект CancellationTokenRegistration управляет синхронизацией потока и обеспечивает остановку выполнения в конкретный момент времени при получении обратного вызова.
Для обеспечения отклика системы и во избежание взаимоблокировок при регистрации обратных вызовов необходимо придерживаться следующих рекомендаций:
Метод обратного вызова должен быть быстрым, так как он вызывается синхронно, и поэтому возврат вызова Cancel будет выполнен после возврата из функции обратного вызова.
Если вы вызываете Dispose во время выполнения обратного вызова и удерживаете блокировку, которую ожидает функция обратного вызова, в программе может произойти взаимоблокировка. После завершения работы метода Dispose можно освобождать любые ресурсы, которые необходимы для обратного вызова.
Обратные вызовы не должны обрабатывать какие-либо ручные потоки или использовать SynchronizationContext в обратном вызове. Если обратный вызов необходимо выполнить для определенного потока, воспользуйтесь конструктором System.Threading.CancellationTokenRegistration, который позволяет задать активный SynchronizationContext.Current в качестве целевого объекта syncContext. Выполнение ручного потока в обратном вызове может привести к взаимоблокировке.
Более полный пример содержится в разделе Практическое руководство. Регистрация обратных вызовов для запросов на отмену.
Прослушивание с помощью дескриптора ожидания
В случаях, когда отменяемая операция может блокироваться на время ожидания примитива синхронизации, такого как System.Threading.ManualResetEvent или System.Threading.Semaphore, можно с помощью свойства CancellationToken.WaitHandle включить ожидание операцией как этого события, так и запроса отмены. Дескриптору ожидания токена отмены будет отправлен сигнал в ответ на запрос отмены, и метод сможет с помощью возвращаемого значения метода WaitAny определить, был ли этот сигнал отправлен токеном отмены. Затем операция может выполнить выход или создать исключение OperationCanceledException в зависимости от ситуации.
' Wait on the event if it is not signaled.
Dim myWaitHandle(2) As WaitHandle
myWaitHandle(0) = mre
myWaitHandle(1) = token.WaitHandle
Dim eventThatSignaledIndex =
WaitHandle.WaitAny(myWaitHandle, _
New TimeSpan(0, 0, 20))
// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
new TimeSpan(0, 0, 20));
В новом коде, предназначенном для .NET Framework 4, объектами System.Threading.ManualResetEventSlim и System.Threading.SemaphoreSlim поддерживается новая инфраструктура отмены в методах Wait. Можно передавать CancellationToken методу, и, когда производится запрос на отмену, событие активируется и создает OperationCanceledException.
Try
' mres is a ManualResetEventSlim
mres.Wait(token)
Catch e As OperationCanceledException
' Throw immediately to be responsive. The
' alternative is to do one more item of work,
' and throw on next iteration, because
' IsCancellationRequested will be true.
Console.WriteLine("Canceled while waiting.")
Throw
End Try
' Simulating work.
Console.Write("Working...")
Thread.SpinWait(500000)
try
{
// mres is a ManualResetEventSlim
mres.Wait(token);
}
catch (OperationCanceledException)
{
// Throw immediately to be responsive. The
// alternative is to do one more item of work,
// and throw on next iteration, because
// IsCancellationRequested will be true.
Console.WriteLine("The wait operation was canceled.");
throw;
}
Console.Write("Working...");
// Simulating work.
Thread.SpinWait(500000);
Более полный пример содержится в разделе Практическое руководство. Прослушивание запросов на отмену, содержащих дескрипторы ожидания.
Прослушивание нескольких токенов одновременно
В некоторых случаях прослушиватель может ожидать передачи нескольких токенов отмены одновременно. Например, отменяемая операция может в дополнение к токену отмены, переданному извне в качестве аргумента в параметр метода, отслеживать также внутренний токен отмены. Для этого следует создать исходный связанный токен, который может объединить несколько токенов в один, как показано в следующем примере.
Public Sub DoWork(ByVal externalToken As CancellationToken)
' Create a new token that combines the internal and external tokens.
Dim internalToken As CancellationToken = internalTokenSource.Token
Dim linkedCts As CancellationTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken)
Using (linkedCts)
Try
DoWorkInternal(linkedCts.Token)
Catch e As OperationCanceledException
If e.CancellationToken = internalToken Then
Console.WriteLine("Operation timed out.")
ElseIf e.CancellationToken = externalToken Then
Console.WriteLine("Canceled by external token.")
externalToken.ThrowIfCancellationRequested()
End If
End Try
End Using
End Sub
public void DoWork(CancellationToken externalToken)
{
// Create a new token that combines the internal and external tokens.
this.internalToken = internalTokenSource.Token;
this.externalToken = externalToken;
using (CancellationTokenSource linkedCts =
CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken))
{
try
{
DoWorkInternal(linkedCts.Token);
}
catch (OperationCanceledException)
{
if (internalToken.IsCancellationRequested)
{
Console.WriteLine("Operation timed out.");
}
else if (externalToken.IsCancellationRequested)
{
Console.WriteLine("Cancelling per user request.");
externalToken.ThrowIfCancellationRequested();
}
}
}
}
Следует отметить, что после выполнения исходным связанным токеном всех возложенных на него функций необходимо вызвать в нем метод Dispose. Более полный пример содержится в разделе Практическое руководство. Прослушивание нескольких запросов на отмену.
Совместная работа кода библиотеки и пользовательского кода
Инфраструктура универсальной отмены позволяет отменять пользовательский код кодом библиотеки, а также обратный процесс — отмену кода библиотеки пользовательским кодом в режиме совместной работы. Успешная совместная работа зависит от соблюдения каждой стороной следующих рекомендаций:
Если код библиотеки предоставляет отменяемые операции, он также должен предоставить общие методы, принимающие внешний токен отмены, чтобы пользовательский код мог запрашивать отмену.
Если код библиотеки выполняет вызов пользовательского кода, он должен быть в состоянии обработать исключение OperationCanceledException(externalToken) как совместную отмену, а не только как исключение сбоя.
Пользовательские делегаты должны пытаться своевременно отвечать на запросы отмены от кода библиотеки.
Примерами классов, соответствующих этим рекомендациям, являются System.Threading.Tasks.Task и System.Linq.ParallelEnumerable. Дополнительные сведения см. в разделах Отмена задач и Практическое руководство. Отмена запроса PLINQ.