8

I am very new to threading, as a beginner in C#. I have a program that will be firing multiple threads inside of a windows app. My aim is to start a new thread for every item within a list. The items in this list are workstation names on anetwork. Each thread that is created will look to do repairs on each machine, when the thread has finished it will write to a log file of any errors found etc. But what i want to be able to determine is when all threads have finished. So if i have 100 machines, 100 threads, how do i determine when all have closed?

Heres my method below :-

private void repairClientsToolStripMenuItem_Click(object sender, EventArgs e)
{
    if (machineList.Count() != 0)
    {
        foreach (string ws in machineList)
        {
            new Thread(new ParameterizedThreadStart(fixClient), stack).Start(ws);
        }
    }
    else
    {
         MessageBox.Show("Please import data before attempting this procedure");
    }
}
Sergey Berezovskiy
  • 232,247
  • 41
  • 429
  • 459
Derek
  • 8,300
  • 12
  • 56
  • 88
  • possible duplicate of [Wait for two threads to finish](http://stackoverflow.com/questions/594230/wait-for-two-threads-to-finish) – Christian.K Apr 24 '12 at 08:43
  • 1
    There's a nice method for Tasks, `Task.WaitAll` (http://msdn.microsoft.com/en-us/library/dd270695.aspx). Of course, if you have to use threads, that won't help. – phipsgabler Apr 24 '12 at 08:49
  • I also recomend you start using the Task Parallel library. – vtortola Apr 24 '12 at 09:41
  • 1
    @Christian.K - not a duplicate. The link does not indicate or mention a GUI event-handler. Any solution that involves waiting on a sunchro object like Join() or WFMO(), must not be used inside an event-handler. The completed execution of the last task should trigger the Invoke/BeginInvoke execution of a delegate on the GUI thread. – Martin James Apr 24 '12 at 12:22
  • Okay answerers how 'bout we stop suggesting blocking calls on the UI thread huh? – Brian Gideon Apr 24 '12 at 13:31

8 Answers8

12

The way to do this would be to keep a reference to all the threads and then Join on them. This basically means that the current thread will block until the joined thread completes.

Change your loop to something like:

foreach (string ws in machineList)
{
   var thread = new Thread(new ParameterizedThreadStart(fixClient), stack);
   _machineThreads.Add(thread)
   thread.Start();
}

(where _machineThreads is a list of System.Thread)

You can then block until all are complete with something like:

private void WaitUntilAllThreadsComplete()
{
   foreach (Thread machineThread in _machineThreads)
   {
      machineThread.Join();
   } 
}

HOWEVER - you almost certainly don't want to be doing this for the scenario you describe:

  • You shouldn't create a large number of threads - explicitly creating hundreds of threads is not a great idea
  • You should prefer other approaches - try looking at Parallel.ForEach and System.Threading.Task. These days, .Net gives you lots of help when working with threading and asynchronous tasks - I really would advise you read up on it rather than trying to "roll your own" with explicit threads.
  • This looks like a click handler. Is it ASP.NET web forms, or a desktop application? If the former, I certainly wouldn't advise spawning lots of threads to perform background tasks from the request. In either case, do you really want your web page or GUI to block while waiting for the threads to complete?
Rob Levine
  • 40,328
  • 13
  • 85
  • 111
  • Thanks for this! Absolutley Brilliant! – Derek Apr 24 '12 at 08:47
  • Will it make a difference that ive limited each thread to 32kb? I was wary of teh memory usage I'd create on the server, but though that limiting it to 32kb would be suffice? – Derek Apr 24 '12 at 09:03
  • It's not just memory usage - creating a thread is expensive, compared to using one from the thread pool. Also - I don't know what your threads are doing, but unless they are bottle-necked elsewhere - maybe by IO to disk or database access, then they will all be trying to run constantly. This means the CPU will have to spend a lot of time just switching between the threads rather than executing them. Switching threads is also expensive. It is often quicker to have a smaller number of threads and reuse them, than to create loads of threads and have them compete for CPU time. – Rob Levine Apr 24 '12 at 09:19
4

you can use: IsAlive. But you have keep a reference like

 Thread t = new Thread(new ThreadStart(...));
 t.start();
 if(t.IsAlive)
 {
    //do something
 }
 else
 {
    //do something else
 }
jorne
  • 894
  • 2
  • 11
  • 23
2

Another idea would be to use Parallel.ForEach in a seperate thread: This is also safe if you have too many machines to fix

private void repairClientsToolStripMenuItem_Click(object sender, EventArgs e)
{

    if (machineList.Count() != 0)
    {
        AllFinished=False;
        new Thread(new ThreadStart(fixAllClients).Start();
    }
    else
    {
         MessageBox.Show("Please import data before attempting this procedure");
    }
}

private void fixAllClients(){
    var options = new ParallelOptions{MaxDegreeOfParallelism=10};
    Parallel.ForEach(machineList. options, fixClient);
    AllFinished=True;
}
weismat
  • 7,195
  • 3
  • 43
  • 58
  • 1
    No completion signaling mechanism. How is the main thread going to be notified upon completion? You need to Invoke/BeginInvoke instead of setting some flag. – Martin James Apr 24 '12 at 12:26
  • @MartinJames: At least it doesn't block the UI thread like many of the other answers :) – Brian Gideon Apr 24 '12 at 16:07
2

Never wait for thread completion, or anything else, in a GUI event handler. If you spawn many threads, (and yes, don't do that - see Rob's post), or submit many tasks to a threadpool, the last entitiy to complete execution should signal the GUI thread that the job is complete. Usually, this involves calling into some object that counts down the remaining tasks/threads and signals when the last one fires in. Look at System.Threading.CountdownEvent.

Martin James
  • 24,453
  • 3
  • 36
  • 60
  • +1 for being the only person to point out that all of these suggestions using `Thread.Join`, `WaitHandle.WaitOne`, etc. will not work on the UI thread. – Brian Gideon Apr 24 '12 at 13:30
2

There is an alternative way of doing this that uses the CountdownEvent class.

The code that starts the threads must increment a counter, and pass a CountdownEvent object to each thread. Each thread will call CountdownEvent.Signal() when it is finished.

The following code illustrates this approach:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication6
{
    class Program
    {
        static void Main(string[] args)
        {
            int numTasks = 20;
            var rng = new Random();

            using (var finishedSignal = new CountdownEvent(1))
            {
                for (int i = 0; i < numTasks; ++i)
                {
                    finishedSignal.AddCount();
                    Task.Factory.StartNew(() => task(rng.Next(2000, 5000), finishedSignal));
                }

                // We started with a count of 1 to prevent a race condition.
                // Now we must decrement that count away by calling .Signal().

                finishedSignal.Signal(); 
                Console.WriteLine("Waiting for all tasks to complete...");
                finishedSignal.Wait();
            }

            Console.WriteLine("Finished waiting for all tasks to complete.");
        }

        static void task(int sleepTime, CountdownEvent finishedSignal)
        {
            Console.WriteLine("Task sleeping for " + sleepTime);
            Thread.Sleep(sleepTime);
            finishedSignal.Signal();
        }
    }
}
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • 1
    +1 for first post that suggests an alternative that could avoid waiting in a GUI event handler. Waiting in a GUI event-handler with Join() or some other blocking wait is verging on suicidal. – Martin James Apr 24 '12 at 12:17
  • @MartinJames: No, `CountdownEvent.Wait` does not pump messages so it too would block UI thread. – Brian Gideon Apr 24 '12 at 16:14
  • Yes, the correct thing to do would be to have another thread that calls finishedSignal.Wait() and then uses BeginInvoke() to call an appropriate method on the UI thread. – Matthew Watson Apr 24 '12 at 22:45
2

Let us get something out of the way first.

  • Do not create individual threads for this. Threads are an expensive resource. Instead use thread pooling techniques.
  • Do not block the UI thread by calling Thread.Join, WaitHandle.WaitOne, or any other blocking mechanism.

Here is how I would do this with the TPL.

private void repairClientsToolStripMenuItem_Click(object sender, EventArgs e) 
{ 
  if (machineList.Count() != 0) 
  { 
    // Start the parent task.
    var task = Task.Factory.StartNew(
      () =>
      {
        foreach (string ws in machineList)
        {
          string capture = ws;
          // Start a new child task and attach it to the parent.
          Task.Factory.StartNew(
            () =>
            {
              fixClient(capture);
            }, TaskCreationOptions.AttachedToParent);
        }
      }, TaskCreationOptions.LongRunning);

    // Define a continuation that happens after everything is done.
    task.ContinueWith(
      (parent) =>
      {
        // Code here will execute after the parent task including its children have finished.
        // You can safely update UI controls here.
      }, TaskScheduler.FromCurrentSynchronizationContext);
  } 
  else 
  { 
    MessageBox.Show("Please import data before attempting this procedure"); 
  } 
} 

What I am doing here is creating a parent task that will itself spin up child tasks. Notice that I use TaskCreationOptions.AttachedToParent to associate the child tasks with their parent. Then on the parent task I call ContinueWith which gets executed after the parent and all of its children have completed. I use TaskScheduler.FromCurrentSynchronizationContext to get the continuation to happen on the UI thread.

And here is an alternate solution using Parallel.ForEach. Notice that it is a little bit cleaner solution.

private void repairClientsToolStripMenuItem_Click(object sender, EventArgs e) 
{ 
  if (machineList.Count() != 0) 
  { 
    // Start the parent task.
    var task = Task.Factory.StartNew(
      () =>
      {
        Parallel.Foreach(machineList,
          ws =>
          {
            fixClient(ws);
          });
      }, TaskCreationOptions.LongRunning);

    // Define a continuation that happens after everything is done.
    task.ContinueWith(
      (parent) =>
      {
        // Code here will execute after the parent task has finished.
        // You can safely update UI controls here.
      }, TaskScheduler.FromCurrentSynchronizationContext);
  } 
  else 
  { 
    MessageBox.Show("Please import data before attempting this procedure"); 
  } 
} 
Brian Gideon
  • 47,849
  • 13
  • 107
  • 150
  • As per @Excuseme baking powder question. There is syntax error with your use of ContinueWith(). The action will be passed the delegate of the former Task as a parameter. That way you can fx. pass the results of the parent Task into the continuation Task. – Thor Jan 23 '17 at 15:26
  • 1
    @Thor: Thanks for bringing that to my attention. I believe I have it fixed now. – Brian Gideon Jan 23 '17 at 19:32
2

Brian's solution is not complete and produces a syntax error. If not for the syntax error it would work and solve the initial poster's problem. I do not know how to fix the syntax error thus I'm posting this question for it to be resolved so that the initial question can be resolved. Please Do Not delete this message. it is pertinent to the initial question being answered.

@Brian Gideon: Your solution would be perfect with the exception of the following code:

// Define a continuation that happens after everything is done.
parent.ContinueWith(
  () =>
  {
    // Code here will execute after the parent task has finished.
    // You can safely update UI controls here.
  }, TaskScheduler.FromCurrentSynchronizationContext);

The specific problem with this is in the () => portion. This is producing a syntax error which reads Delegate System.Action "System.Threading.Tasks.Task" does not take 0 arguments

I really wish this would work and I do not know the solution to this. I've tried to look up the error but I am not understanding what parameters its requiring. If anyone can answer this it would be extremely helpful. This is the only missing piece to this problem.

  • Thank you for bringing this to my attention, I have posted an answer that explains the parameter it requires. – Thor Jan 23 '17 at 15:36
1

You could create WaitHandle for each thread you are waiting for:

WaitHandle[] waitHandles = new WaitHandle[machineList.Count()];

Add ManualResetEvent to list and pass it to thread:

for (int i = 0; i < machineList.Count(); i++)
{
    waitHandles[i] = new ManualResetEvent(false);
    object[] parameters = new object[] { machineList[i], waitHandles[i] };
    new Thread(new ParameterizedThreadStart(fixClient), stack).Start(parameters);
}

// wait until each thread will set its manual reset event to signaled state
EventWaitHandle.WaitAll(waitHandles);

Inside you thread method:

public void fixClient(object state)
{
    object[] parameters = (object[])state;
    string ws = (string)parameters[0];
    EventWaitHandle waitHandle = (EventWaitHandle)parameters[1];

    // do your job 

    waitHandle.Set();
}

Main thread will continue execution when all threads will set their wait handles.

Sergey Berezovskiy
  • 232,247
  • 41
  • 429
  • 459
  • `WaitHandle.WaitAll` isn't a bad option in some cases, but in this case it will block the UI thread. Well, actually to be completely padantic it will throw an exception because it has a 64 handle limit. – Brian Gideon Apr 24 '12 at 16:11