Pergunta

Considere um hipotética método de um objeto que faz coisas para você:

public class DoesStuff
{
    BackgroundWorker _worker = new BackgroundWorker();

    ...

    public void CancelDoingStuff()
    {
        _worker.CancelAsync();

        //todo: Figure out a way to wait for BackgroundWorker to be cancelled.
    }
}

Como se pode esperar de um BackgroundWorker ser feito?


No passado as pessoas têm tentado:

while (_worker.IsBusy)
{
    Sleep(100);
}

este impasses , porque IsBusy não é limpo até após o evento RunWorkerCompleted é tratado, e que o evento não pode se tratado até que o aplicativo estiver inativo. A aplicação não vai ocioso até que o trabalhador é feito. (Além disso, é um loop ocupado -. Nojento)

Outros têm add sugeriu kludging-lo em:

while (_worker.IsBusy)
{
    Application.DoEvents();
}

O problema com isso é que é Application.DoEvents() faz com que mensagens atualmente na fila para ser processado, o que causar problemas reentrada (.NET não é re-entrantes).

Espero usar alguma solução envolvendo objetos de sincronização de Eventos, onde o código espera para um evento - que evento RunWorkerCompleted manipuladores conjuntos do trabalhador. Algo como:

Event _workerDoneEvent = new WaitHandle();

public void CancelDoingStuff()
{
    _worker.CancelAsync();
    _workerDoneEvent.WaitOne();
}

private void RunWorkerCompletedEventHandler(sender object, RunWorkerCompletedEventArgs e)
{
    _workerDoneEvent.SetEvent();
}

Mas estou de volta para o impasse:. O manipulador de eventos não pode ser executado até que o aplicativo estiver inativo, ea aplicação não vai ocioso porque está à espera de um evento

Então, como você pode esperar por um BackgroundWorker ao fim?


Atualizar As pessoas parecem ser confundido por esta questão. Eles parecem pensar que eu vou estar usando o BackgroundWorker como:

BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += MyWork;
worker.RunWorkerAsync();
WaitForWorkerToFinish(worker);

É não , isto é não o que estou fazendo, e isso é não que está sendo pedido aqui. Se fosse esse o caso, não haveria nenhum ponto em usar um trabalhador de fundo.

Foi útil?

Solução

Se eu entender o seu direito exigência, você poderia fazer algo parecido com isto (código não testado, mas mostra a idéia geral):

private BackgroundWorker worker = new BackgroundWorker();
private AutoResetEvent _resetEvent = new AutoResetEvent(false);

public Form1()
{
    InitializeComponent();

    worker.DoWork += worker_DoWork;
}

public void Cancel()
{
    worker.CancelAsync();
    _resetEvent.WaitOne(); // will block until _resetEvent.Set() call made
}

void worker_DoWork(object sender, DoWorkEventArgs e)
{
    while(!e.Cancel)
    {
        // do something
    }

    _resetEvent.Set(); // signal that worker is done
}

Outras dicas

Há um problema com este resposta. As necessidades de interface do usuário para continuar a processar mensagens enquanto você está esperando, caso contrário não irá repintar, que vai ser um problema se o seu trabalho de fundo leva um longo tempo para responder ao pedido de cancelamento.

A segunda falha é que _resetEvent.Set() nunca será chamado se o segmento de trabalho gera uma exceção - deixando o segmento principal esperando indefinidamente -. No entanto esta falha poderia ser facilmente corrigido com um try / finally bloquear

Uma maneira de fazer isso é para exibir um diálogo modal que tem um temporizador que repetidamente verifica se o trabalhador fundo tem trabalho terminado (ou cancelamento acabado no seu caso). Uma vez que o trabalho de fundo terminou, os retornos de diálogo modais controlar a sua aplicação. O usuário não pode interagir com a interface do usuário até que isso aconteça.

Outro método (supondo que você tem um máximo de janela de um modal aberta) é definir ActiveForm.Enabled = false, então laço na Aplicação, DoEvents até que o trabalhador fundo tem cancelamento acabado, após o qual você pode definir ActiveForm.Enabled = true novamente.

Quase todos você está confuso com a pergunta, e não estão entendendo como um trabalhador é usado.

Considere um manipulador de eventos RunWorkerComplete:

private void OnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (!e.Cancelled)
    {
        rocketOnPad = false;
        label1.Text = "Rocket launch complete.";
    }
    else
    {
        rocketOnPad = true;
        label1.Text = "Rocket launch aborted.";
    }
    worker = null;
}

E tudo é bom.

Agora vem uma situação onde as necessidades de chamadas para abortar a contagem regressiva, porque eles precisam para executar um auto-destruição de emergência do foguete.

private void BlowUpRocket()
{
    if (worker != null)
    {
        worker.CancelAsync();
        WaitForWorkerToFinish(worker);
        worker = null;
    }

    StartClaxon();
    SelfDestruct();
}

E há também uma situação onde precisamos abrir os portões de acesso ao foguete, mas não ao mesmo tempo fazendo uma contagem regressiva:

private void OpenAccessGates()
{
    if (worker != null)
    {
        worker.CancelAsync();
        WaitForWorkerToFinish(worker);
        worker = null;
    }

    if (!rocketOnPad)
        DisengageAllGateLatches();
}

E, finalmente, precisamos de-abastecer o foguete, mas isso não é permitido durante uma contagem regressiva:

private void DrainRocket()
{
    if (worker != null)
    {
        worker.CancelAsync();
        WaitForWorkerToFinish(worker);
        worker = null;
    }

    if (rocketOnPad)
        OpenFuelValves();
}

Sem a capacidade de esperar por um trabalhador de cancelar, temos de mover todos os três métodos para o RunWorkerCompletedEvent:

private void OnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (!e.Cancelled)
    {
        rocketOnPad = false;
        label1.Text = "Rocket launch complete.";
    }
    else
    {
        rocketOnPad = true;
        label1.Text = "Rocket launch aborted.";
    }
    worker = null;

    if (delayedBlowUpRocket)
        BlowUpRocket();
    else if (delayedOpenAccessGates)
        OpenAccessGates();
    else if (delayedDrainRocket)
        DrainRocket();
}

private void BlowUpRocket()
{
    if (worker != null)
    {
        delayedBlowUpRocket = true;
        worker.CancelAsync();
        return;
    }

    StartClaxon();
    SelfDestruct();
}

private void OpenAccessGates()
{
    if (worker != null)
    {
        delayedOpenAccessGates = true;
        worker.CancelAsync();
        return;
    }

    if (!rocketOnPad)
        DisengageAllGateLatches();
}

private void DrainRocket()
{
    if (worker != null)
    {
        delayedDrainRocket = true;
        worker.CancelAsync();
        return;
    }

    if (rocketOnPad)
        OpenFuelValves();
}

Agora, eu poderia escrever meu código assim, mas eu não só vou. Eu não me importo, eu não sou apenas.

Você pode verificar na RunWorkerCompletedEventArgs na RunWorkerCompletedEventHandler para ver o que o estado era. Sucesso, cancelado ou um erro.

private void RunWorkerCompletedEventHandler(sender object, RunWorkerCompletedEventArgs e)
{
    if(e.Cancelled)
    {
        Console.WriteLine("The worker was cancelled.");
    }
}

Atualizar : Para ver se o trabalhador tem chamado .CancelAsync () usando o seguinte:

if (_worker.CancellationPending)
{
    Console.WriteLine("Cancellation is pending, no need to call CancelAsync again");
}

Você não espera para o trabalho de fundo para ser concluído. Que praticamente derrota o propósito de lançar um segmento separado. Em vez disso, você deve deixar seu acabamento método, e mover qualquer código que depende de conclusão para um lugar diferente. Você deixa o trabalhador dizer-lhe quando ele é feito e chamar qualquer código restante depois.

Se você quiser espera de algo para uso completo uma construção de segmentação diferente que fornece um WaitHandle.

Por que você não pode simplesmente amarrar o evento BackgroundWorker.RunWorkerCompleted. É uma chamada de retorno que irão "ocorrer quando a operação de fundo ter sido concluída, foi cancelada, ou apresentou uma exceção."

Eu não entendo por que você iria querer esperar por um BackgroundWorker a completa; ele realmente parece ser o oposto exato da motivação para a classe.

No entanto, você pode começar cada método com uma chamada para worker.IsBusy e tê-los sair se estiver em execução.

só quero dizer que eu vim aqui porque eu preciso de um trabalho de fundo que esperar enquanto eu estava correndo um processo assíncrono, enquanto em um loop, a minha correção foi a maneira mais fácil do que todas essas outras coisas ^^

foreach(DataRow rw in dt.Rows)
{
     //loop code
     while(!backgroundWorker1.IsBusy)
     {
         backgroundWorker1.RunWorkerAsync();
     }
}

Só percebi que eu iria partilhar porque é onde eu acabei enquanto procura uma solução. Além disso, este é meu primeiro post sobre estouro de pilha por isso, se o seu mau ou qualquer coisa que eu adoraria críticos! :)

Hm, talvez eu não estou recebendo a pergunta certa.

O backgroundworker chama o evento WorkerCompleted uma vez que seu 'workermethod' (o método / função / sub que manipula o backgroundworker.doWork-evento ) é concluído por isso não há necessidade de verificar se o BW ainda está em execução. Se você quer parar seu trabalho verificar o cancelamento pendente propriedade dentro do seu 'método de trabalhador'.

O fluxo de trabalho de um objeto BackgroundWorker basicamente exige que você manipular o evento RunWorkerCompleted para ambos os casos de execução e usar o cancelamento de usuários normais. É por isso que a propriedade RunWorkerCompletedEventArgs.Cancelled existe. Basicamente, fazendo isso corretamente exige que você considere seu método Cancelar para ser um método assíncrono em si.

Aqui está um exemplo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.ComponentModel;

namespace WindowsFormsApplication1
{
    public class AsyncForm : Form
    {
        private Button _startButton;
        private Label _statusLabel;
        private Button _stopButton;
        private MyWorker _worker;

        public AsyncForm()
        {
            var layoutPanel = new TableLayoutPanel();
            layoutPanel.Dock = DockStyle.Fill;
            layoutPanel.ColumnStyles.Add(new ColumnStyle());
            layoutPanel.ColumnStyles.Add(new ColumnStyle());
            layoutPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
            layoutPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));

            _statusLabel = new Label();
            _statusLabel.Text = "Idle.";
            layoutPanel.Controls.Add(_statusLabel, 0, 0);

            _startButton = new Button();
            _startButton.Text = "Start";
            _startButton.Click += HandleStartButton;
            layoutPanel.Controls.Add(_startButton, 0, 1);

            _stopButton = new Button();
            _stopButton.Enabled = false;
            _stopButton.Text = "Stop";
            _stopButton.Click += HandleStopButton;
            layoutPanel.Controls.Add(_stopButton, 1, 1);

            this.Controls.Add(layoutPanel);
        }

        private void HandleStartButton(object sender, EventArgs e)
        {
            _stopButton.Enabled = true;
            _startButton.Enabled = false;

            _worker = new MyWorker() { WorkerSupportsCancellation = true };
            _worker.RunWorkerCompleted += HandleWorkerCompleted;
            _worker.RunWorkerAsync();

            _statusLabel.Text = "Running...";
        }

        private void HandleStopButton(object sender, EventArgs e)
        {
            _worker.CancelAsync();
            _statusLabel.Text = "Cancelling...";
        }

        private void HandleWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Cancelled)
            {
                _statusLabel.Text = "Cancelled!";
            }
            else
            {
                _statusLabel.Text = "Completed.";
            }

            _stopButton.Enabled = false;
            _startButton.Enabled = true;
        }

    }

    public class MyWorker : BackgroundWorker
    {
        protected override void OnDoWork(DoWorkEventArgs e)
        {
            base.OnDoWork(e);

            for (int i = 0; i < 10; i++)
            {
                System.Threading.Thread.Sleep(500);

                if (this.CancellationPending)
                {
                    e.Cancel = true;
                    e.Result = false;
                    return;
                }
            }

            e.Result = true;
        }
    }
}

Se você realmente não quer que seu método para sair, eu sugiro colocar uma bandeira como um AutoResetEvent em um BackgroundWorker derivada, em seguida, substituir OnRunWorkerCompleted para definir o sinalizador. Ele ainda é meio que kludgy; Eu recomendo tratar o evento como um método assíncrono cancelar e fazer o que ele está fazendo atualmente no manipulador RunWorkerCompleted.

Estou um pouco atrasado para a festa aqui (cerca de 4 anos), mas que sobre a criação de uma linha assíncrona que pode lidar com um loop ocupado sem bloquear a interface do usuário, em seguida, ter o retorno de chamada de esse segmento ser a confirmação de que o BackgroundWorker tem cancelamento acabado?

Algo parecido com isto:

class Test : Form
{
    private BackgroundWorker MyWorker = new BackgroundWorker();

    public Test() {
        MyWorker.DoWork += new DoWorkEventHandler(MyWorker_DoWork);
    }

    void MyWorker_DoWork(object sender, DoWorkEventArgs e) {
        for (int i = 0; i < 100; i++) {
            //Do stuff here
            System.Threading.Thread.Sleep((new Random()).Next(0, 1000));  //WARN: Artificial latency here
            if (MyWorker.CancellationPending) { return; } //Bail out if MyWorker is cancelled
        }
    }

    public void CancelWorker() {
        if (MyWorker != null && MyWorker.IsBusy) {
            MyWorker.CancelAsync();
            System.Threading.ThreadStart WaitThread = new System.Threading.ThreadStart(delegate() {
                while (MyWorker.IsBusy) {
                    System.Threading.Thread.Sleep(100);
                }
            });
            WaitThread.BeginInvoke(a => {
                Invoke((MethodInvoker)delegate() { //Invoke your StuffAfterCancellation call back onto the UI thread
                    StuffAfterCancellation();
                });
            }, null);
        } else {
            StuffAfterCancellation();
        }
    }

    private void StuffAfterCancellation() {
        //Things to do after MyWorker is cancelled
    }
}

Em essência o que isso faz é fogo fora de um outro segmento a ser executado em segundo plano que apenas espera nele do laço ocupado para ver se o MyWorker foi concluída. Uma vez MyWorker terminou cancelando o fio vai sair e nós podemos usá-lo de AsyncCallback para executar qualquer método que precisamos seguir o cancelamento de sucesso - que vai funcionar como um pseudo-evento. Uma vez que este é separado do segmento interface do usuário não vai bloquear a interface do usuário enquanto aguardamos MyWorker ao fim cancelamento. Se sua intenção é realmente para bloquear e esperar a cancelar então este é inútil para você, mas se você só quer esperar para que você possa iniciar outro processo, em seguida, isso funciona muito bem.

Eu sei que é muito tarde (5 anos), mas o que você está procurando é usar uma linha e uma SynchronizationContext . Você vai ter que organizar UI chama de volta para o segmento UI "à mão", em vez de deixar o quadro fazê-lo auto-magicamente.

Isso permite que você use um segmento que você pode esperar por se for necessário.

Imports System.Net
Imports System.IO
Imports System.Text

Public Class Form1
   Dim f As New Windows.Forms.Form
  Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
   BackgroundWorker1.WorkerReportsProgress = True
    BackgroundWorker1.RunWorkerAsync()
    Dim l As New Label
    l.Text = "Please Wait"
    f.Controls.Add(l)
    l.Dock = DockStyle.Fill
    f.StartPosition = FormStartPosition.CenterScreen
    f.FormBorderStyle = Windows.Forms.FormBorderStyle.None
    While BackgroundWorker1.IsBusy
        f.ShowDialog()
    End While
End Sub




Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork

    Dim i As Integer
    For i = 1 To 5
        Threading.Thread.Sleep(5000)
        BackgroundWorker1.ReportProgress((i / 5) * 100)
    Next
End Sub

Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
    Me.Text = e.ProgressPercentage

End Sub

 Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted

    f.Close()

End Sub

End Class

A solução de Fredrik Kalseth para este problema é o melhor que eu encontrei até agora. Outras soluções usam Application.DoEvent() que podem causar problemas ou simplesmente não funcionam. Deixe-me lançou sua solução em uma classe reutilizável. Desde BackgroundWorker não é selada, podemos derivar nossa classe a partir dele:

public class BackgroundWorkerEx : BackgroundWorker
{
    private AutoResetEvent _resetEvent = new AutoResetEvent(false);
    private bool _resetting, _started;
    private object _lockObject = new object();

    public void CancelSync()
    {
        bool doReset = false;
        lock (_lockObject) {
            if (_started && !_resetting) {
                _resetting = true;
                doReset = true;
            }
        }
        if (doReset) {
            CancelAsync();
            _resetEvent.WaitOne();
            lock (_lockObject) {
                _started = false;
                _resetting = false;
            }
        }
    }

    protected override void OnDoWork(DoWorkEventArgs e)
    {
        lock (_lockObject) {
            _resetting = false;
            _started = true;
            _resetEvent.Reset();
        }
        try {
            base.OnDoWork(e);
        } finally {
            _resetEvent.Set();
        }
    }
}

Com bandeiras e travamento adequado, nós temos certeza que _resetEvent.WaitOne() realmente só é chamado se algum trabalho foi iniciado, caso contrário _resetEvent.Set(); poderia nunca foi chamado!

O try-finally assegura que _resetEvent.Set(); será chamado, mesmo se uma exceção deve ocorrer em nosso DoWork manipulador. Caso contrário, o aplicativo poderia congelar para sempre ao chamar CancelSync!

Nós iria utilizá-lo como este:

BackgroundWorkerEx _worker;

void StartWork()
{
    StopWork();
    _worker = new BackgroundWorkerEx { 
        WorkerSupportsCancellation = true,
        WorkerReportsProgress = true
    };
    _worker.DoWork += Worker_DoWork;
    _worker.ProgressChanged += Worker_ProgressChanged;
}

void StopWork()
{
    if (_worker != null) {
        _worker.CancelSync(); // Use our new method.
    }
}

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
    for (int i = 1; i <= 20; i++) {
        if (worker.CancellationPending) {
            e.Cancel = true;
            break;
        } else {
            // Simulate a time consuming operation.
            System.Threading.Thread.Sleep(500);
            worker.ReportProgress(5 * i);
        }
    }
}

private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    progressLabel.Text = e.ProgressPercentage.ToString() + "%";
}

Você também pode adicionar um manipulador para o evento RunWorkerCompleted como mostrado aqui:
BackgroundWorker Classe < em> (documentação Microsoft) .

fechar o formulário fecha o meu arquivo de log aberto. Meu trabalho de fundo escreve que logfile, então não posso deixar terminar MainWin_FormClosing() até que meus fundo termina trabalhadores. Se eu não esperar para o meu trabalho de fundo para terminar, exceções acontecer.

Por que isso é tão difícil?

Um simples obras Thread.Sleep(1500), mas atrasa o desligamento (se muito), ou faz com que exceções (se muito curta).

Para desligar logo após o termina de trabalho de fundo, basta usar uma variável. Isso está funcionando para mim:

private volatile bool bwRunning = false;

...

private void MainWin_FormClosing(Object sender, FormClosingEventArgs e)
{
    ... // Clean house as-needed.

    bwInstance.CancelAsync();  // Flag background worker to stop.
    while (bwRunning)
        Thread.Sleep(100);  // Wait for background worker to stop.
}  // (The form really gets closed now.)

...

private void bwBody(object sender, DoWorkEventArgs e)
{
    bwRunning = true;

    BackgroundWorker bw = sender as BackgroundWorker;

    ... // Set up (open logfile, etc.)

    for (; ; )  // infinite loop
    {
        ...
        if (bw.CancellationPending) break;
        ...
    } 

    ... // Tear down (close logfile, etc.)

    bwRunning = false;
}  // (bwInstance dies now.)

Você pode piggy back off do evento RunWorkerCompleted. Mesmo que você já tenha adicionado um manipulador de eventos para _worker, você pode adicionar outro um que irá executar na ordem em que foram adicionados.

public class DoesStuff
{
    BackgroundWorker _worker = new BackgroundWorker();

    ...

    public void CancelDoingStuff()
    {
        _worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler((sender, e) => 
        {
            // do whatever you want to do when the cancel completes in here!
        });
        _worker.CancelAsync();
    }
}

Isto pode ser útil se você tiver várias razões pelas quais um cancelamento pode ocorrer, tornando a lógica de um único manipulador RunWorkerCompleted mais complicado do que você quer. Por exemplo, o cancelamento quando um usuário tenta fechar o formulário:

void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    if (_worker != null)
    {
        _worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler((sender, e) => this.Close());
        _worker.CancelAsync();
        e.Cancel = true;
    }
}

Eu uso o método async e await que esperar para o trabalhador terminar o seu trabalho:

    public async Task StopAsync()
    {
        _worker.CancelAsync();

        while (_isBusy)
            await Task.Delay(1);
    }

e no método DoWork:

    public async Task DoWork()
    {
        _isBusy = true;
        while (!_worker.CancellationPending)
        {
            // Do something.
        }
        _isBusy = false;
    }

Você também pode encapsular o circuito while em DoWork com try ... catch ao conjunto _isBusy é false em exceção. Ou, simplesmente, verificar _worker.IsBusy na StopAsync enquanto loop.

Aqui está um exemplo de total aplicação:

class MyBackgroundWorker
{
    private BackgroundWorker _worker;
    private bool _isBusy;

    public void Start()
    {
        if (_isBusy)
            throw new InvalidOperationException("Cannot start as a background worker is already running.");

        InitialiseWorker();
        _worker.RunWorkerAsync();
    }

    public async Task StopAsync()
    {
        if (!_isBusy)
            throw new InvalidOperationException("Cannot stop as there is no running background worker.");

        _worker.CancelAsync();

        while (_isBusy)
            await Task.Delay(1);

        _worker.Dispose();
    }

    private void InitialiseWorker()
    {
        _worker = new BackgroundWorker
        {
            WorkerSupportsCancellation = true
        };
        _worker.DoWork += WorkerDoWork;
    }

    private void WorkerDoWork(object sender, DoWorkEventArgs e)
    {
        _isBusy = true;
        try
        {
            while (!_worker.CancellationPending)
            {
                // Do something.
            }
        }
        catch
        {
            _isBusy = false;
            throw;
        }

        _isBusy = false;
    }
}

Para parar o trabalhador e esperar por ele vai até o final:

await myBackgroundWorker.StopAsync();

Os problemas com este método são:

  1. Você tem que usar assíncrono métodos todo o caminho.
  2. aguardam Task.Delay é impreciso. No meu PC, Task.Delay (1), na verdade, espera ~ 20ms.

oh homem, alguns deles têm obtido ridiculamente complexa. tudo o que você precisa fazer é verificar a propriedade BackgroundWorker.CancellationPending dentro do manipulador de DoWork. você pode verificá-lo a qualquer momento. uma vez que está pendente, definir e.Cancel = True e fiança do método.

// método aqui Worker_DoWork private void (object sender, DoWorkEventArgs e) { BackgroundWorker bw = (remetente como BackgroundWorker);

// do stuff

if(bw.CancellationPending)
{
    e.Cancel = True;
    return;
}

// do other stuff

}

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top