dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.29k stars 4.74k forks source link

Freeze/lock of dotnet process in net 5 mac #60252

Closed Whiletru3 closed 2 years ago

Whiletru3 commented 3 years ago

Description

Hello,

I have a lock/freeze on dotnet using Threads, BlockingCollection and a while loop. If I put wait in the last loop, it will not freeze/lock This lock happened in the latest stable Visual Studio on Mac : 8.10.10 (build 8)

Reproduction Steps

using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;

namespace multithread
{
    public class HighPriorityFile
    {
        public int Index { get; private set; }
        public bool Processed { get; set; }
        public HighPriorityFile(int index)
        {
            Index = index;
            Processed = false;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            // prepare the thing to do in multithread
            int npages = 10;
            var pages = new HighPriorityFile[npages];
            for (var pagesCounter = 0; pagesCounter < npages; pagesCounter++)
            {
                pages[pagesCounter] = new HighPriorityFile(pagesCounter);
            }

            // put indexes in a BlockingCollection
            var pagesToConvert = new BlockingCollection<int>(new ConcurrentQueue<int>(pages.Select(p=>p.Index)));
            pagesToConvert.CompleteAdding();

            var threadsConvertCount = 2;
            var threadsConvert = new Thread[threadsConvertCount];

            for (var convertCount = 0; convertCount < threadsConvertCount; convertCount++)
            {
                // start the threads
                threadsConvert[convertCount] = new Thread(() =>
                {
                    while (pagesToConvert.TryTake(out int pageIndex))
                    {
                        // simulate work to do...
                        Thread.Sleep(1000);

                        pages[pageIndex].Processed = true;
                        Console.WriteLine($"Convert page {pageIndex} - ${Thread.CurrentThread.Name}");
                    }
                }
                )
                { 
                    Name=$"Thread_FileConverter {convertCount}"
                };
                threadsConvert[convertCount].Start();
            }

            var i = 0;

            // this loop is responsible for the lock/freeze
            // ok this way to do is not a best practice, but it shouldn't lock/freeze
            // if you run it, it will lock/freeze.
            // if you put a breakpoint in the first line in the while, and do a step by step it will go to the end
            // if you uncomment the Thread.Sleep(1), and run, il will go to the end.
            i = 0;
            while (i < npages)
            {
                if (pages[i].Processed)
                {
                    Console.WriteLine($"Create page {pages[i].Index}");
                    pages[i] = null;
                    i++;
                }
                else
                {
                    //Thread.Sleep(1);
                }
            }

            for (i = 0; i < threadsConvertCount; i++)
                threadsConvert[i].Join();

            Console.WriteLine("finished");
        }
    }
}

Expected behavior

the process should go to the end and write in the console log Like this : Hello World! Convert page 1 - $Thread_FileConverter 0 Convert page 2 - $Thread_FileConverter 3 Convert page 0 - $Thread_FileConverter 1 Convert page 3 - $Thread_FileConverter 2 Create page 0 Create page 1 Create page 2 Create page 3 Convert page 4 - $Thread_FileConverter 0 Convert page 6 - $Thread_FileConverter 1 Convert page 5 - $Thread_FileConverter 3 Convert page 7 - $Thread_FileConverter 2 Create page 4 Create page 5 Create page 6 Create page 7 Convert page 8 - $Thread_FileConverter 0 Create page 8 Convert page 9 - $Thread_FileConverter 1 Create page 9 finished

Actual behavior

this code lock/freeze the dotnet process run at 100% of cpu, break in visual studio do nothing, and stop keep the 100%cpu dotnet process running

Regression?

No response

Known Workarounds

put a thread.sleep in the last while resolve the freeze/lock

Configuration

=== Visual Studio Community 2019 for Mac ===

Version 8.10.10 (build 8) Installation UUID: a3923c5c-dae0-47f1-a41f-fef3c1dd9e5e GTK+ 2.24.23 (Raleigh theme ) Xamarin.Mac 6.18.0.23 (d16-6 / 088c73638)

Package version: 612000140

=== Mono Framework MDK ===

Runtime: Mono 6.12.0.140 (2020-02/51d876a041e) (64-bit) Package version: 612000140

=== Roslyn (Language Service) ===

3.10.0-4.21269.26+029847714208ebe49668667c60ea5b0a294e0fcb

=== NuGet ===

Version: 5.9.0.7134

=== .NET Core SDK ===

SDK: /usr/local/share/dotnet/sdk/5.0.401/Sdks SDK Versions: 5.0.401 5.0.400 3.1.413 3.1.402 MSBuild SDKs: /Applications/Visual Studio.app/Contents/Resources/lib/monodevelop/bin/MSBuild/Current/bin/Sdks

=== .NET Core Runtime ===

Runtime: /usr/local/share/dotnet/dotnet Runtime Versions: 5.0.10 5.0.9 3.1.19 3.1.8 2.1.23 2.1.22

=== Updater ===

Version: 11

=== Apple Developer Tools ===

Xcode 11.6 (16141) Build 11E708

=== Build Information ===

Release ID: 810100008 Git revision: a3ff4b6e658e1f94623e1f3ed34ca94ed4fe78d8 Build date: 2021-09-23 19:50:51-04 Build branch: release-8.10

=== Operating System ===

Mac OS X 10.15.6

Other information

No response

dotnet-issue-labeler[bot] commented 3 years ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

danmoseley commented 3 years ago

Hello, can you reduce this repro further?

Whiletru3 commented 3 years ago

I reduced it to the minimum :

using System;
using System.Threading;
using System.Threading.Tasks;

namespace multithread
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            // prepare the thing to do in multithread
            int npages = 10;
            var indexesFinished = new bool[npages];

            var task = new Task(() =>
            {
                Parallel.For(0, npages, new ParallelOptions { MaxDegreeOfParallelism = 2 }, (index) => {
                    // simulate work to do ...
                    Thread.Sleep(1000);
                    indexesFinished[index] = true;
                    Console.WriteLine($"Convert page {index} - ${Thread.CurrentThread.Name}");
                });
            });
            task.Start();

            // this loop is responsible for the lock/freeze
            // ok this way to do is not a best practice, but it shouldn't lock/freeze
            // if you run it, it will lock/freeze.
            // if you put a breakpoint in the first line in the while, and do a step by step it will go to the end
            // if you uncomment the Thread.Sleep(1), and run, il will go to the end.
            int i = 0;
            while (i < npages)
            {
                if (indexesFinished[i])
                {
                    Console.WriteLine($"Create page {i}");
                    i++;
                }
                else
                {
                    //Thread.Sleep(1);
                }
            }

            task.Wait();

            Console.WriteLine("finished");
        }
    }
}
ghost commented 3 years ago

Tagging subscribers to this area: @JulieLeeMSFT See info in area-owners.md if you want to be subscribed.

Issue Details
### Description Hello, I have a lock/freeze on dotnet using Threads, BlockingCollection and a while loop. If I put wait in the last loop, it will not freeze/lock This lock happened in the latest stable Visual Studio on Mac : 8.10.10 (build 8) ### Reproduction Steps ``` using System; using System.Collections.Concurrent; using System.Linq; using System.Threading; namespace multithread { public class HighPriorityFile { public int Index { get; private set; } public bool Processed { get; set; } public HighPriorityFile(int index) { Index = index; Processed = false; } } class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); // prepare the thing to do in multithread int npages = 10; var pages = new HighPriorityFile[npages]; for (var pagesCounter = 0; pagesCounter < npages; pagesCounter++) { pages[pagesCounter] = new HighPriorityFile(pagesCounter); } // put indexes in a BlockingCollection var pagesToConvert = new BlockingCollection(new ConcurrentQueue(pages.Select(p=>p.Index))); pagesToConvert.CompleteAdding(); var threadsConvertCount = 2; var threadsConvert = new Thread[threadsConvertCount]; for (var convertCount = 0; convertCount < threadsConvertCount; convertCount++) { // start the threads threadsConvert[convertCount] = new Thread(() => { while (pagesToConvert.TryTake(out int pageIndex)) { // simulate work to do... Thread.Sleep(1000); pages[pageIndex].Processed = true; Console.WriteLine($"Convert page {pageIndex} - ${Thread.CurrentThread.Name}"); } } ) { Name=$"Thread_FileConverter {convertCount}" }; threadsConvert[convertCount].Start(); } var i = 0; // this loop is responsible for the lock/freeze // ok this way to do is not a best practice, but it shouldn't lock/freeze // if you run it, it will lock/freeze. // if you put a breakpoint in the first line in the while, and do a step by step it will go to the end // if you uncomment the Thread.Sleep(1), and run, il will go to the end. i = 0; while (i < npages) { if (pages[i].Processed) { Console.WriteLine($"Create page {pages[i].Index}"); pages[i] = null; i++; } else { //Thread.Sleep(1); } } for (i = 0; i < threadsConvertCount; i++) threadsConvert[i].Join(); Console.WriteLine("finished"); } } } ``` ### Expected behavior the process should go to the end and write in the console log Like this : Hello World! Convert page 1 - $Thread_FileConverter 0 Convert page 2 - $Thread_FileConverter 3 Convert page 0 - $Thread_FileConverter 1 Convert page 3 - $Thread_FileConverter 2 Create page 0 Create page 1 Create page 2 Create page 3 Convert page 4 - $Thread_FileConverter 0 Convert page 6 - $Thread_FileConverter 1 Convert page 5 - $Thread_FileConverter 3 Convert page 7 - $Thread_FileConverter 2 Create page 4 Create page 5 Create page 6 Create page 7 Convert page 8 - $Thread_FileConverter 0 Create page 8 Convert page 9 - $Thread_FileConverter 1 Create page 9 finished ### Actual behavior this code lock/freeze the dotnet process run at 100% of cpu, break in visual studio do nothing, and stop keep the 100%cpu dotnet process running ### Regression? _No response_ ### Known Workarounds put a thread.sleep in the last while resolve the freeze/lock ### Configuration === Visual Studio Community 2019 for Mac === Version 8.10.10 (build 8) Installation UUID: a3923c5c-dae0-47f1-a41f-fef3c1dd9e5e GTK+ 2.24.23 (Raleigh theme ) Xamarin.Mac 6.18.0.23 (d16-6 / 088c73638) Package version: 612000140 === Mono Framework MDK === Runtime: Mono 6.12.0.140 (2020-02/51d876a041e) (64-bit) Package version: 612000140 === Roslyn (Language Service) === 3.10.0-4.21269.26+029847714208ebe49668667c60ea5b0a294e0fcb === NuGet === Version: 5.9.0.7134 === .NET Core SDK === SDK: /usr/local/share/dotnet/sdk/5.0.401/Sdks SDK Versions: 5.0.401 5.0.400 3.1.413 3.1.402 MSBuild SDKs: /Applications/Visual Studio.app/Contents/Resources/lib/monodevelop/bin/MSBuild/Current/bin/Sdks === .NET Core Runtime === Runtime: /usr/local/share/dotnet/dotnet Runtime Versions: 5.0.10 5.0.9 3.1.19 3.1.8 2.1.23 2.1.22 === Updater === Version: 11 === Apple Developer Tools === Xcode 11.6 (16141) Build 11E708 === Build Information === Release ID: 810100008 Git revision: a3ff4b6e658e1f94623e1f3ed34ca94ed4fe78d8 Build date: 2021-09-23 19:50:51-04 Build branch: release-8.10 === Operating System === Mac OS X 10.15.6 ### Other information _No response_
Author: Whiletru3
Assignees: -
Labels: `area-CodeGen-coreclr`, `untriaged`
Milestone: -
JulieLeeMSFT commented 3 years ago

@echesakovMSFT PTAL. cc @dotnet/jit-contrib (corrected echesakov with echesakovMSFT)

echesakov commented 3 years ago

@Whiletru3 I believe such behavior (in your last example) is correct, there is no guarantee that the value of indexesFinished[i] read by Main function thread would be the latest value (i.e. true) as set by the other (worker) threads. Hence, the loop in Main never finishes because the following branch is never taken

if (indexesFinished[i])
{
    i++
}
danmoseley commented 3 years ago

@Whiletru3 why do you need the while loop at all -- can you move that work into each of the worker threads instead?

More generally, can you eliminate Parallel.For and Thread and just use Task based programming throughout? then the system will handle scheduling, and you can chain work with ContinueWith() and wait on everything with WaitAll().

Whiletru3 commented 3 years ago

@echesakovMSFT

@Whiletru3 I believe such behavior (in your last example) is correct, there is no guarantee that the value of indexesFinished[i] read by Main function thread would be the latest value (i.e. true) as set by the other (worker) threads. Hence, the loop in Main never finishes because the following branch is never taken

if (indexesFinished[i])
{
    i++
}

I have things to do in parallel then get the results in the original order. This code is working well on Windows but hang on mac. Of course, the last loop is not a best practice (as it is running and take cpu in the loop), but it should not hang.

In the first repro, i use threads and blockingcollection, so the workers take work in the order of the blockingcollection. The second repro is only to isolate the issue, parallel.for is taking work in random order that's why i dont use that...

Whiletru3 commented 3 years ago

@danmoseley

@Whiletru3 why do you need the while loop at all -- can you move that work into each of the worker threads instead?

More generally, can you eliminate Parallel.For and Thread and just use Task based programming throughout? then the system will handle scheduling, and you can chain work with ContinueWith() and wait on everything with WaitAll().

I can't use the ContinueWith and WaitAll as only one thread should take the result of the worker in the right original order. Basically, here i need to rasterize pdfs to images and get the pages in the right order to display in a viewer.

danmoseley commented 3 years ago

The Tasks can return an index that you can sort on?

@stephentoub is there a canonical way to do this?

stephentoub commented 3 years ago

is there a canonical way to do this?

From skimming the thread, the request is basically a way to process a sequence in parallel but consume the results serially in the original order as they're available?

That necessitates a reordering buffer that can track completed results by original index and yield the next one in the original order when it's available. Both PLINQ and the Dataflow library include such support.

With PLINQ, it might look like:

IEnumerable<int> output = from i in ParallelEnumerable.Range(0, 100).AsOrdered().WithMergeOptions(ParallelMergeOptions.NotBuffered).WithDegreeOfParallelism(2)
                          select Process(i);

foreach (int item in output)
{
    Console.WriteLine(item);
}

static int Process(int input)
{
    // Simulate some work
    Thread.Sleep(Random.Shared.Next(1, 1000));
    return input * 2;
}

With Dataflow, it might look like:

using System.Threading.Tasks.Dataflow;

var transform = new TransformBlock<int, int>(input => Process(input), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 2 });

for (int i = 0; i < 100; i++)
{
    transform.Post(i);
}

await foreach (int item in transform.ReceiveAllAsync())
{
    Console.WriteLine(item);
}

static int Process(int input)
{
    // Simulate some work
    Thread.Sleep(Random.Shared.Next(1, 1000));
    return input * 2;
}
echesakov commented 3 years ago

@Whiletru3 Do you see any output before the app hangs?

Whiletru3 commented 3 years ago

@Whiletru3 Do you see any output before the app hangs?

@echesakovMSFT : only the Helloworld, then it freeze

EgorBo commented 3 years ago

@Whiletru3 Do you see any output before the app hangs?

@echesakovMSFT : only the Helloworld, then it freeze

How do you launch your sample, does hang reproduce via dotnet run -c Release -f net5.0 ?

Whiletru3 commented 3 years ago

I launch it in debug inside visual studio mac. Launching it in release inside visual studio freeze Launching it in release inside visual studio don't freeze dotnet run -c Release -f net5.0 don't freeze dotnet run -c Debug -f net5.0 don't freeze

echesakov commented 3 years ago

@Whiletru3 Based on the output you are observing the workers threads never start executing. And this only happens under VS for Mac, not with "dotnet run" command.

I looked at the JIT-ed code for both Main and the worker methods and haven't seen anything suspicious. In order to look into this further I would need someone who has macOS x64. @BruceForstall Can you please try reproducing this on your Mac?

Whiletru3 commented 3 years ago

@Whiletru3 Based on the output you are observing the workers threads never start executing. And this only happens under VS for Mac, not with "dotnet run" command.

@echesakovMSFT : that's right

JulieLeeMSFT commented 2 years ago

@BruceForstall, opportunistically setting to .NET 7. Please move to future if it cannot be done in .NET 7.

BruceForstall commented 2 years ago

I can reproduce this hang with the original example above on VS for Mac 17.3 (Build 2102) (on x64). However, the "hang" does not occur when using "dotnet run" at the console. Given this, it is not a .NET runtime issue, and probably not even a libraries issue. Perhaps there is some problematic way in which VS for Mac invokes applications? Or perhaps there is some illegal threading-related code in the test case, exacerbated by the VS for Mac invocation.

If this is still a problem for you, I suggest opening a bug in Visual Studio for Mac ("Help" => "Report a Problem").