0

I am new to async and parallel programming and my question revolves around attempting to run a method that takes a couple parameters, and then execute that method in parallel. I need to run this in parallel because the method being called is updating PLC's on my factory floor. If it is down synchrounsly, this process can take nearly 10 minutes because of how many there are. I am able to get the method to run once, using the last PLC in my custom class, but won't run for the other PLC's in my list. Sample code below:

List<Task> task = new List<Task>();
foreach(PLC plc in PlcCollection)
{
    string plcName = plc.Name;
    string tagFormat = plc.TagFormat
    tasks.Add(Task.Run(async () => await MakeTags(plcName, tagFormat)));
}

Parallel.ForEach(tasks, task => task.Start());

// Code to be done after tasks are complete

public async Task MakeTags(string plcName, string tagFormat)
{
    //Code to update the PLC's
}

The method makeTags works like it should when called - it updates the PLC correctly. But it only runs once, and only for one of my PLC values. I have also tried Task.WhenAll(tasks) instead of the Parallel.ForEach, with the same problem. Does anyone have any advice and feedback on what I am doing wrong and anything that I could try? Thanks!

EDIT: To clarify - I am expecting the MakeTags method to be executed in parallel for however many PLC's there are in the PlcCollection (the number will be variable). What I am getting is that the MakeTags method is only being called once, for one PLC element, when there are multiple PLC's in the collection. This process is started from a web app and then the server takes care of the rest. No UI elements are being updated or changed. It's all behind the scenes work.

EDIT 2: See below for a screenshot of the Debugger. In this example 2 PLC's (TTC_WALL and RR_HEPA_EE) are loaded into my list of tasks. However, only the RR_HEPA_EE is processed and then the program completes without an exception being thrown.

enter image description here

Edit 3: I made changes and now code looks like the following:

List<Task> task = new List<Task>();
foreach(PLC plc in PlcCollection)
{
    //plcName and tagFormat are just strings
    tasks.Add(MakeTags(plcName, tagFormat);
}

Task.WhenAll(tasks).ContinueWith(t =>
{
    //Code to do
    Console.WriteLine("****PLC Update Complete*****");
});

return; //program ends

public async Task MakeTags(string plcName, string tagFormat)
{
await Task.Run(() =>
{
    Console.WriteLine("Updating PLC " + plcName);
    //Code to update the PLC's
Console.WriteLine("PLC Updated for :" + plcName);
});
return;
}

However I am still getting only one of the tasks to run. Debugger screenshot below, showing only 1 of the 2 PLC's were updated. I should be seeing a console line saying "PLC Updated for TTC_WALL" as well. enter image description here

John Muraski
  • 41
  • 1
  • 7
  • Without knowing what your code, how it's currently behaving, and how that differs from what it's supposed to do, no one can help you. – Servy Jun 10 '21 at 17:02
  • The first paragraph states that I am looking for the MakeTags method to run in parallel, and that it is behaving by instead running multiple times it is running only once. I added another section to hopefully make it more clear! – John Muraski Jun 10 '21 at 17:40
  • Saying that you have some code (That you haven't shown us) and you think it's running once (but with us having no way of verifying that that's even the problem) is not really any different. – Servy Jun 10 '21 at 17:42
  • 1
    Do you want to call the `MakeTags` for all the PLC's at once, or you want to [limit the concurrency of the asynchronous operations](https://stackoverflow.com/questions/10806951/how-to-limit-the-amount-of-concurrent-async-i-o-operations)? – Theodor Zoulias Jun 10 '21 at 17:59
  • 1
    Why are you using `Task.Run()`? This call queues a worker thread on the threadpool, which is not necessary since `MakeTags()` is asynchronous and is presumably IO-bound. All you need to do is add `MakeTags(plcName, tagFormat)` to your `Task` list then await the result using `Task.WhenAll()`. Additionally, you don't need to use `Parallel.ForEach` to "start" the work. When you invoke `Task.Run()`, a thread is queued to the threadpool and will be run as allowed by your task scheduler. In the case of invoking async methods, they begin when invoked. – Patrick Tucci Jun 10 '21 at 17:59
  • @TheodorZoulias I am hoping to call all of them at once, if possible. – John Muraski Jun 10 '21 at 18:10
  • 1
    @PatrickTucci I am using Task.Run() as that was my understanding on how to set up a task - This is my first ever attempt at async programming and it's been a challenge to learn. If I understand what you are saying, I should try the following: `tasks.Add(MakeTags(plcName, tagFormat)` then `Task.WhenAll(tasks.ToArray());`? – John Muraski Jun 10 '21 at 18:13
  • I also added a screenshot of the debugger – John Muraski Jun 10 '21 at 18:13
  • 1
    @JohnMuraski a `Task` just represents an asynchronous operation. It is not shorthand for a thread or parallelism. The distinction is important and one that you should research, but outside the scope of comment discussion. To answer your question, yes, the code you posted should do what you want. It's worth noting that if `MakeTags()` contains CPU-intensive code, it could place a significant load on the server. If it's IO-bound, this should not be an issue. – Patrick Tucci Jun 10 '21 at 18:31
  • 1
    Most likely the problem is within `MakeTags`. – Stephen Cleary Jun 10 '21 at 18:48
  • Made some changes, but still ended up with the same issues. I added what my changes were, and some more of the code in MakeTags method. So I am still getting only one of my tasks to run from what I can see. – John Muraski Jun 10 '21 at 18:53
  • Got it solved with the answer below. Thank you @PatrickTucci and TheodorZoulias for you help! both of your comments and answers helped point me in the right direction. – John Muraski Jun 10 '21 at 20:33

1 Answers1

1

The problem is probably that your code does not wait for the completion of the tasks. You can wait for all of them to complete by using the blocking Task.WaitAll method.

Task[] tasks = PlcCollection
    .Select(plc => MakeTagsAsync(plc.Name, plc.TagFormat))
    .ToArray();
Task.WaitAll(tasks);
Console.WriteLine("****PLC Update Complete*****");

This way the current thread will be blocked until all the tasks have completed, and any exceptions that may have occurred will be surfaced bundled in an AggregateException.

The above approach assumes that the MakeTagsAsync method is genuinely asynchronous. If it's not, and instead it fakes asynchrony by wrapping internally some synchronous code in Task.Run, at first you should read why this is a bad idea here: Should I expose asynchronous wrappers for synchronous methods? Then make the method synchronous again by removing the Task.Run wrapper and changing the return type to void, and use the Parallel class or the PLINQ library to invoke the method in parallel. Here is a PLINQ example:

PlcCollection
    .AsParallel()
    .ForAll(plc => MakeTags(plc.Name, plc.TagFormat));
Console.WriteLine("****PLC Update Complete*****");
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    Thank you Theodor Zoulias and @Patrick Tucci! I used the PLINQ method in the answer above and it worked. My method started to run in parallel! Still got a lot to learn for parallel programming but this was a great start. – John Muraski Jun 10 '21 at 20:32
  • @JohnMuraski in case you want to control the degree of parallelism, the PLINQ includes a `WithDegreeOfParallelism` operator that you can attach after the `AsParallel` operator. Be aware that the parallelism is further limited by the availability of `ThreadPool` threads. When the `ThreadPool` is saturated, it injects new threads at a slow rate. So keep in mind the option of using the `ThreadPool.SetMinThreads` method at the start of the program, to configure (increase) the minimum number of threads that the `ThreadPool` creates instantly on demand. – Theodor Zoulias Jun 10 '21 at 20:41