Mark Lisoway
Mark Lisoway

Reputation: 629

Multithread foreach slows down main thread

Edit: As per the discussion in the comments, I was overestimating how much many threads would help, and have gone back to Parallell.ForEach with a reasonable MaxDegreeOfParallelism, and just have to wait it out.

I have a 2D array data structure, and perform work on slices of the data. There will only ever be around 1000 threads required to work on all the data simultaneously. Basically there are around 1000 "days" worth of data for all ~7000 data points, and I would like to process the data for each day in a new thread in parallel.

My issue is that doing work in the child threads dramatically slows the time in which the main thread starts them. If I have no work being done in the child threads, the main thread starts them all basically instantly. In my example below, with just a bit of work, it takes ~65ms to start all the threads. In my real use case, the worker threads will take around 5-10 seconds to compute all what they need, but I would like them all to start instantly otherwise, I am basically running the work in sequence. I do not understand why their work is slowing down the main thread from starting them.

How the data is setup shouldn't matter (I hope). The way it's setupmight look weird I was just simulating exactly how I receive the data. What's important is that if you comment out the foreach loop in the DoThreadWork method, the time it takes to start the threads is waaay lower.

I have the for (var i = 0; i < 4; i++) loop just to run the simulation multiple times to see 4 sets of timing results to make sure that it wasn't just slow the first time.

Here is a code snippet to simulate my real code:

public static void Main(string[] args)
{
    var fakeData = Enumerable
        .Range(0, 7000)
        .Select(_ => Enumerable.Range(0, 400).ToArray())
        .ToArray();

    const int offset = 100;
    var dataIndices = Enumerable
        .Range(offset, 290)
        .ToArray();

    for (var i = 0; i < 4; i++)
    {
        var s = Stopwatch.StartNew();
        var threads = dataIndices
            .Select(n =>
            {
                var thread = new Thread(() =>
                {
                    foreach (var fake in fakeData)
                    {
                        var sliced = new ArraySegment<int>(fake, n - offset, n - (n - offset));
                        DoThreadWork(sliced);
                    }
                });

                return thread;
            })
            .ToList();

        foreach (var thread in threads)
        {
            thread.Start();
        }
        
        Console.WriteLine($"Before Join: {s.Elapsed.Milliseconds}");

        foreach (var thread in threads)
        {
            thread.Join();
        }
        
        Console.WriteLine($"After Join: {s.Elapsed.Milliseconds}");
    }
}

private static void DoThreadWork(ArraySegment<int> fakeData)
{
    // Commenting out this foreach loop will dramatically increase the speed
    // in which all the threads start
    var a = 0;
    foreach (var fake in fakeData)
    {
        // Simulate thread work
        a += fake;
    }
}

Upvotes: 0

Views: 298

Answers (1)

Moho
Moho

Reputation: 16498

Use the thread/task pool and limit thread/task count to 2*(CPU Cores) at most. Creating more threads doesn't magically make more work get done as you need hardware "threads" to run them (1 per CPU core for non-SMT CPU's, 2 per core for Intel HT, AMD's SMT implementation). Executing hundreds to thousands of threads that don't have to passively await asynchronous callbacks (i.e. I/O) makes running the threads far less efficient due to thrashing the CPU with context switches for no reason.

Upvotes: 3

Related Questions