Как реализовать остановку потока перед запуском нового его экземпляра?

iRumba

Помогите разрулить потоки. Я не буду предоставлять код, мне нужна просто идея.

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

Framework 4.0 (без async / await)

2 ответа

iRumba

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

class Program
{
    private static Thread _thread;

    static void Main(string[] args)
    {
        Console.ReadLine();
        TriggerEventOld();

        Console.ReadLine();
        TriggerEventOld();

        // дождемся завершения
        _thread.Join();
    }

    private static void TriggerEventOld()
    {
        if (_thread != null)
        {
            _thread.Abort();
            // корректнее будет дождаться завершения
            _thread.Join();
        }

        _thread = new Thread(Foo);
        _thread.Start();
    }

    private static void Foo()
    {
        Console.WriteLine("Foo started");

        try
        {
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(500); 
            }
        }
        catch (ThreadAbortException)
        {
            Console.WriteLine("Foo aborted");
        }

        Console.WriteLine("Foo ended");
    }
}

Лучше не связываться с Abort(), и использовать "мягкое" завершения потока с помощью ManualResetEventSlim, который послужит флагом завершения. У такого способа есть один минус -- если вы запускаете синхронный код, который может долго отвечать (например, чтение файла или запрос в сеть), то придется долго ждать завершения потока при новом запуске.

class Program
{
    private static Thread _thread;
    private static ManualResetEventSlim _reset = new ManualResetEventSlim();

    static void Main(string[] args)
    {
        Console.ReadLine();
        TriggerEventOld();

        Console.ReadLine();
        TriggerEventOld();

        // дождемся завершения
        _thread.Join();
    }

    private static void TriggerEventOld()
    {
        if (_thread != null)
        {
            _reset.Set();
            // корректнее будет дождаться завершения
            _thread.Join();
        }

        _thread = new Thread(Foo);
        _reset.Reset();
        _thread.Start();
    }

    private static void Foo()
    {
        Console.WriteLine("Foo started");

        for (int i = 0; i < 10; i++)
        {
            if (_reset.IsSet)
            {
                Console.WriteLine("Foo canceled");
                break;
            }

            Thread.Sleep(500); 
        }

        if (!_reset.IsSet)
        {
            Console.WriteLine("Foo ended");
        }
    }
}

Но поскольку вы используете .NET 4.0, значит вам доступен TPL. В таком случае будет лучше воспользоваться Task и CancellationToken, с ними вы можете сделать "мягкую" остановку задачи. Преимущество новой модели заключается в том, что практически все асинхронные методы поддерживают остановку с помощью CancellationToken, поэтому код будет реагировать на завершение быстрее.

class Program
{
    private static Task _task;
    private static CancellationTokenSource _cts;

    static void Main(string[] args)
    {
        Console.ReadLine();
        TriggerEventNew();

        Console.ReadLine();
        TriggerEventNew();

        // дождемся завершения;
        // этот вызов может выбросить исключение,
        // если внутри возникнет необработанное исключение
        _task.Wait();
    }

    private static void TriggerEventNew()
    {
        if (_task != null)
        {
            _cts.Cancel();
            // корректнее будет дождаться завершения;
            // этот вызов может выбросить исключение,
            // если внутри возникнет необработанное исключение
            _task.Wait();
        }

        // пересоздаем каждый раз, поскольку это одноразовый объект
        _cts = new CancellationTokenSource();
        _task = Task.Factory.StartNew(() => FooNew(_cts.Token), _cts.Token);
    }

    private static void FooNew(CancellationToken token)
    {
        Console.WriteLine("FooNew started");

        for (int i = 0; i < 10; i++)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("FooNew aborted");
                break;
            }

            Thread.Sleep(500);
        }

        if (!token.IsCancellationRequested)
        {
            Console.WriteLine("FooNew ended");
        }
    }
}

Если будете использовать token.ThrowIfCancellationRequested(), не забудьте обернуть код в блок try/catch c проверкой токена:

try
{
    ...
    token.ThrowIfCancellationRequested();
    ...
}
catch (OperationCanceledException e)
{
    if (e.CancellationToken != token)
    {
        // отмена была вызвана не нами, бросаем исключение
        throw;
    }
}


iRumba

В итоге я придумал собственное решение. Если его немного развить, то можно и под другие задачи приспособить. Но мне достаточно такого функционала.

public class TaskQueue
{
    Task _worker;
    Queue<action> _queue;
    int _maxTasks;
    bool _deleteOld;
    object _lock = new object();

    public TaskQueue(int maxTasks, bool deleteOld = true)
    {
        if (maxTasks < 1)
            throw new ArgumentException("TaskQueue: максимальное число задач должно быть больше 0");
        _maxTasks = maxTasks;
        _deleteOld = deleteOld;
        _queue = new Queue<action>(maxTasks);
    }

    public bool Add(Action action)
    {
        if (_queue.Count() < _maxTasks)
        {
            _queue.Enqueue(action);
            DoWorkAsync();
            return true;
        }
        if (_deleteOld)
        {
            _queue.Dequeue();
            return Add(action);
        }
        return false;
    }

    void DoWorkAsync()
    {
        if(_queue.Count>0)
            _worker = Task.Factory.StartNew(DoWork);
    }

    void DoWork()
    {
        lock (_lock)
        {
            if (_queue.Count > 0)
            {
                _queue.Dequeue().Invoke();
                DoWork();
            }
        }
    }
}
</action></action>

Здесь все просто. Есть очередь задач определенного размера и флагом, говорящим о том, удалять ли старые задачи в случае переполнения очереди. То есть я добавляю задачу, она пытается добавиться в очередь. Если в очереди есть свободное место, добавляется, если нет, то либо не добавляется, либо удаляет из очереди старую задачу (которая следующая на выполнение). Далее запускается обработчик очереди, который, если не выполняется другая задача, берет задачу из очереди и запускает. После завершения запускается рекурсивно.

licensed under cc by-sa 3.0 with attribution.