4

Suppose you have a search textbox and have a search algorithm attached to the TextChanged event, that runs with a BackgroundWorker. If there comes a new character in the textbox, i need to cancel the previous search and run it again.

I tried using events in between the main thread and the bgw, from this previous question, but I still get the error "currently busy and cannot run multiple tasks concurrently"

    BackgroundWorker bgw_Search = new BackgroundWorker();
    bgw_Search.DoWork += new DoWorkEventHandler(bgw_Search_DoWork);

    private AutoResetEvent _resetEvent = new AutoResetEvent(false);

    private void txtSearch_TextChanged(object sender, EventArgs e)
    {
        SearchWithBgw();
    }

    private void SearchWithBgw()
    {
        // cancel previous search
        if (bgw_Search.IsBusy)
        {
            bgw_Search.CancelAsync();

            // wait for the bgw to finish, so it can be reused.
            _resetEvent.WaitOne(); // will block until _resetEvent.Set() call made
        }

        // start new search
        bgw_Search.RunWorkerAsync();   // error "cannot run multiple tasks concurrently"
    }

    void bgw_Search_DoWork(object sender, DoWorkEventArgs e)
    {
        Search(txtSearch.Text, e);
    }

    private void Search(string aQuery, DoWorkEventArgs e)
    {
        int i = 1;            
        while (i < 3)             // simulating search processing...
        {
            Thread.Sleep(1000);                           
            i++;

            if (bgw_Search.CancellationPending)
            {
                _resetEvent.Set(); // signal that worker is done
                e.Cancel = true;
                return;
            }
        }
    }

EDIT To reflect answers. Don´t reuse the BackgroundWorker, create a new one:

    private void SearchWithBgw()
    {   
        if (bgw_Search.IsBusy)
        {
            bgw_Search.CancelAsync();
            _resetEvent.WaitOne(); // will block until _resetEvent.Set() call made

            bgw_Search = new BackgroundWorker();
            bgw_Search.WorkerSupportsCancellation = true;
            bgw_Search.DoWork += new DoWorkEventHandler(bgw_Search_DoWork);
        }

        bgw_Search.RunWorkerAsync();        
    }
Community
  • 1
  • 1
Carlos Torres
  • 419
  • 2
  • 9
  • 19

4 Answers4

8

When the _resetEvent.WaitOne() call completes, the worker thread isn't actually done. It is busy returning from DoWork() and waiting for an opportunity to run the RunWorkerCompleted event, if any. That takes time.

There is no reliable way to ensure the BGW is completed in a synchronous way. Blocking on IsBusy or waiting for the RunWorkerCompleted event to run is going to cause deadlock. If you really want to use only one bgw then you'll have to queue the requests. Or just don't sweat the small stuff and allocate another bgw. They cost very little.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
2
  • Do not reuse a Backgroundworker. It is a cheap resource, it is not a Thread.
  • make sure your Bgw code stops, yours looks OK. The Bgw will release the Thread to the pool.
  • but in the mean time, create a new Task/Bgw for a new job.
  • You may want to unsubscribe your Completed event from the old Bgw.
H H
  • 263,252
  • 30
  • 330
  • 514
2

Create a new background worker if the old one exists.

private void SearchWithBgw()
{
    // cancel previous search
    if (bgw_Search.IsBusy)
    {
        bgw_Search.CancelAsync();

        // wait for the bgw to finish, so it can be reused.
        _resetEvent.WaitOne(); // will block until _resetEvent.Set() call made
        BackgroundWorker bgw_Search = new BackgroundWorker();
        bgw_Search.DoWork += new DoWorkEventHandler(bgw_Search_DoWork);


    }

    // start new search
    bgw_Search.RunWorkerAsync();   // error "cannot run multiple tasks concurrently"
}

Also I know you put fake code in, but you want to make sure you set _resetEvent when the code completes normally too.

Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • If I create a `new BackgroundWorker()` within this function, how can I cancel the previous one? Should I keep the workers in a list or something? Since the search function updates a control, I don´t want multiple workers updating the same control. – Carlos Torres Aug 12 '11 at 19:58
  • @Carlos, you cancel the old one and wait for it to single the _resetEvent before you create the new one. – Scott Chamberlain Aug 12 '11 at 20:12
1

I think you should consider not cancelling the background worker.

If you cancel requests and the user types faster than your server returns queries, he will not see suggestions until he is finished typing.

In interactive scenarios like this, It could be better to show responses that run behind with what the user's typing. Your user will know he can stop typing if the word he has in mind is your suggestions list.

This will be also better for your server when it is busy, because instead of many cancelled requests, who will cost something but that are ultimately not shown, there will be fewer requests whose response you actually use.

I ran into similar issues with (3d) rendering applications, where the beginner's mistake is to cancel and rerender on every mousemove. This lead to a lot of computation and little interactive feedback.

  • +1 Thanks, good point. I felt the need to cancel because I update GUI in the background work and I didn´t want multiple workers updating the same control. – Carlos Torres Aug 13 '11 at 14:43