Почему кажется, что PSeq.map с вычислительным выражением зависает?

Я пишу парсер, используя FSharp.Collections.ParallelSeq и повторить вычисление. Я хотел бы получать HTML-код с нескольких страниц параллельно и повторять запросы, когда они терпят неудачу.

Например:

open System
open FSharp.Collections.ParallelSeq

type RetryBuilder(max) = 
  member x.Return(a) = a               // Enable 'return'
  member x.Delay(f) = f                // Gets wrapped body and returns it (as it is)
                                       // so that the body is passed to 'Run'
  member x.Zero() = failwith "Zero"    // Support if .. then 
  member x.Run(f) =                    // Gets function created by 'Delay'
    let rec loop(n) = 
      if n = 0 then failwith "Failed"  // Number of retries exceeded
      else try f() with _ -> loop(n-1)
    loop max

let retry = RetryBuilder(4)

let getHtml (url : string) = retry { 
    Console.WriteLine("Get Url")
    return 0;
}

//A property/field?
let GetHtmlForAllPages = 
    let pages = {1 .. 10}
    let allHtml = pages |> PSeq.map(fun x -> getHtml("http://somesite.com/" + x.ToString())) |> Seq.toArray
    allHtml

[<EntryPoint>]
let main argv = 
    let htmlForAllPages = GetHtmlForAllPages
    0 // return an integer exit code

Когда я пытаюсь взаимодействовать с GetHtmlForAllPages из main, код кажется зависшим. Пошаговое выполнение кода показывает, что PSeq.map начинает работать с первыми четырьмя значениями pages.

Что происходит, из-за чего выражение вычисления retry никогда не запускается/не завершается? Есть какое-то странное взаимодействие между PSeq и retry?

Код работает, как и ожидалось, если я сделаю GetHtmlForAllPages функцией и вызову ее. Мне любопытно, что происходит, когда GetHtmlForAllPages является полем?


person JoshVarty    schedule 29.03.2017    source источник


Ответы (1)


Похоже, вы зашли в тупик внутри статического конструктора. Сценарий описан здесь:

CLR использует внутреннюю блокировку, чтобы гарантировать, что статический конструктор:

  • вызывается только один раз
  • выполняется перед созданием любого экземпляра класса или перед доступом к любым статическим членам.

При таком поведении CLR существует потенциальная возможность взаимоблокировки, если мы выполним любую асинхронную операцию блокировки в статическом конструкторе. (...)

Основной поток будет ждать завершения вспомогательного потока внутри статического конструктора. Поскольку вспомогательный поток обращается к методу экземпляра, он сначала попытается получить внутреннюю блокировку. Поскольку внутренняя блокировка уже получена основным потоком, мы окажемся в ситуации взаимной блокировки.

Использование Parallel LINQ (или любой другой подобной библиотеки, такой как FSharp.Collections.ParallelSeq) в статическом конструкторе заставит вас столкнуться с этой проблемой.

К сожалению, статический конструктор класса, сгенерированного компилятором, — это то, что вы получаете для своего значения GetHtmlForAllPages. Из ILSpy (с форматированием С#):

namespace <StartupCode$ConsoleApplication1>
{
    internal static class $Program
    {
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        internal static readonly Program.RetryBuilder retry@17;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        internal static readonly int[] GetHtmlForAllPages@24;

        [DebuggerBrowsable(DebuggerBrowsableState.Never), DebuggerNonUserCode, CompilerGenerated]
        internal static int init@;

        static $Program()
        {
            $Program.retry@17 = new Program.RetryBuilder(4);
            IEnumerable<int> pages = Operators.OperatorIntrinsics.RangeInt32(1, 1, 10);
            ParallelQuery<int> parallelQuery = PSeqModule.map<int, int>(new Program.allHtml@26(), pages);
            ParallelQuery<int> parallelQuery2 = parallelQuery;
            int[] allHtml = SeqModule.ToArray<int>((IEnumerable<int>)parallelQuery2);
            $Program.GetHtmlForAllPages@24 = allHtml;
        }
    }
}

и в вашем реальном классе Program:

[CompilationMapping(SourceConstructFlags.Value)]
public static int[] GetHtmlForAllPages
{
    get
    {
        return $Program.GetHtmlForAllPages@24;
    }
}

Вот откуда тупик.

Как только вы измените GetHtmlForAllPages на функцию (путем добавления ()), она перестанет быть частью этого статического конструктора, благодаря чему программа будет работать так, как ожидалось.

person MarcinJuraszek    schedule 29.03.2017