如何正确等待BackgroundWorker完成?

40

观察以下代码片段:

var handler = GetTheRightHandler();
var bw = new BackgroundWorker();
bw.RunWorkerCompleted += OnAsyncOperationCompleted;
bw.DoWork += OnDoWorkLoadChildren;
bw.RunWorkerAsync(handler);

现在假设我想要等到bw完成工作。该如何正确地实现这个目标?

我的解决方案是:

bool finished = false;
var handler = GetTheRightHandler();
var bw = new BackgroundWorker();
bw.RunWorkerCompleted += (sender, args) =>
{
  OnAsyncOperationCompleted(sender, args);
  finished = true;
});
bw.DoWork += OnDoWorkLoadChildren;
bw.RunWorkerAsync(handler);
int timeout = N;
while (!finished && timeout > 0)
{
  Thread.Sleep(1000);
  --timeout;
}
if (!finished)
{
  throw new TimedoutException("bla bla bla");
}

但我不喜欢这种方式。

我考虑用一个同步事件来替换 finished 标志,在 RunWorkerCompleted 处理程序中将其设置并稍后阻塞它,而不是使用 while-sleep 循环。 但是很遗憾,这是错误的,因为代码可能在 WPF 或 WindowsForm 同步上下文中运行,在这种情况下,我会阻塞与 RunWorkerCompleted 处理程序运行的同一线程,这显然不是很明智的举动。

我想知道更好的解决方案。

谢谢。

编辑:

P.S.

  • 示例代码是故意编写的,以阐明我的问题。 我完全了解完成回调,但我想知道如何等待完成。 这就是我的问题。
  • 我了解 Thread.JoinDelegate.BeginInvokeThreadPool.QueueUserWorkItem 等等,但问题特别涉及到 BackgroundWorker

编辑2:

好吧,如果我解释一下场景,那么这会很容易。

我有一个单元测试方法,它调用一些异步代码,这些代码最终涉及一个 BackgroundWorker,我可以向其中传递完成处理程序。 所有的代码都是我的,因此我可以根据需要更改实现方式。但是,我不打算替换 BackgroundWorker,因为它会自动使用正确的同步上下文,所以当在 UI 线程上调用代码时,完成回调会在同一 UI 线程上调用,这非常好。

无论如何,单元测试方法可能会在 BW 完成工作之前结束,这不太好。 因此,我希望等待 BW 完成,并想知道最佳方法。

它还有更多组件,但整体情况大致如我刚才所描述的那样。


1
@Barfieldmv:这就是为什么AutoResetEvent是如此优雅的解决方案。您可以等待一个或多个任务,无论它们在哪个线程上运行。对于多个任务,只需使用静态方法AutoResetEvent.WaitAll(new[] {doneEvent1, doneEvent2})即可。 - JohannesH
@JohannesH 要能够等待多个事件,您必须确保它们被触发并得到处理。恐怕在 UI 线程上阻塞会阻塞 Windows 表单上的所有事件,以至于您可能永远等不到。如果我没记错的话,BackgroundWorker 在后台线程上处理其 DoWork 事件,并在主线程上触发 WorkCompleted 事件(如果主线程未被阻塞)。这进入了难以调试的多线程领域,至少对我来说是这样。 - CodingBarfield
1
我必须指出,RunWorkerCompleted 处理程序被阻塞的问题也存在于 Mark 原始的 while(!finished...){...} 解决方案中。因此,该解决方案将始终抛出 TimedoutException 异常。据我所知,由于 BackgroundWorker 的性质,只能等待 DoWork 处理程序完成,而无法等待 RunWorkerCompleted 处理程序完成。此外,无法保证作业已启动,但可以通过超时值解决此问题。 - JohannesH
@ JohannesH 实际上,当线程不是 UI 线程时,我的代码可以正常工作,这对于单元测试线程是正确的。但我有模拟 WPF 线程的单元测试,通过创建一个 Dispatcher 并在 Dispatcher 上下文中运行单元测试代码来欺骗代码,使其认为它是一个 UI 线程。在这种情况下,我没有尝试这种代码,而是采用另一种方法。如果有一个统一的解决方案,我很感兴趣。 - mark
@mark 在另一个后台工作线程中使用AutoResetEvent。请参见下面的我的答案 - KevinBui
显示剩余2条评论
10个回答

46

尝试使用AutoResetEvent类,就像这样:

var doneEvent = new AutoResetEvent(false);
var bw = new BackgroundWorker();

bw.DoWork += (sender, e) =>
{
  try
  {
    if (!e.Cancel)
    {
      // Do work
    }
  }
  finally
  {
    doneEvent.Set();
  }
};

bw.RunWorkerAsync();
doneEvent.WaitOne();

注意: 无论发生什么情况,你都应该确保调用doneEvent.Set()。此外,你可能想要为doneEvent.WaitOne()提供一个指定超时时间的参数。

注:这段代码基本上是Fredrik Kalseth类似问题的答案的复制。


你说得对,我的问题非常相似。不知何故我错过了Fredrik的问题。 - mark

20

如果要等待后台工作线程(单个或多个),请执行以下操作:

  1. Create a List of Background workers you have programatically created:

    private IList<BackgroundWorker> m_WorkersWithData = new List<BackgroundWorker>();
    
  2. Add the background worker in the list:

    BackgroundWorker worker = new BackgroundWorker();
    worker.DoWork += new DoWorkEventHandler(worker_DoWork);
    worker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged);
    worker.WorkerReportsProgress = true;
    m_WorkersWithData.Add(worker);
    worker.RunWorkerAsync();
    
  3. Use the following function to wait for all workers in the List:

    private void CheckAllThreadsHaveFinishedWorking()
    {
        bool hasAllThreadsFinished = false;
        while (!hasAllThreadsFinished)
        {
            hasAllThreadsFinished = (from worker in m_WorkersWithData
                                     where worker.IsBusy
                                     select worker).ToList().Count == 0;
            Application.DoEvents(); //This call is very important if you want to have a progress bar and want to update it
                                    //from the Progress event of the background worker.
            Thread.Sleep(1000);     //This call waits if the loop continues making sure that the CPU time gets freed before
                                    //re-checking.
        }
        m_WorkersWithData.Clear();  //After the loop exits clear the list of all background workers to release memory.
                                    //On the contrary you can also dispose your background workers.
    }
    

2
我忘了在这里提到的另一件事是,因为你正在使用 += 添加事件......当后台工作器完成执行时,你必须执行 -= ,否则后台工作器将无法释放。 - Azhar Khorasany
@Kiquenet:这不就是你要寻找的核心功能吗?在等待所有后台工作完成之前,您调用CheckAllThreadsHaveFinishedWorking()方法,以便程序继续运行。 - Azhar Khorasany

7

BackgroundWorker有一个完成事件。不要等待,从完成处理程序中调用剩余的代码路径。


11
我知道这一点,而代码片段展示了它。然而,问题是关于等待。 - mark

2

在循环中使用 Application.DoEvents() 来检查 backgrWorker.IsBusy 并不是一个好的方式。

我同意 @JohannesH 的观点,你应该使用 AutoResetEvent 作为一种优雅的解决方案。但是不要在 UI 线程中使用它,否则会导致主线程被阻塞;它应该来自另一个后台工作线程。

AutoResetEvent aevent = new AutoResetEvent(false);    
private void button1_Click(object sender, EventArgs e)
{
    bws = new BackgroundWorker();
    bws.DoWork += new DoWorkEventHandler(bw_work);
    bws.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_complete);
    bws.RunWorkerAsync();

    bwWaiting.DoWork += new DoWorkEventHandler(waiting_work);
    bwWaiting.RunWorkerCompleted += new RunWorkerCompletedEventHandler(waiting_complete);
    bwWaiting.RunWorkerAsync();
}

void bw_work(object sender, DoWorkEventArgs e)
{
    Thread.Sleep(2000);
}

void bw_complete(object sender, RunWorkerCompletedEventArgs e)
{
    Debug.WriteLine("complete " + bwThread.ToString());
    aevent.Set();
}
void waiting_work(object sender, DoWorkEventArgs e)
{
    aevent.WaitOne();
}

void waiting_complete(object sender, RunWorkerCompletedEventArgs e)
{
    Debug.WriteLine("complete waiting thread");
}

为什么Application.DoEvents()不是一个好的方法?它似乎工作得很好。 - OfficeAddinDev
什么是bwWaiting?只是另一个后台工作者吗? - Jake Gaston

2

VB.NET

While BackgroundWorker1.IsBusy()
    Windows.Forms.Application.DoEvents()
End While

你可以使用这个功能来连接多个事件。(伪代码如下)
download_file("filepath")

    While BackgroundWorker1.IsBusy()
       Windows.Forms.Application.DoEvents()
    End While
'Waits to install until the download is complete and lets other UI events function install_file("filepath")
While BackgroundWorker1.IsBusy()
    Windows.Forms.Application.DoEvents()
End While
'Waits for the install to complete before presenting the message box
msgbox("File Installed")

2

这个问题很老,但我认为作者没有得到他想要的答案。

这有点不好看,而且是用vb.NET编写的,但对我来说有效。

Private Sub MultiTaskingForThePoor()
    Try
        'Start background worker
        bgwAsyncTasks.RunWorkerAsync()
        'Do some other stuff here
        For i as integer = 0 to 100
            lblOutput.Text = cstr(i)
        Next

        'Wait for Background worker
        While bgwAsyncTasks.isBusy()
            Windows.Forms.Application.DoEvents()
        End While

        'Voila, we are back in sync
        lblOutput.Text = "Success!"
    Catch ex As Exception
        MsgBox("Oops!" & vbcrlf & ex.Message)
    End Try
End Sub

0

不太确定您所说的“等待”是什么意思。您是指希望在某项任务完成后(由BW完成),再执行其他操作吗? 像您平常一样使用bw.RunWorkerCompleted(为了可读性,可以使用单独的函数),在回调函数中执行下一步操作。 同时启动一个计时器,以确保任务不会花费过长时间。

var handler = GetTheRightHandler();
var bw = new BackgroundWorker();
bw.RunWorkerCompleted += (sender, args) =>
{
  OnAsyncOperationCompleted(sender, args);
});
bw.DoWork += OnDoWorkLoadChildren;
bw.RunWorkerAsync(handler);

Timer Clock=new Timer();
Clock.Interval=1000;
Clock.Start();
Clock.Tick+=new EventHandler(Timer_Tick);

public void Timer_Tick(object sender,EventArgs eArgs)
{   
    if (bw.WorkerSupportsCancellation == true)
    {
        bw.CancelAsync();
    }

    throw new TimedoutException("bla bla bla");
 }

在OnDoWorkLoadChildren中:

if ((worker.CancellationPending == true))
{
    e.Cancel = true;
    //return or something
}

0
我也在寻找一个合适的解决方案。我通过使用独占锁来解决等待问题。代码中的关键路径是向公共容器(这里是控制台)写入内容以及增加或减少工作线程。任何线程在写入此变量时都不应该干扰,否则计数就不能再保证了。
public class Program
{
    public static int worker = 0;
    public static object lockObject = 0;

    static void Main(string[] args)
    {

        BackgroundworkerTest backgroundworkerTest = new BackgroundworkerTest();
        backgroundworkerTest.WalkDir("C:\\");
        while (backgroundworkerTest.Worker > 0)
        {
            // Exclusive write on console
            lock (backgroundworkerTest.ExclusiveLock)
            {
                Console.CursorTop = 4; Console.CursorLeft = 1;
                var consoleOut = string.Format("Worker busy count={0}", backgroundworkerTest.Worker);
                Console.Write("{0}{1}", consoleOut, new string(' ', Console.WindowWidth-consoleOut.Length));
            }
        }
    }
}

public class BackgroundworkerTest
{
    private int worker = 0;
    public object ExclusiveLock = 0;

    public int Worker
    {
        get { return this.worker; }
    }

    public void WalkDir(string dir)
    {
        // Exclusive write on console
        lock (this.ExclusiveLock)
        {
            Console.CursorTop = 1; Console.CursorLeft = 1;
            var consoleOut = string.Format("Directory={0}", dir);
            Console.Write("{0}{1}", consoleOut, new string(' ', Console.WindowWidth*3 - consoleOut.Length));
        }

        var currentDir = new System.IO.DirectoryInfo(dir);
        DirectoryInfo[] directoryList = null;
        try
        {
            directoryList = currentDir.GetDirectories();
        }
        catch (UnauthorizedAccessException unauthorizedAccessException)
        {
            // No access to this directory, so let's leave
            return;
        }

        foreach (var directoryInfo in directoryList)
        {
            var bw = new BackgroundWorker();

            bw.RunWorkerCompleted += (sender, args) =>
            {
                // Make sure that this worker variable is not messed up
                lock (this.ExclusiveLock)
                {
                    worker--;
                }
            };

            DirectoryInfo info = directoryInfo;
            bw.DoWork += (sender, args) => this.WalkDir(info.FullName);

            lock (this.ExclusiveLock)
            {
                // Make sure that this worker variable is not messed up
                worker++;
            }
            bw.RunWorkerAsync();
        }
    }
}

0

我使用TasksBackgroundWorker来完成编程。

您可以创建任意数量的任务并将它们添加到任务列表中。 当添加一个任务时,工作者将启动;如果在工作者处于忙碌状态时添加任务,则会重新启动;一旦没有任务可执行,工作者将停止。

这将使您能够异步更新GUI,而不会冻结它。

对我来说,这个方案非常有效。

    // 'tasks' is simply List<Task> that includes events for adding objects
    private ObservableCollection<Task> tasks = new ObservableCollection<Task>();
    // this will asynchronously iterate through the list of tasks 
    private BackgroundWorker task_worker = new BackgroundWorker();

    public Form1()
    {
        InitializeComponent();
        // set up the event handlers
        tasks.CollectionChanged += tasks_CollectionChanged;
        task_worker.DoWork += task_worker_DoWork;
        task_worker.RunWorkerCompleted += task_worker_RunWorkerCompleted;
        task_worker.WorkerSupportsCancellation = true;

    }

    // ----------- worker events
    void task_worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (tasks.Count != 0)
        {
            task_worker.RunWorkerAsync();
        }
    }

    void task_worker_DoWork(object sender, DoWorkEventArgs e)
    {
        try
        {

            foreach (Task t in tasks)
            {
                t.RunSynchronously();
                tasks.Remove(t);
            }
        }
        catch
        {
            task_worker.CancelAsync();
        }
    }


    // ------------- task event
    // runs when a task is added to the list
    void tasks_CollectionChanged(object sender,
        System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        if (!task_worker.IsBusy)
        {
            task_worker.RunWorkerAsync();
        }
    }

现在你所需要做的就是创建一个新的任务并将其添加到列表中。它将按照被放置到列表中的顺序由工作线程运行。
Task t = new Task(() => {

        // do something here
    });

    tasks.Add(t);

-1
在OpenCV中存在WaitKey函数。它可以通过以下方式解决这个问题:
while (this->backgroundWorker1->IsBusy) {
    waitKey(10);
    std::cout << "Wait for background process: " << std::endl;
}
this->backgroundWorker1->RunWorkerAsync();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接