0

I wrote a thread test application to see multiple and parallel started threads returning behaviour. I am new on async programming so can you please give a comment about this code?

I create multiple instances of TestThreadClass using Parallel to see what is happening when async operation is completed at the same time. When threads completed, I added them into a thread-safe dictionary (ConcurrentDictionary) with their random generated guids.

Below is a random async class returns in 1 milliseconds;

public class TestThreadClass
{
    private Task<ThreadResultModel> myTask;
    public readonly int myNumber;
    public readonly string myId;

    public TestThreadClass(int _myNumber)
    {
        myNumber = _myNumber;

        myId = Guid.NewGuid().ToString();
    }

    public async Task<ThreadResultModel> StartOperation()
    {
        myTask = InvokeOperation();

        return await myTask;
    }

    private async Task<ThreadResultModel> InvokeOperation()
    {
        await Task.Delay(TimeSpan.FromMilliseconds(1));

        return new ThreadResultModel(myNumber, myId, "Returned");
    }
}

Below is creator class I used WinForms application. I start parallel threads and after they completed I fill them to a GridView to compare return milliseconds;

public partial class Form1 : Form
{
    private const int threadNumber = 100;
    private readonly ConcurrentDictionary<string, ThreadResultModel> startedConcurrentDictionary;
    private readonly ConcurrentDictionary<string, ThreadResultModel> returnedConcurrentDictionary;

    public Form1()
    {
       InitializeComponent();

       startedConcurrentDictionary = new ConcurrentDictionary<string, ThreadResultModel>();
       returnedConcurrentDictionary = new ConcurrentDictionary<string, ThreadResultModel>();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
       FillComboboxes();
    }

    private void FillComboboxes()
    {
        for (int i = 1; i <= threadNumber; i++)
        {
           DdlThreadNumber.Items.Add(i.ToString());
        }
    }

    private void BtnStartThreads_Click(object sender, EventArgs e)
    {
        Parallel.For(0, int.Parse(DdlThreadNumber.Text), StartAThread);
    }

    private void StartAThread(int threadTag)
    {
        TestThreadClass t = new TestThreadClass(threadTag);

        startedConcurrentDictionary.TryAdd(t.myId, new ThreadResultModel(threadTag, t.myId, "Started"));
            
       t.StartOperation().ContinueWith(result =>
       {
           returnedConcurrentDictionary.TryAdd(result.Result.MyId, result.Result);
       });
    }

    private void BtnReport_Click(object sender, EventArgs e)
    {
       foreach (var item in startedConcurrentDictionary)
       {
            GrdThreads.Rows.Add(item.Value.MyNumber, item.Value.MyId, item.Value.EventType, item.Value.Time);
       }

       foreach (var item in returnedConcurrentDictionary)
       {
           GrdThreads.Rows.Add(item.Value.MyNumber, item.Value.MyId, item.Value.EventType, item.Value.Time);
       }
    }

    private void GrdThreads_SelectionChanged(object sender, EventArgs e)
    {
        statusLabel.Text = GrdThreads.SelectedRows.Count.ToString();
    }
}

I just want to know is this approach is correct or not.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
candogg
  • 145
  • 7
  • Be aware that the `Parallel.For` [is not async friendly](https://stackoverflow.com/questions/67915707/unexpected-different-results-using-task-whenall-and-parallel-foreach-with-an-ent/67923412#67923412). You can look [here](https://stackoverflow.com/questions/15136542/parallel-foreach-with-asynchronous-lambda/) for alternatives. Also combining `ContinueWith` with `await` [is not a good idea](https://stackoverflow.com/questions/18965200/difference-between-await-and-continuewith), since these two mechanisms accomplish the same thing. Use one or the other, and preferably `await`. – Theodor Zoulias Oct 17 '21 at 14:26
  • @TheodorZoulias actually, in this case `Parallel.For` calls `void` method, which additionally isn't `async`, so it's not a big issue – karolgro Oct 21 '21 at 13:07
  • @karolgro you are right, the `Parallel.For` calls a `void` method, but the code as a whole speaks volumes about the intentions of the author, which is to parallelize multiple asynchronous operations. Unfortunately they assumed that the `Parallel.For` is the right tool for solving this problem, which is not. The [`Parallel.ForEachAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.parallel.foreachasync) is, but it's not released yet. – Theodor Zoulias Oct 21 '21 at 13:56

1 Answers1

1

I just want to know is this approach is correct or not.

The answer to this question depends on what you are trying to achieve. What the code above does is:

  1. Parallel.For executes several items at once, but not all. It runs 100 calls on underlying scheduler and the number of executions at once depends on number of CPU cores. But it won't start new threads for each call, just re-use existing ones
  2. Each StartAThread doesn't start a thread. They are just run on Parallel.For scheduler's thread until the await. After that, they disappear from any thread and after some delay they are added to ThreadPool
  3. ThreadPool executes the work after awaits, load-balancing the calls between its threads
  4. Code in ContinueWith method is also added on ThreadPool

So, by just using await or Parallel.For, you won't create 100 threads. Since creating threads is slow and each thread take much memory, all those features will use some internal mechanisms to re-use the existing threads, as you don't need more threads than CPU can handle

But you still create 100 concurrent Tasks, all split into several parts (with await or ContinueWith), run independently by ThreadPool on several threads (usually about number of CPU cores/threads).

If you literally want to create 100 threads, you need to use new Thread(...) call

karolgro
  • 181
  • 1
  • 8