0
\$\begingroup\$

I wrote some function to compare the duration taken for 1000 loops. Just wondering if the comparison is right? For the thread, I want to set to maximum 3 thread. Thread is core, right? Task took the longest time which seems not correct.

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Multithreading_Loop
{
class Program
{
    static void Main(string[] args)
    {
        //PrintLoopWithoutThread();
        //PrintLoopWithThread();
        //PrintLoopWithTaskWaitAll();
        //PrintLoopWithParallelFor();
        PrintLoopWithParallelForEach();
    }

    /// <summary>
    /// Elapsed time: 4.13 seconds
    /// </summary>
    private static void PrintLoopWithoutThread()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();

        for (var x = 1; x <= 1000; x++)
        {
            Console.WriteLine($"Worker Id: {Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine(x);
        }

        sw.Stop();
        Console.WriteLine(sw.Elapsed.TotalSeconds);
    }

    /// <summary>
    /// Elapsed time: 11.4 seconds
    /// </summary>
    private static void PrintLoopWithThread()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();

        Thread t1 = new Thread(PrintLoopWithoutThread);
        t1.Start();
        Thread t2 = new Thread(PrintLoopWithoutThread);
        t2.Start();
        Thread t3 = new Thread(PrintLoopWithoutThread);
        t3.Start();
        t1.Join();
        t2.Join();
        t3.Join();

        sw.Stop();
        Console.WriteLine(sw.Elapsed.TotalSeconds);
    }


    /// <summary>
    /// Elapsed time: 15.5 seconds
    /// </summary>
    private static void PrintLoopWithTaskWaitAll()
    {
        Task task1 = Task.Factory.StartNew(() => PrintLoopWithoutThread());
        Task task2 = Task.Factory.StartNew(() => PrintLoopWithoutThread());
        Task task3 = Task.Factory.StartNew(() => PrintLoopWithoutThread());

        Task.WaitAll(task1, task2, task3);

    }

    /// <summary>
    /// Elapsed time: 2.2 seconds
    /// </summary>
    private static void PrintLoopWithParallelFor()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();

        Parallel.For(0, 999, x =>
        {
            Console.WriteLine($"WorkerId: {Task.CurrentId}. Number: " + x);
        });

        sw.Stop();
        Console.WriteLine(sw.Elapsed.TotalSeconds);
    }

    /// <summary>
    /// Elapsed time: 2.0 seconds
    /// </summary>
    private static void PrintLoopWithParallelForEach()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();

        var data = Enumerable.Range(0, 999);

        // If computer has 4 cores and you want to use the maximum 4, 
        // then put MaxDegreeOfParallelism = 4
        Parallel.ForEach(data, new ParallelOptions { MaxDegreeOfParallelism = 4 }, i =>
        {
            Console.WriteLine($"WorkerId: {Task.CurrentId}. Number: " + i);
        });

        sw.Stop();
        Console.WriteLine(sw.Elapsed.TotalSeconds);
    }

}
}
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

You definitely should remove all Console.WriteLine operations that are executed during the measuring. You want to measure pure excecution time.

Also thread creation/management costs time i.e. overhead. When using Thread (executing PrintLoopWithThread) you started the Stopwatch BEFORE you created the threads, which is correct, because you want to include the overhead of each parallelization method. But when creating Task instances (when executing PrintLoopWithTaskWaitAll), you started the Stopwatch AFTER their creation, which leads to ignore instantiation costs i.e. overhead. The results are therefore not comparable at all.

Furthermore keep in mind that Parallel.For performs bad for small workloads:

If the body of the loop performs only a small amount of work, you may find that you achieve better performance by partitioning the iterations into larger units of work. The reason for this is that there are two types of overhead that are introduced when processing a loop: the cost of managing worker threads and the cost of invoking a delegate method. In most situations, these costs are negligible, but with very small loop bodies they can be significant.

Threading can slow down operations/applications significantly. For efficient multithreading it's not enough to use a fixed number of threads. You usually use smart partitioning/ workload balancing algorithms, thread pooling and metrics to find the pivot when multithreading becomes too expensive i.e. wrong choice. It's not simple to implement efficient multithreading or test efficiency.

Most of the time tests are only relevant for a special scenario - the tested scenario. So, it'S best to test at least both expected ends: a worst-case sccenarion and a best-case scenario. You maybe will come to the conclusion to use dynamic workload based multithreading (like PLINQ does) and only use threads under certain conditions.
The best is to test with the real code you are trying to parallelize. 999 iterations is way too few to have significant impact - at least when running no-ops. You want CPU bound heavy loads, but 999 "empty" iterations is just a blink in terms of CPU workload.

\$\endgroup\$
2
  • \$\begingroup\$ a) "110% sure", really? OP's code runs on 9 different threads on my machine b) What kind of "internal optimizations" do you think can happen in a Parallel.For? \$\endgroup\$ Feb 4, 2020 at 13:45
  • \$\begingroup\$ Yes you are alright. I was mixing it up with PLINQ, which analyses the query to decide whether to use parallelization or not. But nevertheless does a Parallel.For with only a small workload perform bad. I will fix my answer. Thank you. \$\endgroup\$
    – BionicCode
    Feb 4, 2020 at 14:10
1
\$\begingroup\$

The Console uses synchronization:

I/O operations that use these streams are synchronized, which means that multiple threads can read from, or write to, the streams.

This synchronization ensures that all sentences are printed and nothing gets lost due to a data race.

However, synchronization kills multithreading, because the threads will stop at the synchronization object. Don't use Console.WriteLine() for performance comparisons.

Another thing to consider: at the moment you have 3 tasks, each printing 1000 lines. That way, scheduling cannot be done in small pieces. What you typically want is 3000 tasks, each printing 1 line. (read "printing" == "doing work").

\$\endgroup\$
3
  • \$\begingroup\$ Are you sure, we prefer to have so many Tasks ? Your statement applies to Parallesim, The partioner wants to have many items, to split it into (a few 4, 8, 10) tasks. \$\endgroup\$
    – Holger
    Feb 4, 2020 at 11:55
  • \$\begingroup\$ @Holger: I've been processing images and using 1 task per line worked very well (that's easily 3000 tasks for modern digital camera pics). However, I would not have used one task per pixel (12.000.000 tasks). I have not tried, though. \$\endgroup\$ Feb 4, 2020 at 12:13
  • 1
    \$\begingroup\$ OK, I reread a little,and a Task has not much overhead, it's Items on a queue, you have only a small number of real threads working. So in cases where you do not have much to initialize to start your work, you can have 1000. But you still need a minimum amount of work to do. The call of the task should not be more expensive than the inner operation.So the best choice depends on the algorithm of your job. If every chunk of work is equal size, you very likely have no gain in having more than let'say 64 Tasks. But if one runs for 10ms and another for 100ms you could gain something by having more. \$\endgroup\$
    – Holger
    Feb 4, 2020 at 12:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.