C # многопоточность и Windows Forms - PullRequest
4 голосов
/ 12 июня 2009

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

Рабочий поток должен быть в состоянии отменить и сообщить о своем прогрессе. Я не использовал BackgroundWorker, потому что все примеры, которые я видел, содержат код Process в самой форме, а не отдельный объект. Я думал о наследовании LongRunningProcess для BackgroundWorker, но подумал, что это приведет к ненужным методам объекта. В идеале, я бы предпочел не иметь ссылку Form на процесс ("_lrp"), но я не понимаю, как можно было бы отменить процесс, если у меня нет события в LRP, которое проверяет флаг на вызывающем абоненте, но это кажется излишне сложным и, возможно, даже неправильным.

Форма Windows (Правка: перемещено * .EndInvoke вызывает на обратный вызов)

public partial class MainForm : Form
{
    MethodInvoker _startInvoker = null;
    MethodInvoker _stopInvoker = null;
    bool _started = false;

    LongRunningProcess _lrp = null;

    private void btnAction_Click(object sender, EventArgs e)
    {
        // This button acts as a Start/Stop switch.
        // GUI handling (changing button text etc) omitted
        if (!_started)
        {
            _started = true;
            var lrp = new LongRunningProcess();

            _startInvoker = new MethodInvoker((Action)(() => Start(lrp)));
            _startInvoker.BeginInvoke(new AsyncCallback(TransferEnded), null);
        }
        else
        {
            _started = false;
            _stopInvoker = new MethodInvoker(Stop);
                _stopInvoker.BeginInvoke(Stopped, null);
        }
    }

    private void Start(LongRunningProcess lrp)
    {
        // Store a reference to the process
        _lrp = lrp;

        // This is the same technique used by BackgroundWorker
        // The long running process calls this event when it 
        // reports its progress
        _lrp.ProgressChanged += new ProgressChangedEventHandler(_lrp_ProgressChanged);
        _lrp.RunProcess();
    }

    private void Stop()
    {
        // When this flag is set, the LRP will stop processing
        _lrp.CancellationPending = true;
    }

    // This method is called when the process completes
    private void TransferEnded(IAsyncResult asyncResult)
    {
        if (this.InvokeRequired)
        {
            this.BeginInvoke(new Action<IAsyncResult>(TransferEnded), asyncResult);
        }
        else
        {
            _startInvoker.EndInvoke(asyncResult);
            _started = false;
            _lrp = null;
        }
    }

    private void Stopped(IAsyncResult asyncResult)
    {
        if (this.InvokeRequired)
        {
            this.BeginInvoke(new Action<IAsyncResult>(Stopped), asyncResult);
        }
        else
        {
            _stopInvoker.EndInvoke(asyncResult);
            _lrp = null;
        }
    }

    private void _lrp_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        // Update the progress
        // if (progressBar.InvokeRequired) etc...
    }
}

Фоновый процесс:

public class LongRunningProcess
{
    SendOrPostCallback _progressReporter;
    private readonly object _syncObject = new object();
    private bool _cancellationPending = false;

    public event ProgressChangedEventHandler ProgressChanged;

    public bool CancellationPending
    {
        get { lock (_syncObject) { return _cancellationPending; } }
        set { lock (_syncObject) { _cancellationPending = value; } }
    }

    private void ReportProgress(int percentProgress)
    {
        this._progressReporter(new ProgressChangedEventArgs(percentProgress, null));
    }

    private void ProgressReporter(object arg)
    {
        this.OnProgressChanged((ProgressChangedEventArgs)arg);
    }

    protected virtual void OnProgressChanged(ProgressChangedEventArgs e)
    {
        if (ProgressChanged != null)
            ProgressChanged(this, e);
    }

    public bool RunProcess(string data)
    {
        // This code should be in the constructor
        _progressReporter = new SendOrPostCallback(this.ProgressReporter);

        for (int i = 0; i < LARGE_NUMBER; ++i)
        {
            if (this.CancellationPending)
                break;

            // Do work....
            // ...
            // ...

            // Update progress
            this.ReportProgress(percentageComplete);

            // Allow other threads to run
            Thread.Sleep(0)
        }

        return true;
    }
}

Ответы [ 5 ]

1 голос
/ 12 июня 2009

Меня немного смущает использование метода MethodInvoker.BeginInvoke (). Есть ли причина, по которой вы решили использовать это вместо создания нового потока и использования Thread.Start () ...?

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

Наконец, вы полагаетесь на побочный эффект BeginInvoke для запуска LRP в рабочем потоке из пула управляемых потоков. Опять же, вы должны быть уверены, что это ваше намерение. Пул потоков включает в себя семантику организации очередей и отлично работает, когда загружен большим количеством недолговечных процессов. Я не уверен, что это хороший выбор для длительных процессов. Я бы предпочел использовать класс Thread для запуска вашего продолжительного потока.

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

1 голос
/ 12 июня 2009

Вы можете сделать свой _cancellationPending изменчивым и избежать блокировки. Почему вы звоните Стоп в другой теме?

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

protected virtual void OnProgressChanged(ProgressChangedEventArgs e)
{
    var progressChanged = ProgressChanged;
    if (progressChanged != null)
        progressChanged(this, e);
}

Если фоновый работник подходит, его не нужно перекодировать;)

1 голос
/ 12 июня 2009

Мне нравится разделение фонового процесса на отдельный объект. Однако у меня сложилось впечатление, что ваш поток пользовательского интерфейса блокируется до завершения фонового процесса, поскольку вы вызываете BeginInvoke и EndInvoke в одном и том же обработчике кнопок.

MethodInvoker methodInvoker = new MethodInvoker((Action)(() => Start(lrp)));
IAsyncResult result = methodInvoker.BeginInvoke(new AsyncCallback(TransferEnded), null);
methodInvoker.EndInvoke(result);

Или я что-то упустил?

0 голосов
/ 12 июня 2009

Вы можете использовать BackgroundWorker и по-прежнему перемещать свой рабочий код за пределы класса Form. У вашего класса Worker есть метод Work. Пусть Work принимает BackgroundWorker в качестве параметра и перегружает метод Work сигнатурой, отличной от BackgroundWorker, которая отправляет нулевое значение первому методу.

Затем в своей форме используйте BackgroundWorker с ProgressReporting, а в своей Работе (BackgroundWorker bgWorker, params object [] otherParams) вы можете включать такие выражения, как:

    if( bgWorker != null && bgWorker.WorkerReportsProgress )
    {
        bgWorker.ReportProgress( percentage );
    }

... и аналогичным образом включают проверки для CancellationPending.

Тогда в вашем коде форм вы можете обрабатывать события. Сначала установите bgWorker.DoWork += new DoWorkEventHandler( startBgWorker );, где этот метод запускает ваш метод Worker.Work, передавая bgWorker в качестве аргумента.

Это можно затем запустить из события кнопки, которое называется bgWorker.RunWorkerAsync.

Затем вторая кнопка отмены может вызвать bgWorker.CancelAsync, которая затем попадет в ваш раздел, где вы проверили отмену отмены.

В случае успеха или отмены вы будете обрабатывать событие RunWorkerCompleted, где вы проверяете, был ли работник отменен. Тогда, если это не так, вы предполагаете, что все прошло успешно, и идете по этому пути.

Перегрузив метод Work, вы можете сохранить его для повторного использования из кода, который не заботится о Forms или ComponentModel.

И, конечно же, вы реализуете событие progressaged без необходимости заново изобретать колесо на этом. ProTip: ProgressChangedEventArgs принимает значение типа int, но не заставляет его превышать 100. Чтобы сообщить более мелкий процент прогресса, передайте аргумент с множителем (скажем, 100), так что 14,32% будет прогрессом 1432. Затем вы можете отформатируйте отображение, либо переопределите индикатор выполнения, либо отобразите его в виде текстового поля. (все с DRY-дружественным дизайном)

0 голосов
/ 12 июня 2009

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

private static object eventSyncLock = new object();

protected virtual void OnProgressChanged(ProgressChangedEventArgs e)
{
    ProgressChangedEventHandler handler;
    lock(eventSyncLock)
    {
      handler = ProgressChanged;
    }
    if (handler != null)
        handler(this, e);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...