Гарантированный способ отменить зависание Задача?

Мне часто приходится выполнять код в отдельном потоке, который долго работает, блокируется, нестабилен и \ или может зависать вечно. Поскольку в TPL есть множество примеров, которые приятно отменяют задачу с помощью токена отмены, я так и не нашел пример, который убивает зависшую задачу. Вероятно, ожидается, что код, который зависнет навсегда, как только вы свяжетесь с оборудованием или вызовете какой-либо сторонний код. Задание, которое зависает, не может проверить токен отмены и обречено остаться в живых навсегда. В критических приложениях я оснащаю эти задачи живыми сигналами, которые отправляются через регулярные промежутки времени. Как только зависшее задание обнаружено, оно уничтожается и запускается новый экземпляр.

В приведенном ниже коде показан пример задачи, которая вызывает долго выполняющийся метод-заполнитель SomeThirdPartyLongOperation(), который потенциально может зависнуть навсегда. Сначала StopTask() проверяет, выполняется ли еще задача, и пытается отменить ее с помощью токена отмены. Если это не работает, задача зависает и основной поток прерывается \ прерывается в старом стиле.

    private Task _task;
    private Thread _thread;
    private CancellationTokenSource _cancellationTokenSource;

    public void StartTask()
    {
        _cancellationTokenSource = new CancellationTokenSource();
        _task = Task.Factory.StartNew(() => DoWork(_cancellationTokenSource.Token), _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
    }

    public void StopTask()
    {
        if (_task.Status == TaskStatus.RanToCompletion)
            return;
        _cancellationTokenSource.Cancel();
        try
        {
            _task.Wait(2000); // Wait for task to end and prevent hanging by timeout.
        }
        catch (AggregateException aggEx)
        {
            List<Exception> exceptions = aggEx.InnerExceptions.Where(e => !(e is TaskCanceledException)).ToList(); // Ignore TaskCanceledException
            foreach (Exception ex in exceptions)
            {
                // Process exception thrown by task
            }
        }
        if (!_task.IsCompleted) // Task hangs and didn't respond to cancellation token => old school thread abort
        {
            _thread.Interrupt();
            if (!_thread.Join(2000))
            { 
                _thread.Abort();
            }
        }
        _cancellationTokenSource.Dispose();
        if (_task.IsCompleted)
        {
            _task.Dispose();
        }
    }

    private void DoWork(CancellationToken cancellationToken)
    {
        if (string.IsNullOrEmpty(Thread.CurrentThread.Name)) // Set thread name for debugging
            Thread.CurrentThread.Name = "DemoThread";
        _thread = Thread.CurrentThread; // Save for interrupting/aborting if thread hangs
        for (int i = 0; i < 10; i++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            SomeThirdPartyLongOperation(i);
        }
    }

Хотя я использую эту конструкцию уже несколько лет, я хочу знать, есть ли в ней потенциальные ошибки. Я никогда не видел пример задачи, которая сохраняет основной поток или дает ему имя для упрощения отладки, поэтому я немного не уверен, что это правильный путь. Комментировать любые детали можно только приветствовать!

2 ответа

Вероятно, ожидается, что код, который зависнет навсегда, как только вы свяжетесь с оборудованием или вызовете какой-либо сторонний код.

Общение: абсолютно нет. Всегда есть способ тайм-аута с API-интерфейсами связи, поэтому даже при неправильной работе оборудования нет необходимости принудительно завершать операцию ввода-вывода.

Сторонний код: только если вы параноик (или предъявляете высокие требования, такие как автоматизация 24x7).

Вот суть:

  • Нет способа насильно убить задачу.
  • Вы можете принудительно уничтожить поток, но это может легко вызвать серьезные проблемы с состоянием приложения, возможностью при возникновении взаимоблокировок в других частях кода и утечками ресурсов.
  • Вы можете принудительно убить домен приложения, что решает большую часть проблем состояния / взаимоблокировки приложения с уничтожением потоков. Однако это не решает их все, и все еще существует проблема утечек ресурсов.
  • Вы можете принудительно убить процесс. Это единственное действительно чистое и надежное решение.

Поэтому, если вы решите доверять стороннему коду, я рекомендую вам просто вызывать его, как любой другой API. Если вам требуется 100% -ная надежность независимо от сторонних библиотек, вам нужно заключить сторонний dll в отдельный процесс и использовать межпроцессное взаимодействие для его вызова.

Ваш текущий код принудительно убивает поток пула потоков, что, безусловно, не рекомендуется; эти потоки принадлежат пулу потоков, а не вам, и это все еще верно, даже если вы укажете LongRunning, Если вы идете по маршруту kill-thread (который не совсем надежен), то я рекомендую использовать явный поток.

Вопрос в том, почему эта задача вообще висит? Я думаю, что нет универсального решения этой проблемы, но вы должны сосредоточиться на задаче, чтобы всегда быть ответственным, а не заставлять ее прерывать.

В этом коде похоже, что вы ищете простой поток, а не задачу - вы не должны связывать задачи с потоками - очень вероятно, что задача переключится на другой поток после некоторых асинхронных операций, и вы в конечном итоге убивая невинную нить, которая больше не связана с вашей задачей. Если вам действительно нужно уничтожить весь поток, создайте выделенный только для этой работы.

Вы не должны также называть или делать что-либо с любым потоком, который используется для пула задач по умолчанию. Рассмотрим этот код:

static void Main(string[] args)
{
    Task.Run(sth);
    Console.Read();
}

static async Task sth()
{
    Thread.CurrentThread.Name = "My name";
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    await Task.Delay(1);
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    Console.WriteLine(Thread.CurrentThread.Name ?? "No name");
}

выход:

3
4
No name
Другие вопросы по тегам