1

ユーザーがキャンバスに2Dマーカーを配置して3Dジオメトリを生成するC#(WPFを使用)のプロジェクトがあります。2D 配置が変更されると、かなりの量の計算が行われ、新しい 3D ビューが作成されます。この計算の実行には、短時間ですがかなりの時間がかかります。

イベントとプロパティを使用して、自分自身を更新できるようにプログラムを設定しました。プロジェクト全体を表すクラスがあり、「Is3dViewValid」という名前のプロパティがあります。プロジェクトに保存されている複数のオブジェクトのいずれかが、自身のプロパティが変更されたときにこのプロパティを false に設定し、3D データを再生成するイベントをトリガーすることができます。

ただし、再生成にはかなりの時間がかかるため、ユーザーが子オブジェクトの 1 つを常に更新するアクション (具体的にはキャンバス上でマーカーをドラッグする) を実行している場合、UI の遅延が発生します。遅延は、再生を別のスレッドに移動し、限られたスキルですべてをスレッドセーフにしようとするという記念碑的なタスクに対処するのに十分な長さではありません...しかし、そのままにしておくには長すぎます.

そのため、最後に Is3dViewValid が false に設定されてから、実際に計算にコミットする前に、有限の時間 (たとえば 1 秒) が経過するまで待ちます。ユーザーが連続していくつかの変更を行った場合、3D データを再生成する前に、最後の変更後 1 秒まで待機する必要があります。

C#でこれを行う「正しい」方法はありますか? おそらく誤って、これを達成するには、共有された DateTime オブジェクトを待機して監視する 2 番目のスレッドが必要であり、最後に Is3dViewValid が設定されてから一定の時間が経過したときに、元のスレッドでメソッドを呼び出そうとする必要があると想定しています。間違い。これは正しいですか、それとももっと良い方法がありますか?

編集:元の質問を書いている間はこれについて考えていませんでしたが、私が達成しようとしているのは、イベントが t=0.1s、t=0.2s、および t=0.3s で発生した場合、私はregenerate メソッドを t=1.3s で 1 回実行したい

4

3 に答える 3

1

まず、Reactive Extensions for .NET を調べることから始めます。これは、イベント ストリーム (例では通常、ユーザー インタラクション イベント) を操作するためのフレームワークであり、それらのイベントをストリームとして操作します。最も役立つのは、量を指定できるようにするストリームのhttp://msdn.microsoft.com/en-us/library/hh229400(v=vs.103).aspx Observable.Throttle メソッドです。変更をストリームの残りの部分に伝播する前に待機する時間。イベント サブスクリプションを通じてアクションを実行できます。

その他のリソース:

ここにリストされているのは、以前に書かれたものと同じことを表現するより簡潔な方法ですが、必要なコードの非同期実行も含まれています。

public class ApplicationPresenterRX
{
    private DateTime started;
    public Project Project { get; set; }

    public ApplicationPresenterRX()
    {
        // Create the project and subscribe to the invalidation event
        Project = new Project();

        // Convert events into observable stream of events based on custom event.
        Observable.FromEvent(ev => { this.Project.Invalidated += () => ev(); },
                         ev => { this.Project.Invalidated -= () => ev();})
            // Only propagate a maximum of 1 event per second (dropping others) 
            .Throttle(TimeSpan.FromSeconds(1))
            // Finally execute the task needed asynchronously
            // The only caveat is that if the code in Project.HeftyComputation updates UI components in WPF you may need to marshal the UI updates onto the correct thread to make it work.
            .Subscribe(e => { Task.Run(Project.HeftyComputation); });

        // Simulate the user doing stuff
        started = DateTime.Now;
        Project.SimulateUserDoingStuff();
    }
}
于 2013-05-24T19:12:44.643 に答える
0

わかりました、Reactive Extensions についていくつかの調査を行い、コードをまとめました。2 つの潜在的な答えがあります。これらのいずれかが「正しい」方法であるかどうかはわかりませんが、両方とも機能しているようです。RX は非常に強力に見えますが、RX を使用すると C# の能力の限界が広がります。

1. 問題

まず、問題をもう一度説明するために、次のクラスがあると想像してください。

public class Project
{
    public delegate void InvalidateEventHandler();
    public event InvalidateEventHandler Invalidated;

    private void InvalidateMyself() { if (Invalidated != null) Invalidated(); }

    public void HeftyComputation() { Thread.Sleep(2000); }

    public void SimulateUserDoingStuff()
    {
        Thread.Sleep(100);
        InvalidateMyself();
        Thread.Sleep(100);
        InvalidateMyself();
        Thread.Sleep(100);
        InvalidateMyself();
    }

    public Project() { }
}

データを保持し、スレッドセーフではありません。それには3つの方法があります。内部データに基づいて大量の計算を実行し、データが変更されるたびに自分自身を更新します。これは、InvalidateMyself() 関数を呼び出す内部プロパティ (実証されていません) を持ち、親クラスによって処理されるイベントを起動することによって実現されます。親クラスは、ある時点でオブジェクトに自身を更新するように指示することを決定します。最後に、t=0.1s、t=0.2s、および t=0.3s で InvalidateMyself() を呼び出す「ユーザー入力のシミュレート」メソッドがあります。

このクラス自体を更新する最も簡単な方法は、親オブジェクト (この場合はアプリケーション プレゼンター) を取得し、Invalidate イベントをリッスンし、HeftyComputation() メソッドが発生したときに直接起動することです。次のクラスを観察します。

public class ApplicationPresenterBasic
{
    private DateTime started;
    public Project Project { get; set; }

    public ApplicationPresenterBasic()
    {
        // Create the project and subscribe to the invalidation event
        Project = new Project();
        Project.Invalidated += Project_Invalidated;

        // Simulate the user doing stuff
        started = DateTime.Now;
        Project.SimulateUserDoingStuff();
    }

    void Project_Invalidated()
    {
        UpdateProject();
    }

    void UpdateProject()
    {
        System.Diagnostics.Debug.WriteLine(string.Format("Running HeftyComputation() at {0}s", (DateTime.Now - started).TotalSeconds));
        Project.HeftyComputation();
    }
}

これは、オブジェクトが「無効化」されるたびに HeftyComputation() メソッドを実行することを除いて、基本的にこれを行います。出力は次のとおりです。

Running HeftyComputation() at 0.1010058s
Running HeftyComputation() at 2.203126s
Running HeftyComputation() at 4.3042462s

けっこうだ?ここで、アプリケーション プレゼンターが HeftyComputation() を実行する前に、無効化なしで 1 秒間待機するという動作が必要だとしましょう。このようにして、3 つの更新すべてが t=1.3 秒で一度に処理されます。

1 つはリスニング スレッドと System.Windows.Threading Dispatcher を使用する方法、もう 1 つは Reactive Extensions の IObservable.Throttle を使用する方法です。

2. Tasks と System.Windows.Dispatcher を使用したソリューション

バックグラウンド タスクとディスパッチャを使用することの唯一の利点は、サード パーティのライブラリに依存していないことです。ただし、WindowsBase アセンブリの参照にロックされているため、これが Mono で機能するとは想像できません (それが重要な場合)。

public class ApplicationPresenterWindowsDispatcher
{
    private DateTime started;
    public Project Project { get; set; }

    /* Stuff necessary for this solution */
    private delegate void ComputationDelegate();
    private object Mutex = new object();
    private bool IsValid = true;
    private DateTime LastInvalidated;
    private Task ObservationTask;
    private Dispatcher MainThreadDispatcher;
    private CancellationTokenSource TokenSource;
    private CancellationToken Token;

    public void ObserveAndTriggerComputation(CancellationToken ctoken)
    {
        while (true)
        {
            ctoken.ThrowIfCancellationRequested();
            lock (Mutex)
            {
                if (!IsValid && (DateTime.Now - LastInvalidated).TotalSeconds > 1)
                {
                    IsValid = true;
                    ComputationDelegate compute = new ComputationDelegate(UpdateProject);
                    MainThreadDispatcher.BeginInvoke(compute);
                }
            }
        }
    }


    public ApplicationPresenterWindowsDispatcher()
    {
        // Create the project and subscribe to the invalidation event
        Project = new Project();
        Project.Invalidated += Project_Invalidated;

        // Set up observation task
        MainThreadDispatcher = Dispatcher.CurrentDispatcher;
        Mutex = new object();
        TokenSource = new CancellationTokenSource();
        Token = TokenSource.Token;
        ObservationTask = Task.Factory.StartNew(() => ObserveAndTriggerComputation(Token), Token);

        // Simulate the user doing stuff
        started = DateTime.Now;
        Project.SimulateUserDoingStuff();
    }

    void Project_Invalidated()
    {
        lock (Mutex)
        {
            IsValid = false;
            LastInvalidated = DateTime.Now;
        }
    }

    void UpdateProject()
    {
        System.Diagnostics.Debug.WriteLine(string.Format("Running HeftyComputation() at {0}s", (DateTime.Now - started).TotalSeconds));
        Project.HeftyComputation();
    }
}

仕組みは次のとおりです。アプリケーション プレゼンターが作成されると、バックグラウンドで実行される Task が生成されます。このタスクは、Project オブジェクトがまだ有効かどうかを表す bool と、最後の無効化の時刻を表す DateTime を監視します。bool が false で、最後の無効化が 1 秒以上前の場合、Task は HeftyComputation() を実行する Dispatcher を使用してメイン スレッドでメソッドを呼び出します。無効化イベント ハンドラーにより、メイン スレッドは単純に bool と DateTime を更新し、バックグラウンド スレッドがいつ更新を実行するかを決定するのを待ちます。

出力:

Running HeftyComputation() at 1.3060747s

だからこれはうまくいくようです。これはおそらく最善の方法ではありません。また、私は並行処理が得意ではありません。誰かがここで間違いや問題を見つけたら、指摘してください。

3. Reactive Extensions を使用したソリューション

Reactive Extensions を使用したソリューションを次に示します。幸いなことに、NuGet はそれをプロジェクトに簡単に追加できましたが、それを使用することは、私のスキル レベルの誰かにとっては別の話でした。Normon H は、それはほんの数行のコードで済むと述べていましたが、それは事実であることが判明しました。

デリゲートとラムダに慣れているなら、Reactive Extensions は素晴らしいものに見えます。私はそうではないので、苦労しました。

public class ApplicationPresenterRX
{
    private DateTime started;
    public Project Project { get; set; }

    public ApplicationPresenterRX()
    {
        // Create the project and subscribe to the invalidation event
        Project = new Project();

        var invalidations = Observable.FromEvent(ev => { this.Project.Invalidated += () => ev(); },
                                                 ev => { this.Project.Invalidated -= () => ev(); });
        var throttledInvalidations = invalidations.Throttle(TimeSpan.FromSeconds(1));
        throttledInvalidations.Subscribe(e => { UpdateProject(); });

        // Simulate the user doing stuff
        started = DateTime.Now;
        Project.SimulateUserDoingStuff();
    }

    void UpdateProject()
    {
        System.Diagnostics.Debug.WriteLine(string.Format("Running HeftyComputation() at {0}s", (DateTime.Now - started).TotalSeconds));
        Project.HeftyComputation();
    }
}

それだけです、3行です。Project.Invalidated イベントから IObservable ソースを作成し、最初の IObservable ソースを調整する 2 番目の IObservable ソースを作成し、それをサブスクライブして、調整されたソースがアクティブ化されたときに UpdateProject() メソッドが呼び出されるようにします。イベントで Observable.FromEvent メソッドを適切に呼び出す方法を理解することは、プロセスの最も難しい部分でした。

出力:

Running HeftyComputation() at 1.3090749s

この Web サイトを読むことから: http://www.introtorx.com/uat/content/v1.0.10621.0/13_SchedulingAndThreading.html RX はタイミングとスケジューリングにスレッドを使用しますが、デフォルトではコードを呼び出しません。別のスレッド。したがって、UpdateProject() メソッドはメイン スレッドで実行する必要があります。

于 2013-05-25T05:48:04.923 に答える
0

.NET 4.5 を使用している場合は、イベント ハンドラーでこれを行うことができます。

Task.Delay(TimeSpan.FromSeconds(5))
    .ContinueWith(t => { /* do whatever it is that you want delayed */ });

Task.DelayTaskは、完了する前に一定時間待機する以外はまったく何もしないを作成します。ContinueWith(Action<Task> action)は、「このタスクが完了したら、 this で指定されたこの 2 番目のタスクを実行する」ことを意味しますaction

追加で C# 5.0 を使用している場合は、async/awaitツールとasyncイベント ハンドラーを使用しawait Task.Delay(TimeSpan.FromSeconds(5));て、遅延させたいコードを実行できる可能性があります。


Reactive Extensions を調査するという Norman H の提案に賛成です。これらは非常に強力なツールセットですが、探している以上のものである可能性があります。

于 2013-05-24T19:12:54.037 に答える