17

I am working on a legacy application that is built on top of NET 3.5. This is a constraint that I can't change. I need to execute a second thread to run a long running task without locking the UI. When the thread is complete, somehow I need to execute a Callback.

Right now I tried this pseudo-code:

Thread _thread = new Thread(myLongRunningTask) { IsBackground = True };
_tread.Start();
// wait until it's done
_thread.Join();
// execute finalizer

The second option, which does not lock the UI, is the following:

Thread _thread = new Thread(myLongRunningTask) { IsBackground = True };
_tread.Start();
// wait until it's done
while(_thread.IsAlive)
{
    Application.DoEvents();
    Thread.Sleep(100);
}
// execute finalizer

Of course the second solution is not good cause it overcharge the UI. What is the correct way to execute a callback when a _thread is complete? Also, how do I know if the thread was cancelled or aborted?

*Note: * I can't use the BackgroundWorker and I can't use the Async library, I need to work with the native thread class.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
Raffaeu
  • 6,694
  • 13
  • 68
  • 110
  • Thread.Start(object) can accept an user argument. This argument can be used as state. Have you considered passing the callback (or even a complex type, if required) as an argument? – Caramiriel Sep 27 '13 at 10:33
  • 1
    Could you tell why can't use the BackgroundWorker? There's only a few cases I could think of, and the details may actually be important. – quetzalcoatl Sep 27 '13 at 10:45
  • 1
    I can't use the background worker cause I can't add a dependency to Windows Form .dll cause the async worker will work also with non UI applications – Raffaeu Sep 27 '13 at 11:16

7 Answers7

43

There are two slightly different kinds of requirement here:

  • Execute a callback once the long-running task has completed
  • Execute a callback once the thread in which the long-running task was running has completed.

If you're happy with the first of these, the simplest approach is to create a compound task of "the original long-running task, and the callback", basically. You can even do this just using the way that multicast delegates work:

ThreadStart starter = myLongRunningTask;
starter += () => {
    // Do what you want in the callback
};
Thread thread = new Thread(starter) { IsBackground = true };
thread.Start();

That's very vanilla, and the callback won't be fired if the thread is aborted or throws an exception. You could wrap it up in a class with either multiple callbacks, or a callback which specifies the status (aborted, threw an exception etc) and handles that by wrapping the original delegate, calling it in a method with a try/catch block and executing the callback appropriately.

Unless you take any special action, the callback will be executed in the background thread, so you'll need to use Control.BeginInvoke (or whatever) to marshal back to the UI thread.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Just wondering: is the order of combining/invoking a multicast delegate well defined? If not, the callback may be called before the actual task. – Caramiriel Sep 27 '13 at 10:42
  • @Caramiriel: Yes, it is. `myLongRunningTask` is guaranteed to executer before the callback. – Jon Skeet Sep 27 '13 at 10:43
  • I will try this approach cause I really need a delegate to execute at the end, it does not matter the result of the thread execution – Raffaeu Sep 27 '13 at 11:25
  • What syntax is `starter += () => {` called? I have never, ever seen it before! – Scott Sep 28 '13 at 03:26
  • 3
    @Jaxo: The `() => { }` part is a lambda expression. The `+=` is normal delegate combining. – Jon Skeet Sep 28 '13 at 05:43
  • @JonSkeet Great solution! Since the question is raised 4 years ago, I am wondering in the latest version of c#, it is still the best solution? – camino Jan 30 '17 at 16:38
  • 1
    @camino: These days you'd be more likely to use tasks to start with, and `Task.ContinueWith` may be your friend. – Jon Skeet Jan 30 '17 at 17:00
  • @JonSkeet Cool! Thanks a bunch! – camino Jan 30 '17 at 17:44
  • @JonSkeet Is there anyway to make the callback run in the context of the UI thread? (I know I'm better of use async / await and tasks these days, but for my own curiosity and knowledge I'm using threads) – KidCode Jun 24 '17 at 11:09
  • @KidCode: Just call `Control.BeginInvoke` within the callback to schedule the *rest* of it on the UI thread. – Jon Skeet Jun 24 '17 at 19:58
  • @JonSkeet, how do I wrap this in a method so I can have two parameters, a parameter to pass to `myLongRunningTask` and a call back function which receives a value calculated in `myLongRunningTask`? I understand that `ThreadStart` and `starter` do not accept parameters – AaA Jul 17 '18 at 06:13
  • is this modification correct? `Thread thread = new Thread(() => { string _data = data; Action _callback = callback; string s = Post(_data); _callback(s); });` is `_callback` necessary or I can ignore it? (`data` and `callback` are outer method parameters) – AaA Jul 17 '18 at 07:01
  • @AaA: It would be better to ask this as a new question IMO. – Jon Skeet Jul 17 '18 at 07:47
  • @JonSkeet, sure, hope new question get your attention ;) – AaA Jul 17 '18 at 10:01
  • That's not correct. The code placed where says "Do what you want in the callback" is executed in the same thread ... not after thread ends. – Ibai May 14 '20 at 07:29
  • @Ibai: Yes, it executes when the long-running task is complete rather than when the thread terminates. That's how I interpreted the question, which may or may not have been correct. I'll clarify this in the answer. – Jon Skeet May 14 '20 at 08:27
1

I absolutely understand your requirements, but you've missed one crucial thing: do you really need to wait for the end of that thread synchronously? Or maybe you just need to execute the "finalizer" after thread's end is detected?

In the latter case, simply wrap the call to myLongRunningTask into another method:

void surrogateThreadRoutine() {
    // try{ ..

    mytask();

    // finally { ..
    ..all 'finalization'.. or i.e. raising some Event that you'll handle elsewhere
}

and use it as the thread's routine. That way, you'll know that the finalization will occur at the thread's and, just after the end of the actual job.

However, of course, if you're with some UI or other schedulers, the "finalization" will now run on yours thread, not on the "normal threads" of your UI or comms framework. You will need to ensure that all resources are external to your thread-task are properly guarded or synchronized, or else you'll probably clash with other application threads.

For instance, in WinForms, before you touch any UI things from the finalizer, you will need the Control.InvokeRequired (surely=true) and Control.BeginInvoke/Invoke to bounce the context back to the UI thread.

For instance, in WPF, before you touch any UI things from the finalizer, you will need the Dispatcher.BeginInvoke..

Or, if the clash could occur with any threads you control, simple proper lock() could be enough. etc.

quetzalcoatl
  • 32,194
  • 8
  • 68
  • 107
1

You can use a combination of custom event and the use of BeginInvoke:

public event EventHandler MyLongRunningTaskEvent;

private void StartMyLongRunningTask() {
    MyLongRunningTaskEvent += myLongRunningTaskIsDone;
    Thread _thread = new Thread(myLongRunningTask) { IsBackground = true };
    _thread.Start();
    label.Text = "Running...";
}

private void myLongRunningTaskIsDone(object sender, EventArgs arg)
{
    label.Text = "Done!";
}

private void myLongRunningTask()
{
    try 
    { 
        // Do my long task...
    } 
    finally
    {
        this.BeginInvoke(Foo, this, EventArgs.Empty);
    }
}

I checked, it's work under .NET 3.5

AxFab
  • 1,157
  • 8
  • 11
0

You could use the Observer Pattern, take a look here:

http://www.dofactory.com/Patterns/PatternObserver.aspx

The observer pattern will allow you, to notify other objects which were previously defined as observer.

Xavjer
  • 8,838
  • 2
  • 22
  • 42
0
  1. A very simple thread of execution with completion callback
  2. This does not need to run in a mono behavior and is simply used for convenience
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;

public class ThreadTest : MonoBehaviour
{
    private List<int> numbers = null;

    private void Start()
    {
        Debug.Log("1. Call thread task");

        StartMyLongRunningTask();

        Debug.Log("2. Do something else");
    }

    private void StartMyLongRunningTask()
    {
        numbers = new List<int>();

        ThreadStart starter = myLongRunningTask;

        starter += () =>
        {
            myLongRunningTaskDone();
        };

        Thread _thread = new Thread(starter) { IsBackground = true };
        _thread.Start();
    }

    private void myLongRunningTaskDone()
    {
        Debug.Log("3. Task callback result");

        foreach (int num in numbers)
            Debug.Log(num);
    }


    private void myLongRunningTask()
    {
        for (int i = 0; i < 10; i++)
        {
            numbers.Add(i);

            Thread.Sleep(1000);
        }
    }
}
Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
-1

Try to use ManualRestEvent to signal of thread complete.

Vasiliy
  • 492
  • 3
  • 11
  • 3
    How is that going to help? The OP needs a callback mechanism, which I don't believe `ManualResetEvent` provides. – Jon Skeet Sep 27 '13 at 10:36
-1

Maybe using conditional variables and mutex, or some functions like wait(), signal(), maybe timed wait() to not block main thread infinitely.

In C# this will be:

   void Notify()
{
    lock (syncPrimitive)
    {
        Monitor.Pulse(syncPrimitive);
    }
}

void RunLoop()
{

    for (;;)
    {
        // do work here...

        lock (syncPrimitive)
        {
            Monitor.Wait(syncPrimitive);
        }
    }
}

more on that here: Condition Variables C#/.NET

It is the concept of Monitor object in C#, you also have version that enables to set timeout

public static bool Wait(
   object obj,
   TimeSpan timeout
)

more on that here: https://msdn.microsoft.com/en-us/library/system.threading.monitor_methods(v=vs.110).aspx

Community
  • 1
  • 1
Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143