Como esperar que um BackgroundWorker cancele?

125

Considere um método hipotético 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 alguém pode esperar que um BackgroundWorker seja executado?


No passado, as pessoas tentaram:

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

Mas esses bloqueios , porque IsBusynão são limpos até que o RunWorkerCompletedevento seja tratado e esse evento não pode ser tratado até que o aplicativo fique ocioso. O aplicativo não ficará ocioso até que o trabalhador termine. (Além disso, é um loop ocupado - nojento.)

Outros adicionaram sugestões de críticas a:

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

O problema é que isso faz Application.DoEvents()com que as mensagens atualmente na fila sejam processadas, o que causa problemas de reentrada (o .NET não é reentrante).

Espero usar alguma solução que envolva objetos de sincronização de eventos, onde o código aguarda um evento - RunWorkerCompleteddefinido pelos manipuladores de eventos 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 ao impasse: o manipulador de eventos não pode ser executado até que o aplicativo fique ocioso e o aplicativo não ficará ocioso porque está aguardando um Evento.

Então, como você pode esperar que um BackgroundWorker termine?


Atualizar As pessoas parecem estar confusas com esta pergunta. Eles parecem pensar que vou usar o BackgroundWorker como:

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

Não é isso, não é o que estou fazendo e não é o que está sendo perguntado aqui. Se fosse esse o caso, não faria sentido usar um trabalhador em segundo plano.

Ian Boyd
fonte

Respostas:

130

Se eu entendi direito o seu requisito, você poderia fazer algo assim (código não testado, mas mostra a ideia 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
}
Fredrik Kalseth
fonte
7
Isso bloqueará a interface do usuário (por exemplo, sem novas alterações) se o trabalhador em segundo plano demorar muito para cancelar. É melhor usar um dos seguintes itens para interromper a interação com a interface do usuário: stackoverflow.com/questions/123661/…
Joe
1
Marque com +1 o que o médico pediu ... embora eu concorde com @ Joe se o pedido de cancelamento demorar mais de um segundo.
30139 dotjoe
O que acontece quando o CancelAsync é tratado antes do WaitOne? Ou o botão Cancelar funciona apenas uma vez.
CodingBarfield
6
Eu precisava verificar o ((BackgroundWorker)sender).CancellationPendingpara obter o evento cancel
Luuk
1
Conforme mencionado por Luuk, não é a propriedade Cancel que deve ser verificada, mas CancellationPending. Em segundo lugar, como menciona Joel Coehoorn, esperar o término do thread anula o propósito de usar um thread em primeiro lugar.
Captain Sensible
15

Há um problema com esta resposta. A interface do usuário precisa continuar processando as mensagens enquanto você aguarda; caso contrário, não será redesenhada, o que será um problema se o trabalhador em segundo plano demorar muito para responder à solicitação de cancelamento.

Uma segunda falha é que _resetEvent.Set()nunca será chamada se o segmento de trabalho gerar uma exceção - deixando o segmento principal aguardando indefinidamente - no entanto, essa falha pode ser facilmente corrigida com um bloco try / finalmente.

Uma maneira de fazer isso é exibir uma caixa de diálogo modal que possui um timer que verifica repetidamente se o trabalhador em segundo plano concluiu o trabalho (ou o cancelamento no seu caso). Depois que o trabalhador em segundo plano terminar, a caixa de diálogo modal retornará o controle ao seu aplicativo. O usuário não pode interagir com a interface do usuário até que isso aconteça.

Outro método (supondo que você tenha no máximo uma janela sem janela aberta) é definir ActiveForm.Enabled = false e fazer loop em Application, DoEvents até que o trabalhador em segundo plano termine o cancelamento, após o qual você pode definir ActiveForm.Enabled = true novamente.

Joe
fonte
5
Isso pode ser um problema, mas é fundamentalmente aceito como parte da pergunta "Como aguardar o cancelamento de um BackgroundWorker". Esperar significa esperar, você não faz mais nada. Isso também inclui o processamento de mensagens. Se eu não quisesse esperar pelo trabalhador em segundo plano, você poderia simplesmente chamar .CancelAsync. Mas esse não é o requisito de design aqui.
11119 Ian Boyd
1
+1 para apontar o valor do método CancelAsync, versos aguardando pelo trabalhador em segundo plano.
11119 Ian Boyd
10

Quase todos vocês estão confusos 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 está bem.

Agora chega uma situação em que o chamador precisa interromper a contagem regressiva porque precisa executar uma autodestruiçã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 em que precisamos abrir os portões de acesso ao foguete, mas não durante a contagem regressiva:

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

    if (!rocketOnPad)
        DisengageAllGateLatches();
}

E, finalmente, precisamos 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 aguardar o cancelamento de um trabalhador, devemos 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 não vou. Eu não ligo, simplesmente não sou.

Ian Boyd
fonte
18
Onde está o método WaitForWorkerToFinish? algum código fonte completo?
113912 Kiquenet
4

Você pode verificar o RunWorkerCompletedEventArgs no RunWorkerCompletedEventHandler para ver qual era o status. Sucesso, cancelado ou um erro.

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

Atualização : para ver se seu trabalhador chamou .CancelAsync () usando isto:

if (_worker.CancellationPending)
{
    Console.WriteLine("Cancellation is pending, no need to call CancelAsync again");
}
Seb Nilsson
fonte
2
O requisito é que CancelDoingStuff () não possa retornar até que o trabalhador seja concluído. Verificar como foi concluído realmente não é interessante.
Ian Boyd
Então você terá que criar um evento. Realmente não tem nada a ver com o BackgroundWorker especificamente, você só precisa implementar um evento, ouvi-lo e disparar quando terminar. E RunWorkerCompletedEventHandler é quando está pronto. Dispare outro evento.
Seb Nilsson
4

Vocês não espera o trabalhador em segundo plano concluir. Isso praticamente derrota o propósito de lançar um thread separado. Em vez disso, você deve deixar seu método terminar e mover qualquer código que dependa da conclusão para um local diferente. Você permite que o funcionário avise quando terminar e chame qualquer código restante.

Se você quiser esperar que algo seja concluído, use uma construção de encadeamento diferente que forneça um WaitHandle.

Joel Coehoorn
fonte
1
Você pode sugerir um? Um trabalhador em segundo plano parece ser a única construção de encadeamento que pode enviar notificações para o encadeamento que criou o objeto BackgroundWorker.
Ian Boyd
O trabalhador em segundo plano é uma construção da interface do usuário . Ele usa apenas eventos que seu próprio código ainda precisa saber como chamar. Nada o impede de criar seus próprios delegados para isso.
Joel Coehoorn
3

Por que você não pode simplesmente se vincular ao evento BackgroundWorker.RunWorkerCompleted. É um retorno de chamada que "Ocorrerá quando a operação em segundo plano for concluída, cancelada ou tiver gerado uma exceção".

Rick Minerich
fonte
1
Porque a pessoa que está usando o objeto DoesStuff pediu para cancelar. Os recursos compartilhados que o objeto usa estão prestes a serem desconectados, removidos, descartados, fechados e é necessário saber que isso foi feito para que eu possa prosseguir.
Ian Boyd
1

Não entendo por que você deseja esperar que um BackgroundWorker seja concluído; realmente parece o exato oposto da motivação da turma.

No entanto, você pode iniciar todos os métodos com uma chamada para worker.IsBusy e fazê-los sair se estiver em execução.

Austin Salonen
fonte
Má escolha de palavras; Não estou esperando que seja concluído.
Ian Boyd
1

Só quero dizer que vim aqui porque preciso que um trabalhador em segundo plano aguarde enquanto executava um processo assíncrono enquanto em loop, minha correção foi bem mais fácil do que todas essas outras coisas ^^

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

Imaginei que compartilharia porque foi aqui que acabei pesquisando uma solução. Além disso, este é o meu primeiro post no estouro de pilha, por isso, se é ruim ou qualquer coisa que eu adoraria críticos! :)

Connor Williams
fonte
0

Hm, talvez eu não esteja acertando sua pergunta.

O trabalhador em segundo plano chama o evento WorkerCompleted quando seu 'workermethod' (o método / função / sub que lida com o backgroundworker.doWork-event ) é concluído, para que não seja necessário verificar se o BW ainda está em execução. Se você deseja interromper seu trabalhador, verifique a propriedade pendente de cancelamento dentro do seu 'método de trabalho'.

Stephan
fonte
Entendo que o trabalhador precisa monitorar a propriedade CancellationPending e sair o mais rápido possível. Mas como a pessoa de fora, que solicitou o cancelamento do trabalhador em segundo plano, espera que isso seja feito?
Ian Boyd
O evento WorkCompleted ainda será acionado.
Joel Coehoorn
"Mas como a pessoa de fora, que solicitou o cancelamento do trabalhador em segundo plano, espera que isso seja feito?"
Ian Boyd
Você espera que isso seja feito aguardando o evento WorkCompleted ser acionado. Se você deseja impedir que o usuário tenha um clique adequado em toda a sua GUI, use uma das soluções propostas por @Joe na resposta acima (desative o formulário ou mostre algo modal). Em outras palavras, deixe o loop inativo do sistema esperar por você; ele informará quando terminar (disparando o evento).
Geoff
0

O fluxo de trabalho de um BackgroundWorkerobjeto basicamente requer que você manipule o RunWorkerCompletedevento para casos de uso de execução normal e cancelamento de usuário. É por isso que a propriedade RunWorkerCompletedEventArgs.Cancelled existe. Basicamente, fazer isso corretamente exige que você considere o método Cancel como um método assíncrono.

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 deseja que seu método saia, sugiro colocar uma sinalização como uma AutoResetEventem uma derivada BackgroundWorkere substituir OnRunWorkerCompletedpara definir a sinalização. Ainda é meio que desajeitado; Eu recomendo tratar o evento cancel como um método assíncrono e fazer o que estiver fazendo no RunWorkerCompletedmanipulador.

OwenP
fonte
Mover o código para RunWorkerCompleted não está onde pertence e não é bonito.
Ian Boyd
0

Estou um pouco atrasado para a festa aqui (cerca de 4 anos), mas e quanto à configuração de um encadeamento assíncrono que pode lidar com um loop ocupado sem bloquear a interface do usuário, para que o retorno de chamada desse encadeamento seja a confirmação de que o BackgroundWorker concluiu o cancelamento ?

Algo assim:

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 é disparar outro encadeamento para executar em segundo plano, que apenas aguarda seu loop ocupado para ver se o processo MyWorkerfoi concluído. Uma vez MyWorkerterminado o cancelamento, o encadeamento sairá e podemos usá-lo AsyncCallbackpara executar qualquer método necessário para seguir o cancelamento bem-sucedido - ele funcionará como um evento psuedo. Como isso é separado do segmento da interface do usuário, ele não bloqueará a interface do usuário enquanto aguardamos o MyWorkertérmino do cancelamento. Se sua intenção é realmente travar e aguardar o cancelamento, isso é inútil para você, mas se você quiser apenas esperar para poder iniciar outro processo, isso funcionará perfeitamente.

Infotekka
fonte
0

Eu sei que isso é muito tarde (5 anos), mas o que você está procurando é usar um Thread e um SynchronizationContext . Você precisará reunir as chamadas da interface do usuário de volta para o thread da interface do usuário "manualmente", em vez de permitir que o Framework faça isso automaticamente.

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

Tom Padilla
fonte
0
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
Nitesh
fonte
Qual a sua pergunta? Em Então você tem que ser específico ao fazer perguntas
Alma Do
@Esta é a resposta, não uma pergunta.
Code Léver 28/08/13
0

A solução de Fredrik Kalseth para esse problema é a melhor que encontrei até agora. O uso de outras soluções Application.DoEvent()pode causar problemas ou simplesmente não funcionar. Deixe-me lançar sua solução em uma classe reutilizável. Como BackgroundWorkernão está selado, podemos derivar nossa classe:

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 sinalizadores e bloqueio adequado, garantimos que _resetEvent.WaitOne()realmente seja chamado apenas se algum trabalho tiver sido iniciado, caso contrário, _resetEvent.Set();nunca poderá ser chamado!

A tentativa finalmente garante que _resetEvent.Set();isso será chamado, mesmo se uma exceção ocorrer em nosso manipulador de DoWork. Caso contrário, o aplicativo poderá congelar para sempre ao chamarCancelSync !

Nós o usaríamos assim:

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 ao RunWorkerCompletedevento, como mostrado aqui:
     BackgroundWorker Class (documentação da Microsoft) .

Olivier Jacot-Descombes
fonte
0

Fechar o formulário fecha meu arquivo de log aberto. Meu trabalhador em segundo plano grava esse arquivo de log, então não posso deixarMainWin_FormClosing() terminar até que meu trabalhador em segundo plano termine. Se eu não esperar o término do trabalho em segundo plano, ocorrerão exceções.

Por que isso é tão difícil?

Um simples Thread.Sleep(1500)funciona, mas atrasa o desligamento (se for muito longo) ou causa exceções (se for muito curto).

Para desligar logo após o trabalhador em segundo plano terminar, 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.)
A876
fonte
0

Você pode recuar no evento RunWorkerCompleted. Mesmo se você já adicionou um manipulador de eventos para _worker, pode adicionar outro e eles serão executados 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();
    }
}

isso pode ser útil se você tiver vários motivos pelos quais um cancelamento pode ocorrer, tornando a lógica de um único manipulador RunWorkerCompleted mais complicada do que você deseja. Por exemplo, cancelando 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;
    }
}
Shawn Rubie
fonte
0

Uso o asyncmétodo e awaitaguardo o trabalhador terminar seu trabalho:

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

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

e no DoWorkmétodo:

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

Você também pode encapsular o whileloop DoWorkcom try ... catchpara definir _isBusycomo falseexceção. Ou simplesmente faça check- _worker.IsBusyin no StopAsyncloop while.

Aqui está um exemplo de implementação completa:

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 que ele seja executado até o fim:

await myBackgroundWorker.StopAsync();

Os problemas com este método são:

  1. Você precisa usar métodos assíncronos o tempo todo.
  2. aguardar Task.Delay é impreciso. No meu PC, o Task.Delay (1) realmente espera ~ 20ms.
Anthony
fonte
-2

Oh, cara, algumas delas ficaram ridiculamente complexas. tudo o que você precisa fazer é verificar a propriedade BackgroundWorker.CancellationPending dentro do manipulador do DoWork. você pode verificá-lo a qualquer momento. uma vez pendente, defina e.Cancel = True e saia do método.

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

// do stuff

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

// do other stuff

}


fonte
1
E com esta solução, como a pessoa que ligou para CancelAsync é forçada a esperar pelo cancelamento do trabalhador em segundo plano?
58630 Ian Boyd