0

I'm working on some text parser logic and have IO and CPU bound operations. In general what I do: read file -> process text, read file 2 -> process text 2... and so on.

What I can do is to make operations "process text" and "read file 2" executing at the same time in different tasks. However when I created tree of related tasks I noticed that PLINQ became slower than it was initially. Here is simplified example of code:

class Program { static int idx;

public static void Main()
{
    idx = 0;
    Stopwatch sw = new Stopwatch();
    sw.Start();

    for (int i = 0; i < 10; i++)
    {
        // here I execute my function with PLINQ
        FuncP();
    }

    sw.Stop();

    Console.WriteLine(sw.ElapsedMilliseconds);

    idx = 0;

    sw.Start();

    List<Task> tasks = new List<Task>();
    for (int i = 0; i < 10; i++)
    {
        int iLocal = i;
        Task tsk;
        if (i > 0)
        {
            // next task depends on previous one
            tsk = Task.Run(() =>
            {
                Task.WaitAll(tasks[iLocal - 1]);

                // execute the same function
                FuncP();
            });
        }
        else // i = 0
        {
            // first task does not depend on other tasks
            tsk = Task.Run(() =>
                {
                    // execute the same function
                    FuncP();
                });
        }

        tasks.Add(tsk);
    }

    tasks.Last().Wait();
    sw.Stop();

    Console.WriteLine(sw.ElapsedMilliseconds);
}

private static void FuncP()
{
    Stopwatch sw1 = new Stopwatch();
    sw1.Start();

    Console.WriteLine(string.Format("FuncP start {0}", ++idx));
    string s = new string('c', 2000);
    s.AsParallel()
        .ForAll(_ =>
        {
            for (int i = 0; i < 1000000; i++) ;
        });

    sw1.Stop();

    Console.WriteLine(string.Format("FuncP end {0}, Elapsed {1}", idx, sw1.ElapsedMilliseconds));
}

}

Output is:

FuncP start 1
FuncP end 1, Elapsed 409
FuncP start 2
FuncP end 2, Elapsed 345
FuncP start 3
FuncP end 3, Elapsed 344
FuncP start 4
FuncP end 4, Elapsed 337
FuncP start 5
FuncP end 5, Elapsed 344
FuncP start 6
FuncP end 6, Elapsed 343
FuncP start 7
FuncP end 7, Elapsed 348
FuncP start 8
FuncP end 8, Elapsed 351
FuncP start 9
FuncP end 9, Elapsed 343
FuncP start 10
FuncP end 10, Elapsed 334
3504
FuncP start 1
FuncP end 1, Elapsed 5522 --> here is high execution time
FuncP start 2
FuncP end 2, Elapsed 368
FuncP start 3
FuncP end 3, Elapsed 340
FuncP start 4
FuncP end 4, Elapsed 347
FuncP start 5
FuncP end 5, Elapsed 351
FuncP start 6
FuncP end 6, Elapsed 347
FuncP start 7
FuncP end 7, Elapsed 353
FuncP start 8
FuncP end 8, Elapsed 337
FuncP start 9
FuncP end 9, Elapsed 341
FuncP start 10
FuncP end 10, Elapsed 345
12160

Sometimes it hangs on first run PLINQ in task, sometimes on second one, but with tasks it takes much more time that in loop. Not sure I understand exactly the reason why FuncP execution time is so high, could it be that AsParallel() does not make it parellel due to lack of "free" threads in thread pool?
Can someone explain? Thanks in advance.

Alexey Klipilin
  • 1,866
  • 13
  • 29
  • The whole approach seems problematic (e.g. the use of `Task.WaitAll(tasks[iLocal - 1]);` is not thread-safe since a different thread may be adding to `tasks` at the same time). https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/dataflow-task-parallel-library may be worth considering as an alternative. Or if you don't want to learn that, have `read file` add to a `BlockingCollection` which `process text` reads from in a different thread / task. See https://stackoverflow.com/questions/6608042/parallel-foreach-loop-with-blockingcollection-getconsumableenumerable . – mjwills Mar 16 '18 at 11:38
  • @mjwills sure,I understand all this, just example of roughly made code, was interested mainly in reason for timings, thank you – Alexey Klipilin Mar 16 '18 at 12:09

1 Answers1

1

The issue is likely caused by the fact that the Thread Pool is reasonably conservative (search the article for Thread Injection) in how many spare threads it has lying around. Matt Warren's article is worth a read on the issue.

If you set the ThreadPool to have a minimum number of threads, for example using:

ThreadPool.SetMinThreads(100, 100);

then the two code samples act roughly the same since 'spare' threads are available.

Note that I would not suggest using 100 in production - it is merely an example to show the contrast.

mjwills
  • 23,389
  • 6
  • 40
  • 63