Отменить задачу по времени

У меня есть многопоточное приложение, где мне нужно отменить каждую задачу через определенное время, даже если в момент отмены они используют неуправляемые ресурсы. Сейчас я использую следующий код (например, консольное приложение). В реальном приложении задержка может возникать в неуправляемом ресурсе.

static void Main()
    {
        for (int i = 0; i < 10; i++)
        {
            Task.Factory.StartNew(Do, TaskCreationOptions.LongRunning);
        }

        Console.ReadLine();
    }

    private static void Do()
    {
        new Timer(Thread.CurrentThread.Abort, null, 1000, -1);

        try
        {
            Console.WriteLine("Start " + Task.CurrentId);
            Thread.Sleep(2000);
            Console.WriteLine("End " + Task.CurrentId);
        }
        catch (Exception)
        {
            Console.WriteLine("Thread Aborted " + Task.CurrentId);
        }
    }

Получите результат:

введите здесь описание изображения

Но я не уверен, подходит ли это для реального приложения с точки зрения безопасности. Я также использовал CancellationToken в разных вариантах, но это не дает мне правильного результата, потому что, когда я использую CancellAfter() или .Delay() с временным интервалом и отменяю задачу через определенное время, я получаю следующие результаты:

static void Main()
    {
        for (int i = 0; i < 10; i++)
        {
            var clt = new CancellationTokenSource();

            Task task = new Task(() =>
            {
                Task.Delay(2000).ContinueWith(_ =>
                {
                    clt.Cancel();

                }, clt.Token);

                Do(clt.Token);

            }, clt.Token);

            task.Start();
        }

        Console.ReadLine();
    }

    private static void Do(CancellationToken cltToken)
    {
        Console.WriteLine("Start " + Task.CurrentId);

        Thread.Sleep(2500);

        if (!cltToken.IsCancellationRequested)
        {
            Console.WriteLine("End " + Task.CurrentId);
        }
        else
        {
            Console.WriteLine("Cancelled "+ Task.CurrentId);
        }
    }

введите здесь описание изображения

В этой ситуации все задачи должны быть отменены, потому что Thread.Sleep() > времени, отведенного на выполнение каждой задачи. Но мы можем видеть, что часть времени на выполнение.

Я также использую следующую конструкцию и даю тот же результат:

        static void Main()
    {
        for (int i = 0; i < 10; i++)
        {
            var clt = new CancellationTokenSource();
            clt.CancelAfter(2000);

            Task.Factory.StartNew(Do, clt.Token);

        }

        Console.ReadLine();
    }

    private static void Do(object obj)
    {
        var cltToken = (CancellationToken) obj;

        Console.WriteLine("Start " + Task.CurrentId);

        Thread.Sleep(2500);

        if (!cltToken.IsCancellationRequested)
        {
            Console.WriteLine("End " + Task.CurrentId);
        }
        else
        {
            Console.WriteLine("Cancelled "+ Task.CurrentId);
        }
    }

введите здесь описание изображения

Я также использую Parallel и инициализирую токен отмены внутри метода Do() и использую Timer для отмены токена после промежутка времени, но все они дают одинаковый результат.

Итак, почему это происходит и как правильно отменить задачу через определенное время???


person Andrey Zhminka    schedule 11.10.2013    source источник


Ответы (2)


Вы можете получить те же результаты, что и исходная версия «отмены», используя те же тайминги. Например, этот код:

static void Main()
{
    var clt = new CancellationTokenSource();
    clt.CancelAfter(1000);
    for (int i = 0; i < 10; i++)
    {
        Task.Run(() => Do(clt.Token));
    }
    Console.ReadLine();
}

private static void Do(CancellationToken cltToken)
{
    Console.WriteLine("Start " + Task.CurrentId);
    Thread.Sleep(2000);

    if (!cltToken.IsCancellationRequested)
    {
        Console.WriteLine("End " + Task.CurrentId);
    }
    else
    {
        Console.WriteLine("Cancelled "+ Task.CurrentId);
    }
}

Будет производить что-то похожее на:

Start 111
Start 112
Start 113
Start 114
Start 115
Start 116
Start 117
Start 118
Start 119
Start 120
Cancelled 111
Cancelled 112
Cancelled 118
Cancelled 116
Cancelled 114
Cancelled 113
Cancelled 117
Cancelled 115
Cancelled 119
Cancelled 120

Использование CancellationTokenSource — лучший вариант, чем прерывание потоков. Thread.Abort — плохая идея, поскольку она прерывает поток, не предоставляя надлежащих механизмов очистки. Использование токена позволяет вам совместно аккуратно обрабатывать отмену.

Что касается того, почему ваши другие параметры не работали должным образом - время, которое вы использовали, было слишком близко друг к другу. Это особенно проблема при работе под отладчиком, так как это предотвратит одновременное срабатывание таймингов (то есть: CancelAfter, а также Thread.Sleep). Если вы запустите выпускную сборку вне хост-процесса Visual Studio, вы, скорее всего, обнаружите, что они работают намного надежнее.

person Reed Copsey    schedule 11.10.2013

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

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

  • Очевидно, что нет возможности опросить CancellationToken, пока поток выполняет неуправляемый код. Таким образом, нет никакого способа инициировать корректное завершение работы самостоятельно.
  • Помимо того факта, что вы все равно не должны прерывать поток, вызов Thread.Abort не будет вводить сигнал прерывания в цель, пока она не присоединится к управляемой области. Другими словами, прерывания не завершат потоки, выполняющие неуправляемый код. Это было сделано намеренно, чтобы сделать аборты более безопасными.

Единственный способ сделать это надежно — запустить неуправляемый ресурс вне процесса. Это означает, что вам нужно будет запустить новый процесс для выполнения неуправляемого кода, а затем использовать WCF (или другой протокол связи) для отправки сообщений/данных туда и обратно. Если неуправляемый ресурс не отвечает своевременно, вы можете убить процесс. Это безопасно, потому что уничтожение другого процесса не повреждает состояние текущего процесса.

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

person Brian Gideon    schedule 11.10.2013