0

I am creating my first multithreading C#/.NET based app that will run on a Azure Service Fabric cluster. As the title says, I wish to run a variable number of concurrent parametrizable infinite-loop type of threads, that will utilize the RunAsync method.

Each child thread looks something like this:

        public async Task childThreadCall(...argument list...)
        {
            while (true)
            {
                try
                {
                    //long running code
                    //do something useful here
                    //sleep for an independently parameterizable period, then wake up and repeat
                }
                catch (Exception e)
                {
                    //Exception Handling
                }
            }
        }

There are a variable number of such child threads that are called in the RunAsync method. I want to do something like this:

        protected override async Task RunAsync(CancellationToken cancellationToken)
        {
            try
            {
                for (int i = 0; i < input.length; i++)
                {
                    ThreadStart ts[i] = new ThreadStart(childThreadCall(...argument list...));
                    Thread tc[i] = new Thread(ts);
                    tc[i].Start();
                }
            }
            catch (Exception e)
            {
                //Exception Handling
            }
        }

So basically each of the child threads run independently from the others, and keep doing so forever. Is it possible to do such a thing? Could someone point me in the right direction? Are there any pitfalls to be aware of?

Peter Bons
  • 26,826
  • 4
  • 50
  • 74
  • I would steer clear of the Thread class unless you have a specific use case for it – TheGeneral Dec 04 '20 at 08:02
  • How long is long? What kind of work are we talking about, is it I/O bound? – Peter Bons Dec 04 '20 at 08:34
  • @PeterBons The work of each independent thread is basically querying a Azure Data Explorer cluster, waiting for the results, parsing the results and storing it in Azure Blob Storage. Then waiting a predefined interval (say 1-2 hours) and then restarting this process. This will continue forever till the app is deployed to a Service Fabric Cluster. Note that the sleep period for each thread is independently set. – desiengineer Dec 04 '20 at 19:04
  • @PeterBons Note that I don't necessary need to synchronize for thread completion. Each can do its own job once properly triggered. – desiengineer Dec 04 '20 at 19:12

1 Answers1

2

The RunAsync method is called upon start of the service. So yes it can be used to do what you want. I suggest using Tasks, as they play nicely with the cancelation token. Here is a rough draft:

protected override async Task RunAsync(CancellationToken cancellationToken)
{
    var tasks = new List<Task>();
    try
    {
        for (int i = 0; i < input.length; i++)
        {
            tasks.Add(MyTask(cancellationToken, i);
        }
        
        await Task.WhenAll(tasks);
    }
    catch (Exception e)
    {
        //Exception Handling
    }
}

public async Task MyTask(CancellationToken cancellationToken, int a)
{
    while (true)
    {
        cancellationToken.ThrowIfCancellationRequested();

        try
        {
            //long running code, if possible check for cancellation using the token
            //do something useful here
            await SomeUseFullTask(cancellationToken);
            
            //sleep for an independently parameterizable period, then wake up and repeat
            await Task.Delay(TimeSpan.FromHours(1), cancellationToken);
        }
        catch (Exception e)
        {
            //Exception Handling
        }
    }
}

Regarding pitfalls, there is a nice list of things to think of in general when using Tasks.

Do mind that Tasks are best suited for I/O bound work. If you can post what exactly is done in the long running process please do, then I can maybe improve the answer to best suit your use case.

One important thing it to respect the cancellation token passed to the RunAsync method as it indicates the service is about to stop. It gives you the opportunity to gracefully stop your work. From the docs:

Make sure cancellationToken passed to RunAsync(CancellationToken) is honored and once it has been signaled, RunAsync(CancellationToken) exits gracefully as soon as possible. Please note that if RunAsync(CancellationToken) has finished its intended work, it does not need to wait for cancellationToken to be signaled and can return gracefully.

As you can see in my code I pass the CancellationToken to child methods so they can react on a possible cancellation. In your case there will be a cancellation because of the endless loop.

Peter Bons
  • 26,826
  • 4
  • 50
  • 74
  • 1
    It should be noted that combining the `while (!cancellationToken.IsCancellationRequested)` with `await Task.Delay(..., cancellationToken)` may result to inconsistent cancellation behavior. When the token is canceled, the outcome may either be exceptional or not. My opinion is that the `IsCancellationRequested` property should be rarely used, if ever. The `ThrowIfCancellationRequested` method should be used instead, to ensure that the cancellation is consistently exceptional, always. – Theodor Zoulias Dec 04 '20 at 08:54
  • 1
    @TheodorZoulias yes, you are right, it is better to throw. Updated my answer. – Peter Bons Dec 04 '20 at 08:56
  • If you want to make sure that each of the `MyTask` instances is started as soon as possible you could consider putting `await Task.Yield()` in the beginning or otherwise the execution would run synchronously until the first async call is hit. – Daniel Lerps Dec 04 '20 at 09:03
  • @DanielLerps the `await Task.Yield()` does what you want only in the absence of a synchronization context. And it does so accidentally, because it is specifically designed for use in environments with a synchronization context installed. For this reason I consider it a hack, and I suggest using instead the `Task.Run` method, which is a non context-aware mechanism for offloading work to the `ThreadPool`. – Theodor Zoulias Dec 04 '20 at 09:24
  • @PeterBons Thank you for this answer. It is extremely close to what I need. I have a follow up question though, what if, say I have 20 Tasks, but there is a limit of say 5 on the concurrency of the SomeUseFullTask(). That is, SomeUseFullTask() is very heavy on some limited compute/IO resources, and thus need to be started only when a it can acquire one of 5 tokens. If there are no tokens, it waits till a token is available. Is it possible to implement such a thing? – desiengineer Dec 11 '20 at 01:13