2015-11-18 1 views
2

Следующий код работает без проблем:кажущаяся тупиковой с помощью консоли и PLINQ

// This code outputs: 
// 3 
// 2 
// 1 
// 
// foo 
// DotNetFiddle: https://dotnetfiddle.net/wDRD9L 
public class Program 
{ 
    public static void Main() 
    { 
     Console.WriteLine("foo"); 
    } 

    static Program() 
    {  
     var sb = new System.Text.StringBuilder(); 
     var list = new List<int>() { 1,2,3 }; 
     list.AsParallel().WithDegreeOfParallelism(4).ForAll(item => { sb.AppendLine(item.ToString()); }); 
     Console.WriteLine(sb.ToString()); 
    } 
} 

Как только я заменяю sb.AppendLine с вызовом Console.WriteLine код висит, как там тупик где-то.

// This code hangs. 
// DotNetFiddle: https://dotnetfiddle.net/pbhNR2 
public class Program 
{ 
    public static void Main() 
    { 
     Console.WriteLine("foo"); 
    } 

    static Program() 
    {  
     var list = new List<int>() { 1,2,3 }; 
     list.AsParallel().WithDegreeOfParallelism(4).ForAll(item => { Console.WriteLine(item.ToString()); }); 
    } 
} 

Сначала я подозревал Console.WriteLine не поточно-, но в соответствии с документацией является потокобезопасным.

В чем причина этого поведения?

+0

Это может быть связано с этим: http://stackoverflow.com/questions/15143931/strange-behaviour-of-console-readkey-with-multithreading - вы вызываете Console.WriteLine() в фоновом потоке, прежде чем были правильно инициализированы. –

+1

Вам повезло с 'sb.AppendLine', метод не является потокобезопасным, если у вас было больше и больше строк, ваши строки были бы неверными или некоторые даже отсутствовали бы. –

+0

Для меня он блокируется даже без 'Console.WriteLine' внутри статического конструктора' Program'. Вызов 'ForAll' (с пустым телом) в cctor вызывает зависание. (Targeting 4.5.2, VS 2013) – xxbbcc

ответ

1

Короткий вариант: никогда не блокируйте внутри конструктора, и особенно не в конструкторе static.

В вашем примере разница заключается в использовании анонимного метода. В первом случае вы захватили локальную переменную, которая заставляет анонимный метод компилироваться в свой собственный класс. Но во втором случае нет захвата переменной, и поэтому достаточно метода static. За исключением того, что статический метод помещается в класс Program. Который все еще инициализируется.

Таким образом, вызов анонимного метода блокируется инициализацией класса (вы не можете, из потока, отличного от того, где выполняется статический конструктор, выполнить метод в классе до тех пор, пока этот класс не завершит инициализация), и инициализация класса блокируется выполнением анонимного метода (метод ForAll() не вернется, пока все эти методы не выполнили).

Тупик.


Трудно понять, какое хорошее предложение может быть выполнено, учитывая, что пример (как и ожидалось) упрощенной версии того, что вы действительно делаете. Но суть в том, что вы не должны делать длительные вычисления в статическом конструкторе. Если это достаточно медленный алгоритм, который оправдывает использование ForAll(), то он достаточно медленный, чтобы он действительно не должен был быть частью инициализации класса в первую очередь.

Среди множества возможных вариантов решения проблемы можно выбрать класс Lazy<T>, который упрощает отсрочку инициализации до тех пор, пока она не понадобится.

Например, предположим, что ваш параллельный код не просто выписывает элементы списка, но фактически обрабатывает их каким-то образом. То есть это часть фактической инициализации списка. После этого вы можете заключить, что инициализация в фабричным методом, выполняемой Lazy<T> по требованию, а не в статическом конструкторе:

public class Program 
{ 
    public static void Main() 
    { 
     Console.WriteLine("foo"); 
    } 

    private static readonly Lazy<List<int>> _list = new Lazy<List<int>>(() => InitList()); 

    private static List<int> InitList() 
    { 
     var list = new List<int>() { 1,2,3 }; 
     list.AsParallel().WithDegreeOfParallelism(4).ForAll(item => { Console.WriteLine(item.ToString()); }); 

     return list; 
    } 
} 

Тогда код инициализации даже не будет выполняться на всех, пока какой-то код не требуется для доступа к списку , который он может сделать через _list.Value.


Это тонко отличается от того, что я чувствовал, что это оправдывает новый ответ (т.вид использования анонимного метода изменяет поведение), но есть, по крайней мере, два других очень тесно связаны вопросы и ответы на переполнение стека:
Plinq statement gets deadlocked inside static constructor
Task.Run in Static Initializer


Как и в сторону: я недавно узнал, что с новым компилятором Roslyn они изменили, как они реализуют анонимные методы в этом сценарии, и даже те, которые могут быть статическими методами, создаются методами экземпляра в отдельном классе (если я правильно помню). Я не знаю, было ли это уменьшать распространенность такого рода ошибок или нет, но это, безусловно, изменило бы поведение (и устранило бы анонимный метод как источник тупика и hellip, конечно, всегда можно было бы воспроизвести проблему с вызов явно объявленного статического, именованного метода).

+1

Спасибо, это объясняет это очень хорошо. По-прежнему приятно отметить, что блокирование вызовов статических методов происходит только между разными потоками. Статические методы, вызываемые из одного потока, выполняемого статическим конструктором, называются просто прекрасными. Может быть, это не хорошая практика, т. – Doug

+0

_ «блокировка в вызовах статического метода происходит только между разными потоками» _ - да, это правильно. _ «Может быть, не хорошая практика» _ - нет, я думаю, это нормально, если никто не нарушает какую-либо другую хорошую практику по статическим конструкторам. То есть предполагая, что инициализация подходит для статического конструктора, но достаточно многословна, что факторинг его в один или несколько методов улучшает код, а затем делать это - вещь, а не что-то, чего следует избегать. Я отредактирую вопрос, чтобы прояснить вопрос о вызове кросс-потоков. –

 Смежные вопросы

  • Нет связанных вопросов^_^