Как правильно обрабатывать закрытие формы при использовании фоновых рабочих? - PullRequest
3 голосов
/ 08 декабря 2010

Я наблюдаю странную ошибку в некоторых моих кодах, которая, как я подозреваю, связана с тем, как закрываются формы и взаимодействуют фоновые рабочие.

Вот код, который потенциально может быть ошибочным:

var worker = new BackgroundWorker();
    worker.DoWork += (sender, args) => {
        command();
    };
    worker.RunWorkerCompleted += (sender, args) => {
        cleanup();
        if (args.Error != null)
            MessageBox.Show("...", "...", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    };
    worker.RunWorkerAsync();

Этот код выполняется в методе в форме, когда нажата кнопка.Команда () работает медленно, для запуска может потребоваться несколько секунд.

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

Проблема в том, что вызов cleanup () иногда вызывает исключение ObjectDisposedException.Я говорю «иногда», потому что это никогда не происходит на моем компьютере.Если форма закрыта до выполнения команды (), обработчик, зарегистрированный для RunWorkerCompleted, не выполняется.На другом компьютере обработчик вызывается один раз из ста.На компьютере коллеги это почти всегда вызывается.По-видимому, вероятность выполнения обработчика увеличивается с возрастом / медлительностью компьютера.

Первый вопрос:

Это ожидаемое поведение BakgroundWorker?Я не ожидал бы, что он узнает что-либо о форме, поскольку я не вижу ничего, что связывало бы форму «это» с «работником».

Второй вопрос:

Как мне поступитьоб исправлении этой проблемы?

Возможные решения, которые я рассматриваю:

  1. Проверьте (! this.IsDisposed) перед вызовом cleanup ().Достаточно ли этого, или форма может быть удалена во время выполнения очистки?
  2. Обернуть вызов cleanup () в try {} catch (ObjectDisposedException).Мне не очень нравится такой подход, так как я могу ловить исключения, которые возникли из-за какой-то другой несвязанной ошибки в cleanup () или одного из вызываемых им методов.
  3. Зарегистрировать обработчик для IsClosingи откладывать или отменять закрытие до тех пор, пока не запустится обработчик для RunWorker Completed.

Дополнительная информация, которая может иметь отношение: код из команды () приведет к обновлению объектов GUI в «this».Такие обновления выполняются через вызовы этой функции F #:

/// Run a delegate on a ISynchronizeInvoke (typically a Windows.Form).
let runOnInvoker (notification_receiver : ISynchronizeInvoke) excHandler (dlg : Delegate) args =
    try
        let args : System.Object[] = args |> Seq.cast |> Array.ofSeq
        notification_receiver.Invoke (dlg, args) |> ignore
    with
        | :? System.InvalidOperationException as op ->
            excHandler(op)

1 Ответ

4 голосов
/ 08 декабря 2010

Упомянутые вами исключения не имеют никакого отношения к BackgroundWorker, за исключением того, что один поток (рабочий) пытается получить доступ к элементам управления, которые были удалены другим потоком (UI).

Решение, которое я бы использовал, заключается в том, чтобы прикрепить обработчик события к событию Form.FormClosed, чтобы установить флаг, который сообщает вам, что пользовательский интерфейс был разрушен.Затем дескриптор RunWorkerCompleted проверит, не сорван ли пользовательский интерфейс, прежде чем пытаться что-либо делать с формой.

Хотя этот подход, вероятно, будет работать более надежно, чем проверка IsDisposed, если вы этого не сделаетеЯвное удаление формы не дает 100% гарантии того, что форма не будет закрыта и / или удалена сразу после того, как код очистки проверил флаг и обнаружил, что он все еще там.Это условие гонки, о котором вы сами упомянули.

Чтобы устранить это состояние гонки, вам необходимо выполнить синхронизацию, например, так:

// set this to new object() in the constructor
public object CloseMonitor { get; private set; }

public bool HasBeenClosed { get; private set; }

private void Form1_FormClosed(object sender, FormClosedEventArgs e) {
    lock (this.CloseMonitor) {
        this.HasBeenClosed = true;
        // other code
    }
}

и для работника:

worker.RunWorkerCompleted += (sender, args) => {
    lock (form.CloseMonitor) {
        if (form.HasBeenClosed) {
            // maybe special code for this case
        }
        else {
            cleanup();
            // and other code
        }
    }
};

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

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

...