0

I have 2 winforms:

  • Form1
  • Form2

Form1 is the main form. Form1 opens Form2. In the Form2 Load event handler, a new background worker thread is started. When worker thread finishes, it will notify the UI thread to update Form2.

The question is, user can close Form2 while the worker thread is still running. So the Form2 may be gone by the time worker thread finishes. Then some Null Reference Exception happens when worker thread attempts to update Form2 UI.

I was planning to use a flag to indicate the Form2 existence. Every time updating the UI, the flag is checked to ensure Form2 exists. But this check-and-act pattern cannot handle the race coditions. Because the form may be closed after the check is pass but before the UI update actions are taken.

So is there some way to solve this issue?

Some code of the Form2:

private void StartComputeGraphWorker()
{// this runs on the UI thread.
    try
    {
        this.generationFinished = false;
        DisableAllControls(); //prevent user input while some background work is underway.
        StartShowProgressMarquee();
        ThreadStart d = new ThreadStart(WorkerStartWrapper);
        worker = new Thread(d);
        worker.IsBackground = true;
        worker.Start();
    }
    catch (Exception ex)
    {
        Logger.LogMessage(Logger.LogLevel.Error, ex.Message);
        EnableAllControls();
        StopShowProgressMarquee();
    }
}


private void NotifyUI(Boolean suceess)
{
    if (suceess)
    {
        // this is on the secondary illustration form. it may NOT exist by now.
        if (!this.formClosed)
        {//race conditions here
            this.Invoke(new MethodInvoker(ShowGraphDataInUIThread));
        }
        else//the form has been closed, we have no place to show the graph, just return.
        {
            return;
        }
    }
    else
    {
        // this logs to the main input form, it always exists.
        Logger.LogMessage(Logger.LogLevel.Warning, "Graph generation failed."); 
    }
}

private void WorkerStartWrapper()
{
    try
    {
        RenderGraphWorker();
        NotifyUI(true);
    }
    catch (Exception ex) // ThreadAbortException or Other Exceptions
    {
        Logger.LogMessage(Logger.LogLevel.Warning, ex.Message);
        NotifyUI(false);
    }
}

ADD 1

I checked below thread:

How to update the GUI from another thread in C#?

It's not exactly the same. My form can be gone. It's not just about cross-thread control updating.

With the BackgroundWorker approach, unsubscribing the RunWorkerCompleted event in the Form2 Closing event can solve my problem.

But I am still wondering if it is possible with Thread class.

smwikipedia
  • 61,609
  • 92
  • 309
  • 482
  • Preventing race conditions is a common situation where you need to catch a relevant exception and react accordingly. Same as catching IOException when trying to do file I/O. – Cody Gray - on strike Aug 29 '17 at 08:30
  • There are literally *hundreds* of duplicate questions. For the last 5 years the answer is "Don't use threads, use Tasks and `async/await`". That, and the `IProgres< T>` interface make the use of `Invoke` obsolete. Even in .NET 4.0 one can use Tasks to run code in the background and continue in the UI thread with `ContinueWith` – Panagiotis Kanavos Aug 29 '17 at 08:31
  • I know `asyn/await` offers a lot of help. I just want to know how to solve it with Thread. – smwikipedia Aug 29 '17 at 08:32
  • Why? Why use an obsolete technique that raises a *lot* of issues? Like the current problem for example. Besides, if you want to use an obsolete technique, why not use BackgroundWorker? It performs precisely what is shown here for pre-4.0 runtimes. – Panagiotis Kanavos Aug 29 '17 at 08:34
  • I tried BackgroundWorker, but enountered similar issue. – smwikipedia Aug 29 '17 at 08:36
  • Why did you assume BGW was broken? You are trying to use global state from multiple threads without any synchronization. *This* question is definitely a duplicate. The *actual* problem is the very existence of `formClosed` and that instead of raising an event or notification, the code is trying to update a form directly. The `ShowGraphDataInUIThread` is the one that should determine whether to show a notification or not – Panagiotis Kanavos Aug 29 '17 at 08:43
  • The background thread shouldn't know *anything* about the UI. It should send a notification or call a callback and let *that* decide where and how to display a notification. This is so fundamental that it's supported out of the bo x in .NET 4.5+ [as shown here](https://blogs.msdn.microsoft.com/dotnet/2012/06/06/async-in-4-5-enabling-progress-and-cancellation-in-async-apis/) – Panagiotis Kanavos Aug 29 '17 at 08:45
  • I already mentioned in my post that the `formClosed` won't work. I also tried writing various event handlers for the BackgroundWorker. The issue is, the form can be gone. In the post you mentioned, the form is sill there. – smwikipedia Aug 29 '17 at 08:47
  • As for BGW, it has a progress event already. There's no need to try and call the UI from inside the worker. Just call [ReportProgress(int,Object)](https://msdn.microsoft.com/en-us/library/a3zbdb1t(v=vs.110).aspx) or `ReportProgress(int)` and have the progress form subscribe to the ProgressChanged event – Panagiotis Kanavos Aug 29 '17 at 08:48
  • 2
    Forget about `formClosed`. *That* is the bug. You are trying to access the UI from inside the worker. Don't. That's what the event is there for. Use the event. Try the examples. They *work* for the past 15 years. That's how long BGW is there. If you *don't* want to receive the event, *unsubscribe* from it. Do it when you close the form – Panagiotis Kanavos Aug 29 '17 at 08:49
  • Thanks. I tried *unsubscribing* the `RunWorkerCompleted` event in the Form2 Closing event. It works. I just wondering if it is possible with the good old Thread? – smwikipedia Aug 29 '17 at 08:59

1 Answers1

0

Today, I rethink about the unsubscribing approach. It looks OK. But actually maybe not.

There's a race condition that the code may be running in the completed event handler while I am unsubscribing it. So unsubsribing it will not prevent it from manipulating a maybe-non-existing form.

I think I should still stick to the standard BGW paradigm and solve this issue other way.

In my scenario, user have 2 ways to cancel a BGW operation.

  • Click the Cancel button.
  • Close the form.

My current solution is:

If user click the Cacncel button, I will show some UI notification in the Cancel button click handler before invoking the bgw.CancelAsync(). Something like this:

this.label1.Text = "Operation Cancelled";
bgw.CancelAsync()

At this time, the UI is guarenteed to exist.

If user close the form, I just call the bgw.CancelAsync() in the form closing event handler. Then the BGW.DoWork() will poll and find this signal and stop execution. I need no UI notification to user here because this is an implicit intention of the user.

For both cancel scenarios, the BGW complete event handler contains NO UI manipulation for the cancelled result.

So to summarize, leave the BGW to complete its life-cycle.

smwikipedia
  • 61,609
  • 92
  • 309
  • 482
  • 1
    It is a fundamental race condition, the BGW will not stop instantaneously. It can easily Invoke a microsecond after the form closed. You must delay the closing until you are sure that the BGW finished running. https://stackoverflow.com/a/1732361/17034 – Hans Passant Sep 16 '17 at 13:51
  • @HansPassant Yes, my approach is still problemtaic. Because my other BGW complete ways will also modify the UI. The from can also be gone. Your approach is correct. If you post it as anwer, I will mark it. Thanks. – smwikipedia Sep 16 '17 at 13:56
  • @HansPassant I just find the `bgw.IsBusy` is already `false` when entering the `bgw.RunWorkerCompleted` event handler. So, even I add a check for `bgw.IsBusy` in the form closing event, it won't prevent the form from closing. So there's still a race condition that my bgw.complete event handler may manipulate a non-existing form. Maybe I still cannot put UI manipulation in the bgw.complete event handler. It would be great if Microsoft keep the IsBusy to true until the complete handler finishes. – smwikipedia Sep 16 '17 at 14:08
  • 1
    Your analysis is not correct, both FormClosing and RunWorkerCompleted run on the UI thread and cannot race. IsBusy is set to false by code that runs on the UI thread, just before the event handler is invoked. So if IsBusy is still true in FormClosing then you have a rock hard guarantee that RunWorkerCompleted has not executed yet. – Hans Passant Sep 16 '17 at 14:23
  • So **if FormClosing sees** IsBusy==false, the RunWorkerCompleted must have finished. – smwikipedia Sep 16 '17 at 14:36
  • 1
    Or the BGW was never started. The distinction matters somewhat, you never have a guarantee that you *could* start it since you must not start it until the form's window is created. The Load event is the first opportunity. Which might have failed. – Hans Passant Sep 16 '17 at 14:41
  • @HansPassant Many thanks for the help. The issue should be solved now. Hope I won't find any more subtle threading issues tomorrow... :) – smwikipedia Sep 16 '17 at 16:16