16

これは大まかな質問ではないかと思いますが、最近、C# から適切な F# に変換する方法を確認したいコードに出くわしました。旅はここ (1) (TPL-F# の相互作用に関する元の問題) から始まり、ここ(2) (F# に変換することを検討しているサンプル コード)から続きます。

サンプル コードは長すぎてここで再現できませんが、興味深い関数はActivateAsyncRefreshHubsおよびAddHubです。特に興味深い点は、

  1. AddHubの署名がありprivate async Task AddHub(string address)ます。
  2. RefreshHubsはループ内で を呼び出しAddHub、 のリストを収集しますtasks。これは最後に によって待機し、await Task.WhenAll(tasks)その結果、戻り値は の署名と一致しますprivate async Task RefreshHubs(object _)
  3. RefreshHubsActivateAsyncasによって呼び出され、最後に関数 signature に一致await RefreshHubs(null)する呼び出しがあります。await base.ActivateAsync()public override async Task ActivateAsync()

質問:

インターフェイスと機能を維持し、既定のカスタム スケジューラを尊重する F# へのそのような関数シグネチャの正しい変換は何でしょうか? それ以外の点では、この「F# での async/await」についても確信が持てません。「機械的に」それを行う方法のように。:)

その理由は、リンク「here (1)」に問題があるように見えるためです (私はこれを確認していません)。F# 非同期操作は、(Orleans) ランタイムによって設定されたカスタムの協調スケジューラを尊重しません。また、ここでは、TPL 操作はスケジューラをエスケープしてタスク プールに移動するため、その使用は禁止されていると述べられています。

これに対処する方法の 1 つは、次のように F# 関数を使用することです。

//Sorry for the inconvenience of shorterned code, for context see the link "here (1)"...
override this.ActivateAsync() =
    this.RegisterTimer(new Func<obj, Task>(this.FlushQueue), null, TimeSpan.FromMilliseconds(100.0), TimeSpan.FromMilliseconds(100.0)) |> ignore

    if RoleEnvironment.IsAvailable then
        this.RefreshHubs(null) |> Async.awaitPlainTask |> Async.RunSynchronously
    else
        this.AddHub("http://localhost:48777/") |> Async.awaitPlainTask |> Async.RunSynchronously

    //Return value comes from here.
    base.ActivateAsync()

member private this.RefreshHubs(_) =
    //Code omitted, in case mor context is needed, take a look at the link "here (2)", sorry for the inconvinience...
    //The return value is Task.
    //In the C# version the AddHub provided tasks are collected and then the
    //on the last line there is return await Task.WhenAll(newHubAdditionTasks) 
    newHubs |> Array.map(fun i -> this.AddHub(i)) |> Task.WhenAll

member private this.AddHub(address) =
    //Code omitted, in case mor context is needed, take a look at the link "here (2)", sorry for the inconvinience...
    //In the C# version:
    //...
    //hubs.Add(address, new Tuple<HubConnection, IHubProxy>(hubConnection, hub))
    //} 
    //so this is "void" and could perhaps be Async<void> in F#... 
    //The return value is Task.
    hubConnection.Start() |> Async.awaitTaskVoid |> Async.RunSynchronously
    TaskDone.Done

このstartAsPlainTask関数は、ここからSacha Barberによるものです。別の興味深いオプションは次のとおりです

module Async =
    let AwaitTaskVoid : (Task -> Async<unit>) =
        Async.AwaitIAsyncResult >> Async.Ignore

<編集:Task.WhenAllも待機する必要があることに気付きました。しかし、適切な方法は何でしょうか?ええと、寝る時間です (悪い駄洒落)...

<編集 2:ここ (1) (TPL-F# の相互作用に関する元の問題)では、Codeplex で、F# は同期コンテキストを使用して作業をスレッドにプッシュするが、TPL はそうではないことが言及されています。さて、これはもっともらしい説明だと思います (ただし、カスタム スケジューラに関係なく、これらのスニペットを適切に翻訳するにはまだ問題があります)。いくつかの興味深い追加情報がから得られる可能性があります

この文脈では、興味深い接線としてHopacに言及する必要があると思います。

<編集 3 : Danielsvickは、カスタム タスク ビルダーを使用するようコメントで適切なアドバイスを提供しています。Daniel は、すでにFSharpxで定義されているものへのリンクを提供しています。

ソースを見ると、パラメーターを持つインターフェイスが次のように定義されていることがわかります

type TaskBuilder(?continuationOptions, ?scheduler, ?cancellationToken) =
    let contOptions = defaultArg continuationOptions TaskContinuationOptions.None
    let scheduler = defaultArg scheduler TaskScheduler.Default
    let cancellationToken = defaultArg cancellationToken CancellationToken.None

オルレアンでこれを使用する場合は、ここのドキュメントTaskSchedulerに従っている必要があるようですTaskScheduler.Current

Orleans には独自のタスク スケジューラがあり、グレイン内で使用されるシングル スレッド実行モデルを提供します。タスクを実行するときは、.NET スレッド プールではなく、Orleans スケジューラを使用することが重要です。

グレイン コードでサブタスクを作成する必要がある場合は、Task.Factory.StartNew を使用する必要があります。

await Task.Factory.StartNew(() =>{ /* ロジック */ });

この手法では、現在のタスク スケジューラ (Orleans スケジューラ) を使用します。

常に .NET スレッド プールを使用する Task.Run の使用は避ける必要があるため、シングル スレッド実行モデルでは実行されません。

TaskScheduler.CurrentTaskScheduler.Defaultの間に微妙な違いがあるようです。たぶん、これは、どの例のケースで望ましくない違いがあるかについての質問を正当化します. Orleansのドキュメントでは使用しないように指摘されておりTask.Run、代わりに. . うーん、私が間違っていなければそうなるようです。Task.Factory.StartNew.Default.DenyAttachChilld

また、カスタム スケジューラに関してはTask.Runvizに問題があるため、タスク スケジューラ (Task.Factory) で説明されているようにカスタムTaskFactoryを使用し、スレッドの数を制御することで、この特定の問題を解決できないかと考えています。同時実行を制限するタスク スケジューラTask.Factory.CreateNew

うーん、これはすでにかなり長い「熟考」になっています。どうやってこれを閉じればいいのだろうか?たぶん、svickDanielがコメントを回答として作成でき、私が両方に賛成してsvickのを受け入れるとしたら?

4

1 に答える 1

1

TaskBuilderFSharpx でuse を使用し、 で渡すことができますTaskScheduler.Current。これが私の翻訳の試みRefreshHubsです。Task<unit>の代わりに が使用されていることに注意してくださいTask

let RefreshHubs _ =
    let task = TaskBuilder(scheduler = TaskScheduler.Current)
    task {
        let addresses = 
            RoleEnvironment.Roles.["GPSTracker.Web"].Instances
            |> Seq.map (fun instance ->
                let endpoint = instance.InstanceEndpoints.["InternalSignalR"]
                sprintf "http://%O" endpoint.IPEndpoint
            )
            |> Seq.toList

        let newHubs = addresses |> List.filter (not << hubs.ContainsKey)
        let deadHubs = hubs.Keys |> Seq.filter (fun x -> 
            not (List.exists ((=) x) addresses))

        // remove dead hubs
        deadHubs |> Seq.iter (hubs.Remove >> ignore)

        // add new hubs
        let! _ = Task.WhenAll [| for hub in newHubs -> AddHub hub |]
        return ()
    }
于 2014-07-22T19:39:02.627 に答える