Warm tip: This article is reproduced from serverfault.com, please click

How to notify thread from other class in C#

发布于 2020-11-27 09:55:41

I have following piece of code
(preproc, calc and display are different classes)

            Thread procThread = new Thread(() => preproc.Start());
            procThread.Start();
            Thread calcThread = new Thread(() => calc.Start());
            calcThread.Start();
            Thread displayThread = new Thread(() => display.Start());
            displayThread.Start();

In the start method of preproc.Start() I have three methods

        lock (data)
        {
            PreprocessSmth(-10.0, 10.0);
        }
        //I want to notify next thread here that preprocess ended and now next thread can run
        //TODO: data can be calculated now
        SaveResultsToDatabase();
        DoSomethingElseThatTakesTime();

And after PreprocessSmth(-10.0, 10.0); I want to notify calcThread that it can start now.

I am using lock but I don't know if it guarantees what I want.

EDIT: Using TPL I did something like that. Is that good practice, or I messed up?

Main method

Task preprocTask = preproc.StartAsync();
Task calcTask = calc.StartAsync();
Task displayTask = Task.Run(() => display.Start());
Task.WaitAll(preprocTask, calcTask, displayTask);

Inside Preproc class StartAsync method

    private Task StartAsync()
    {
        PreprocessSmth(-10.0, 10.0);

        //TODO: data can be calculated now
        Task save = SaveResultsToDatabaseAsync();
        Task doSomething = DoSomethingElseThatTakesTimeAsync();
        return Task.WhenAll(save, doSomething);
    }

And Calc class StartAsync is almost same like Preproc StartAsync. So the flow is like that: Main->preproc.StartAsync()->PreprocessSmth(), then asynchronously SavingToDb and DoSomething, ->calc.StartAsync()->CalcSmth() then SaveDataToDb asynchronously etc...

Questioner
AzJa
Viewed
0
MickyD 2020-11-28 10:20:19

OP:

How to notify thread from other class in C#...And after PreprocessSmth(-10.0, 10.0); I want to notify calcThread that it can start now.

So first a review. If we examine your original code:

Thread procThread = new Thread(() => preproc.Start());
procThread.Start();
Thread calcThread = new Thread(() => calc.Start());
calcThread.Start();
Thread displayThread = new Thread(() => display.Start());
displayThread.Start();

...we can see that three (3) Threads were created to drive preproc, calc and display and all are in a running state. I understand that you want to notify calcThread that it may begin once PreprocessSmth has completed.

One easy way that goes hand-in-hand with old school Thread programming is Windows Synchronization Objects, a common feature to most operating systems.

MSDN has this to say on the subject (my emphasis):

A synchronization object is an object whose handle can be specified in one of the wait functions to coordinate the execution of multiple threads. More than one process can have a handle to the same synchronization object, making interprocess synchronization possible. Tell me more...

...and NET (my emphasis):

.NET provides a range of types that you can use to synchronize access to a shared resource or coordinate thread interaction...Multiple .NET synchronization primitives derive from the System.Threading.WaitHandle class, which encapsulates a native operating system synchronization handle and uses a signaling mechanism for thread interaction. Tell me more...

...specifically:

System.Threading.ManualResetEvent, which derives from EventWaitHandle and, when signaled, stays in a signaled state until the Reset method is called. Tell me more...

The easiest way to think of a ManualResetEvent is like a bathroom tap. It can be initially off but if turned on the tap will stay that way until someone in your household turns it off...that or when the local city council cuts your water supply for unpaid bills.

With that in mind, let's introduce the ManualResetEvent to your code like so:


class Program
{
    #region Statics

    static void Main(string[] args)
    {
        Logger.WriteLine($"Inside Main, thread ID is {Environment.CurrentManagedThreadId}");

        // initial state is NOT set (i.e. the "bathroom tap" is off)
        var goodToGoEvent = new ManualResetEvent(false);  // <--- NEW
        var preproc    = new PreProc();
        var procThread = new Thread(() => preproc.Start(goodToGoEvent)); // pass in event
        procThread.Start();

        var calc       = new Calc();
        var calcThread = new Thread(() => calc.Start(goodToGoEvent)); // pass in event
        calcThread.Start();

        var display       = new Display();
        var displayThread = new Thread(() => display.Start());
        displayThread.Start();

        // wait for Calc to finish
        Logger.WriteLine("Main thread - waiting for Calc thread to complete...");
        calcThread.Join();
        Logger.WriteLine("Main thread - waiting for Calc thread to complete...OK");

        Logger.WriteLine("Press any key to exit");
        Console.ReadKey();
    }

    #endregion
}

internal class Display
{
    #region Methods

    public void Start()
    {
        // NOP
    }

    #endregion
}

internal class Calc
{
    #region Methods

    /// <summary>
    ///     Starts the specified good to go event.
    /// </summary>
    /// <param name="goodToGoEvent">The event to wait on that indicates "good-to-go".</param>
    public void Start(ManualResetEvent goodToGoEvent)
    {
        Logger.WriteLine("Calc - waiting for good-to-go event to be signaled...");
        goodToGoEvent.WaitOne(); // block current thread until event is signaled
        Logger.WriteLine("Calc - waiting for good-to-go event to be signaled...OK");

        // now that PreProc is complete do what needs to be done here
    }

    #endregion
}

internal class PreProc
{
    #region Fields

    private object data = new object();

    #endregion

    #region Methods

    /// <summary>
    ///     Starts the specified good to go event.
    /// </summary>
    /// <param name="goodToGoEvent">the event to signal once processing is complete.</param>
    public void Start(ManualResetEvent goodToGoEvent)
    {
        //lock (data)  // <--- not necessary in this example unless other threads will modify data concurrently
        {
            PreprocessSmth(-10.0, 10.0);
        }

        //I want to notify next thread here that preprocess ended and now next thread can run

        Logger.WriteLine("PreProc - setting goodToGoEvent to signaled");
        goodToGoEvent.Set(); // let other threads know that preprocess has ended

        //TODO: data can be calculated now
        SaveResultsToDatabase();
        DoSomethingElseThatTakesTime();
    }

    private void DoSomethingElseThatTakesTime()
    {
        // NOP
    }

    private void PreprocessSmth(double d, double d1)
    {
        Logger.WriteLine("PreProc - Preprocessing...(sleeping 5 secs)");
        Thread.Sleep(TimeSpan.FromSeconds(5)); // simulate lengthy operation
        Logger.WriteLine("PreProc - Preprocessing...OK");
    }

    private void SaveResultsToDatabase()
    {
        // NOP
    }

    #endregion
}

public static class Logger
{
    #region Static fields

    private static readonly object Locker = new object();

    #endregion

    #region Statics

    public static void WriteLine(string message)
    {
        lock (Locker)
        {
            Console.WriteLine($"{DateTime.Now:hh:mm:ss.ff} [{Environment.CurrentManagedThreadId}] {message}");
        }
    }

    #endregion
}

In my code I intoduce a new variable goodToGoEvent of type ManualResetEvent which is passed to both preproc.Start and calc.Start. It is the job of preproc to Set the event whilst calc Waits for it. Think of it like a set of traffic lights.

Here is the code in action, the numbers shown in square brackets ([ ]) is the corresponding thread ID:

enter image description here

Contemporary Threading APIs

Depending on which .NET version you are using/limited to, you might want to check out Microsoft's Task Parallel Library, Task and how it is all wrapped up in a ribbon with async/await (best experienced in .NET 4.5+).

If I may be so bold:

async/await is the contemporary and easiest way to do async programming whether or not the job is I/O-bound; whether or not the Task requires a thread. Gratuitous Self Promotion

More