わかりました、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() メソッドはメイン スレッドで実行する必要があります。