Вопрос или проблема
В потоке я создаю несколько System.Threading.Task
и запускаю каждую задачу.
Когда я вызываю .Abort()
для завершения потока, задачи не отменяются.
Как я могу передать .Abort()
моим задачам?
Вы не можете. Задачи используют фоновые потоки из пула потоков. Кроме того, отмена потоков с помощью метода Abort не рекомендуется. Вам стоит взглянуть на следующий блог пост, который объясняет правильный способ отмены задач с использованием токенов отмены. Вот пример:
class Program
{
static void Main()
{
var ts = new CancellationTokenSource();
CancellationToken ct = ts.Token;
Task.Factory.StartNew(() =>
{
while (true)
{
// здесь выполняем какую-то тяжелую работу
Thread.Sleep(100);
if (ct.IsCancellationRequested)
{
// другой поток решил отменить
Console.WriteLine("задача отменена");
break;
}
}
}, ct);
// Имитируем ожидание 3 секунды для завершения задачи
Thread.Sleep(3000);
// Не можем больше ждать => отменяем эту задачу
ts.Cancel();
Console.ReadLine();
}
}
Как предлагает этот пост, это можно сделать следующим образом:
int Foo(CancellationToken token)
{
Thread t = Thread.CurrentThread;
using (token.Register(t.Abort))
{
// вычислительная работа здесь
}
}
Хотя это работает, не рекомендуется использовать такой подход. Если вы можете контролировать код, который выполняется в задаче, лучше использовать правильную обработку отмены.
Отменить задачу довольно просто, если вы захватите поток, в котором выполняется задача. Вот пример кода, чтобы продемонстрировать это:
void Main()
{
Thread thread = null;
Task t = Task.Run(() =>
{
// Захватываем поток
thread = Thread.CurrentThread;
// Имитируем работу (обычно из стороннего кода)
Thread.Sleep(1000);
// Если вы закомментируете thread.Abort(), то это будет отображено
Console.WriteLine("Задача завершена!");
});
// Это необходимо в примере, чтобы избежать ситуации, когда поток все еще NULL
Thread.Sleep(10);
// Отменяем задачу, прерывая поток
thread.Abort();
}
Я использовал Task.Run(), чтобы показать самый распространенный случай использования – использование удобства задач с старым однопоточным кодом, который не использует класс CancellationTokenSource для определения, нужно ли его отменять или нет.
Такого рода вещи являются одной из логистических причин, почему Abort
устарел. Прежде всего, не используйте Thread.Abort()
для отмены или остановки потока, если это возможно. Abort()
следует использовать только для принудительного завершения потока, который не отвечает на более мирные запросы остановиться вовремя.
Тем не менее, вам необходимо предоставить общий индикатор отмены, который один поток устанавливает и ждет, пока другой поток периодически проверяет и корректно завершается. .NET 4 включает структуру, специально предназначенную для этой цели, CancellationToken
.
Я использую смешанный подход для отмены задачи.
- Во-первых, я пытаюсь аккуратно отменить ее с помощью отмены.
- Если она все еще выполняется (например, из-за ошибки разработчика), тогда веду себя неправильно и убиваю ее с помощью старомодного метода Abort.
Вот пример ниже:
private CancellationTokenSource taskToken;
private AutoResetEvent awaitReplyOnRequestEvent = new AutoResetEvent(false);
void Main()
{
// Запускаем задачу, которая делает ничего, кроме как спит 1 секунду
LaunchTaskAsync();
Thread.Sleep(100);
// Останавливаем задачу
StopTask();
}
/// <summary>
/// Запускаем задачу в новом потоке
/// </summary>
void LaunchTaskAsync()
{
taskToken = new CancellationTokenSource();
Task.Factory.StartNew(() =>
{
try
{ // Захватываем поток
runningTaskThread = Thread.CurrentThread;
// Выполняем задачу
if (taskToken.IsCancellationRequested || !awaitReplyOnRequestEvent.WaitOne(10000))
return;
Console.WriteLine("Задача завершена!");
}
catch (Exception exc)
{
// Обработка исключения
}
}, taskToken.Token);
}
/// <summary>
/// Остановить выполняющуюся задачу
/// </summary>
void StopTask()
{
// Пытаемся аккуратно отменить задачу
if (taskToken != null)
{
if (taskToken.IsCancellationRequested)
return;
else
taskToken.Cancel();
}
// Уведомляем ожидающий поток, что событие произошло
if (awaitReplyOnRequestEvent != null)
awaitReplyOnRequestEvent.Set();
// Если через 1 секунду задача все еще выполняется, убиваем ее безжалостно
if (runningTaskThread != null)
{
try
{
runningTaskThread.Join(TimeSpan.FromSeconds(1));
}
catch (Exception ex)
{
runningTaskThread.Abort();
}
}
}
Чтобы ответить на вопрос Прерака К. о том, как использовать CancellationTokens, когда не используешь анонимный метод в Task.Factory.StartNew(), вы передаете CancellationToken как параметр в метод, который вы запускаете с помощью StartNew(), как показано в примере MSDN здесь.
например.
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
Task.Factory.StartNew(() => DoSomeWork(1, token), token);
static void DoSomeWork(int taskNum, CancellationToken ct)
{
// Выполняем работу здесь, проверяя и действуя на ct.IsCancellationRequested где применимо,
}
Вы не должны пытаться делать это напрямую. Проектируйте ваши задачи так, чтобы они работали с CancellationToken, и отменяйте их таким образом.
Кроме того, я бы рекомендовал изменить ваш основной поток, чтобы он функционировал также через CancellationToken. Вызов Thread.Abort()
– плохая идея – это может привести к различным проблемам, которые очень трудно диагностировать. Вместо этого этот поток может использовать ту же отмену, которую используют ваши задачи – и тот же CancellationTokenSource
можно использовать для инициирования отмены всех ваших задач и вашего основного потока.
Это приведет к гораздо более простому и безопасному дизайну.
Задачи имеют первоклассную поддержку отмены через токены отмены. Создайте ваши задачи с токенами отмены и отменяйте задачи через них явно.
Вы можете использовать CancellationToken
, чтобы контролировать, будет ли задача отменена. Вы говорите о прерывании ее до того, как она начнется (“неважно, я уже это сделал”), или о фактическом прерывании ее в процессе? Если первое, CancellationToken
может быть полезен; если второе, вы, вероятно, должны будете реализовать свой собственный механизм “выхода” и проверять в соответствующих точках выполнения задачи, следует ли вам быстро завершить (вы все равно можете использовать CancellationToken, чтобы помочь вам, но это немного более ручное выполнение).
MSDN имеет статью о отмене задач:
http://msdn.microsoft.com/en-us/library/dd997396.aspx
Задачи выполняются в пуле потоков (по крайней мере, если вы используете фабрику по умолчанию), поэтому прерывание потока не может повлиять на задачи. Для отмены задач см. отмену задач на msdn.
Я пробовал CancellationTokenSource
, но не смог это сделать. И я сделал это своим способом. И это работает.
namespace Blokick.Provider
{
public class SignalRConnectProvider
{
public SignalRConnectProvider()
{
}
public bool IsStopRequested { get; set; } = false; //1-)Это важно и по умолчанию `false`.
public async Task<string> ConnectTab()
{
string messageText = "";
for (int count = 1; count < 20; count++)
{
if (count == 1)
{
//Делаем что-то.
}
try
{
//Делаем что-то.
}
catch (Exception ex)
{
//Делаем что-то.
}
if (IsStopRequested) //3-)Это важно. Контроль запроса на остановку задачи. Должен быть истинным и внутри.
{
return messageText = "Задача остановлена."; //4-) И так возвращаем и выходим из кода и задачи.
}
if (Connected)
{
//Делаем что-то.
}
if (count == 19)
{
//Делаем что-то.
}
}
return messageText;
}
}
}
И еще класс вызова метода:
namespace Blokick.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MessagePerson : ContentPage
{
SignalRConnectProvider signalR = new SignalRConnectProvider();
public MessagePerson()
{
InitializeComponent();
signalR.IsStopRequested = true; // 2-) И это. Сделать истинным, если задача выполняется и войти в оператор if свойства IsStopRequested.
if (signalR.ChatHubProxy != null)
{
signalR.Disconnect();
}
LoadSignalRMessage();
}
}
}
Вы можете отменить задачу так же, как поток, если вы можете вызвать задачу, чтобы она выполнялась на своем собственном потоке и вызвать Abort
на ее Thread
объекте. По умолчанию задача выполняется в потоке пула потоков или в вызывающем потоке – ни то, ни другое обычно не следует прерывать.
Чтобы гарантировать, что задача получает свой собственный поток, создайте собственный планировщик, производный от TaskScheduler
. В вашей реализации QueueTask
создайте новый поток и используйте его для выполнения задачи. Позже вы можете прервать поток, что приведет к тому, что задача завершится в состоянии ошибки с ThreadAbortException
.
Используйте этот планировщик задач:
class SingleThreadTaskScheduler : TaskScheduler
{
public Thread TaskThread { get; private set; }
protected override void QueueTask(Task task)
{
TaskThread = new Thread(() => TryExecuteTask(task));
TaskThread.Start();
}
protected override IEnumerable<Task> GetScheduledTasks() => throw new NotSupportedException(); // Не используется
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => throw new NotSupportedException(); // Не используется
}
Запустите свою задачу так:
var scheduler = new SingleThreadTaskScheduler();
var task = Task.Factory.StartNew(action, cancellationToken, TaskCreationOptions.LongRunning, scheduler);
Позже вы можете прервать с:
scheduler.TaskThread.Abort();
Обратите внимание, что предостережение о прерывании потока все еще применимо:
Метод
Thread.Abort
следует использовать с осторожностью. Особенно когда вы вызываете его для прерывания потока, отличного от текущего потока, вы не знаете, какой код был выполнен или не выполнен, когда ThreadAbortException выбрасывается, и вы не можете быть уверены в состоянии вашего приложения или в каком-либо состоянии приложения и пользователя, за которое оно отвечает. Например, вызовThread.Abort
может помешать выполнению статических конструкторов или предотвратить освобождение неуправляемых ресурсов.
Вы можете использовать этот класс..:
Он работает для всех типов возвращаемых значений..
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace CarNUChargeTester
{
public class TimeOutTaskRunner<T>
{
private Func<T> func;
private int sec;
private T result;
public TimeOutTaskRunner(Func<T> func, int sec)
{
this.func = func;
this.sec = sec;
}
public bool run()
{
var scheduler = new SingleThreadTaskScheduler();
Task<T> task = Task<T>.Factory.StartNew(func, (new CancellationTokenSource()).Token, TaskCreationOptions.LongRunning, scheduler);
if (!task.Wait(TimeSpan.FromSeconds(sec)))
{
scheduler.TaskThread.Abort();
return false;
}
result = task.Result;
return true;
}
public T getResult() { return result; }
}
class SingleThreadTaskScheduler : TaskScheduler
{
public Thread TaskThread { get; private set; }
protected override void QueueTask(Task task)
{
TaskThread = new Thread(() => TryExecuteTask(task));
TaskThread.Start();
}
protected override IEnumerable<Task> GetScheduledTasks() => throw new NotSupportedException();
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => throw new NotSupportedException();
}
}
Чтобы использовать это, вы можете написать:
TimeOutTaskRunner<string> tr = new TimeOutTaskRunner<string>(f, 10); // 10 сек для выполнения f
if (!tr.run())
errorMsg("TimeOut"); !! Моя функция
tr.getResult() // получить результаты, если не произошло таймаута..
Ответ или решение
Как отменить задачи TPL в .NET
Отмена задач, созданных с помощью обертки TPL (Task Parallel Library
) в .NET, может быть непростой задачей, особенно в контексте использования Thread.Abort()
. Важно понимать, что Thread.Abort()
неэффективно и даже опасно, поскольку оно может приводить к непредсказуемым ситуациям, таким как несогласованное состояние программы или утечки ресурсов.
В этом ответе мы подробно обсудим, как правильно управлять отменой задач, используя токены отмены (CancellationToken
), и какие подходы можно применять для завершения работы задач.
Почему Thread.Abort()
не рекомендуется
-
Непредсказуемое поведение: Вызов
Thread.Abort()
завершает поток немедленно, не позволяя коду, который выполнялся в этом потоке, корректно завершиться. Это может привести к состояниям гонки и утечкам памяти. -
Исключения: При вызове
Abort
в потоке, обрабатывающем экземплярTask
, может быть выброшено исключениеThreadAbortException
, которое не всегда может быть корректно обработано. -
Прекращение работы ресурсов: Неосвобожденные ресурсы, такие как сетевые соединения или файловые дескрипторы, могут не закрываться корректно.
Использование токенов отмены
Рекомендуемая практика для отмены задач — использование CancellationTokenSource
и CancellationToken
. Вот пример, как это можно правильно реализовать:
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
var cancellationTokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = cancellationTokenSource.Token;
Task.Factory.StartNew(() =>
{
try
{
while (true)
{
// Выполнение длительной работы
Thread.Sleep(100);
// Проверка на запрос отмены
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("Задача отменена");
break;
}
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Задача обработала отмену");
}
}, cancellationToken);
// Имитация ожидания выполнения задачи
Thread.Sleep(3000);
// Запрос на отмену задачи
cancellationTokenSource.Cancel();
Console.ReadLine();
}
}
Способы реализации BOOT операции задач
Если по каким-то причинам необходимо "насильно" завершить задачу, вот несколько подходов:
-
Использование токенов отмены: Как уже упоминалось, наиболее безопасный и надежный способ остановить задачу — использовать токены отмены.
-
Создание кастомного планировщика задач: Можно создать планировщик, который запускает каждую задачу на отдельном потоке, и в дальнейшем использовать
Thread.Abort()
.
class SingleThreadTaskScheduler : TaskScheduler
{
public Thread TaskThread { get; private set; }
protected override void QueueTask(Task task)
{
TaskThread = new Thread(() => TryExecuteTask(task));
TaskThread.Start();
}
protected override IEnumerable<Task> GetScheduledTasks() => throw new NotSupportedException();
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => throw new NotSupportedException();
}
// Использование:
var scheduler = new SingleThreadTaskScheduler();
var task = Task.Factory.StartNew(action, cancellationToken, TaskCreationOptions.LongRunning, scheduler);
Но, как уже было сказано, это не рекомендуется из-за возможных проблем с согласованностью данных.
Заключение
В заключение, задача управления отменой в TPL не должна полагаться на Thread.Abort()
. Вместо этого используйте CancellationToken
для безопасной и контролируемой отмены ребер, что защитит ваше приложение от многих проблем. Если вы используете сторонний код, который не поддерживает отмену задач, по возможности улучшите его поддержку. Следуя этим рекомендациям и подходам, вы сможете создать более надежные и управляемые приложения.