the sea of fertility

C#er blog - http://ugaya40.net より移転。今度のブログは落ちない

StatefulModelについて

StatefulModelとは?

StatefulModelはModel-View-Whateverにおける、いわゆる"fatなModel"を構築するためのクラス群です。現在の機能はStetefulModelのための変更通知群とイベントリスナー系がメインになります。github.com

変更通知コレクション群では、スレッドセーフな変更通知コレクションやソート済の変更通知コレクション、または特定のSynchronizationContextにバインドした変更通知コレクションと、それぞれの同期変換機能を提供しています。組み合わせることによって、例えば「スレッドセーフに値を追加できるソート済のUIスレッド上で結果が見える変更通知コレクション」なども簡単に作成できます。ReadOnlyWrapperも用意してあります。

MIT LicenseでNugetにて提供しています。

PCLとして作成していて、サポートするプラットフォームは以下の通りになっています。

なお、私の提供するLivetの一部機能はStatefulModelで置き換えられる予定です。

Model-View-Whateverでのfat Modelが抱える問題

ここで改めてModel-View-WhateverでのModelの責務を説明する気はありません。詳細は以下のエントリと資料に任せます。

ugaya40.hateblo.jp

変更通知コレクションの問題

この通りに実装していくと、変更通知コレクションの同期が大きな課題となってきます。標準で用意されている変更通知コレクションはObservableCollectionのみです。しかしObservableCollectionはスレッドセーフではありません。C#にasync/awaitなどの構文が導入され、ネットワークアクセス/ストレージアクセスなどが非同期になっていく中でこれは大きな問題です。.NET4.5からはObservableCollectionを比較的安全に扱う方法が追加されましたが、後述しますがあまり効率の良い方法とは言えません。

またMVVMなどでのViewModelでは、ほぼ必ずModelの変更通知コレクションをUIスレッド上で同期するコレクションが必要となってきますが、これも標準では用意されていません。

多くのMVVMライブラリでもUIスレッド上に操作を限定する変更通知コレクションは提供されていますが、Modelの変更通知コレクションとViewModelの変更通知コレクションを同期する機能を提供しているのはごく少数です(私の提供するLivetしか知りません)

ソート済のObservableCollectionはどこでしょうか?。その同期の方法は?。

そういった問題を解決するのがStatefulModelです。

イベント購読の問題

M-V-WhateverのWhateverではModelの変更通知をどう監視しますか?また、Model間での変更通知は?
C#では以下のような形でイベントの購読解除が行えません。

f:id:m_onoue:20150715213823p:plain

しかしM-V-Whatever、特にfatなModelを実践しているとこれが大きな問題になります。なぜならイベントの受信先であるWhateverは一般的に生存期間がModelより短いのです。同様にWhateverよりViewの方が生存期間が短くなってきます。

C#ではイベントの受信側の方が生存期間が短い場合、イベント受信側を破棄するタイミングできちんとイベントの購読解除を行わないとメモリリークの原因になります。

f:id:m_onoue:20141225141156p:plain

このまま素直にイベントの監視を行っているとラムダなどでイベントハンドラが短く書けるメリットが生かせません(Rxなどを使っていれば別ですが)。StatefulModelはこの問題にも解決策の一つを提供します。

StatefulModelの機能 - 再定義された変更通知コレクション群

再定義された変更通知コレクション群

StatefulModelが提供する変更通知コレクションは現在のところ以下の3つです。

  • ObservableSynchronizedCollection<T>
  • SortedObservableCollection<TSource,TKey>
  • SynchronizationContextCollection<T>

一個一個説明していきます。

ObservableSynchronizedCollection<T>

ObservableSynchronizedCollection<T>はReaderWriterLockベースのスレッドセーフな変更通知コレクションです。.NET4.5で追加されたBindingOperations.EnableCollectionSynchronizationとはアプローチが異なります。BindingOperations.EnableCollectionSynchronization はシンプルな排他ロックベースです。

なぜアプローチを変えたか説明していきたいと思いますが、事前知識としてこれだけは頭に入れておいてください。

変更通知コレクションは読み取りと書き込みが組み合わさった動作が多いです。変更通知コレクションはINotifyCollectionChangedインターフェースを実装しているが故に、そのイベント引数に情報を渡さなければいけないがために、RemoveAt(int index)などの本来削除対象の情報がインデックス以外必要ない事例でも削除対象のアイテムを取得してこなければなりません。INotifyCollectionChangedのイベント引数は多くの場合操作の対象アイテムと対象インデックスの両方を必要とするのです。

つまり一つの書き込み動作はAddなどの単純なもの以外以下のような形になります。

f:id:m_onoue:20150715233422p:plain

さてではなぜBindingOperations.EnableCollectionSynchronizationとアプローチを変えたのか説明していきましょう。結論からいえばModel-View-Whateverのfat-modelでは単純なlockベースでは効率が悪いと思ったからです。

まずロックを掛けないとどうなるでしょうか?

f:id:m_onoue:20150715221715p:plain

こういうことが起きます。

 

変更通知コレクションはその用途から、Addなどの書き込み処理 + CollectionChangedイベント発生(正確にはPropertyChangedイベントも) + 受信側の読み取り処理はatomicでなければならないということがわかると思います。Removeのように事前読み取りを必要とするものでは事前読み取りをもatomicにしなければなりません。

ではBindingOperations.EnableCollectionSynchronizationのように単純なlockベースだとどうなるでしょうか?

f:id:m_onoue:20150715222428p:plain

こうなりますね。確かに問題ありません。しかしこれでは効率が微妙です。Model-View-Whateverのfat Modelでの変更通知コレクションでは、読み込みも書き込みも入れ子になって頻繁に発生するものです。

  • ネットワークからデータを取得してきて、コレクションを更新して、更新通知を受けてUIスレッドが読みにいく。
  • ユーザーの入力によりコレクションに読み込み+書き込みが発生する。
  • ネットワークから取得したデータをコレクションに追加する際、コレクション内に既存のデータが存在しないか確認してから取得したデータを追加する。

などなどです。つまり実態としてこういうシナリオが多くなります。

f:id:m_onoue:20150715232205p:plain

StatefulModelのObservableSynchronizedCollection<T>はReaderWriterLockSlimでこの問題の影響を小さくしています。ReaderWriterLockSlimには3つのLockモードがあります。

  • 読み取りロック
  • 書き込みロック
  • アップグレード可能ロック

の3つです。

読み取りロックは書き込みロックが取得されているとブロックされます。書き込みロックはどのモードのロックが取得されていてもブロックされます。アップグレード可能ロックはイメージが付きにくいかもしれません。これはDBの共有ロック・排他ロック・更新ロックにおける更新ロックににています。アップグレード可能ロックは書き込みロックにアップグレード可能な読み取りロックというもので、読み取りはブロックしませんが、他のスレッドがアップグレード可能ロックや書き込みロックに入ろうとするとブロックします。

このModelがfat Modelでの変更通知コレクションには功をそうします。

先ほどの例はこういった形に変わります。

f:id:m_onoue:20150715234002p:plain

厳密にいうならば、”LockScope内で単体の読み取り処理ができない場面が少なくなる”です。以下のようになります。

f:id:m_onoue:20150715235020p:plain

これがBindingOperations.EnableCollectionSynchronizationを使用しない理由であり、StatefulModelのObservableSynchronizedCollection<T>の仕組みです。バグが怖そうな仕組みだと思われるでしょうが、ObservableSynchronizedCollection<T>はもともとLivetで採用済なもので、バイナリベースだと数百万(一部の方のおかげで)のPCにはぃっているはずですし、他にもいわゆる業務アプリ含めていろんな開発での実績があります。

 SortedObservableCollection<TSource,TKey>

SortedObservableCollectionはいわゆるソート済変更通知コレクションです。昇順・逆順も指定できます。

SynchronizationContextCollection<T>

SynchronizationContextCollection<T>は特定のSynchronizationContextにバインドしたコレクションです。たとえばDispatcherSynchronizationContextを使用すれば、WPFのUI Dispatcherで変更と変更通知が行われます。

コレクションでは常にSynchronizationContext.Sendが使用されます。Postは使用しません。コレクションの同期を考えるとPostは都合が悪いのです。

(2015/11/21 ver0.4より変更 SynchronizationContext.Postを常に使用)

コレクションの同期と読み取り専用ラッパーへの変換

前述した3つの変更通知コレクションからは、それと一方向に同期したお互いの変更通知コレクションを作成することが可能です。3つの変更通知コレクションはISynchronizableNotifyChangedCollection<T>というインターフェースでこの機能を実現しています。

f:id:m_onoue:20150716000653p:plain

また、ISynchronizableNotifyChangedCollection用のReadOnlyWrapperで包むことも可能です。

f:id:m_onoue:20150716000837p:plain

 その使用方法もシンプルです。

f:id:m_onoue:20150716000908p:plain

 ToSynedXXXメソッドを呼び出すだけです。

この例ではスレッドセーフな変更通知コレクションに対して、降順でソート済になる変更通知コレクションを同期するように作成し、さらに新たにUIスレッド上で動作する変更通知コレクションを作成して各要素が二乗で反映されるように同期させ、最後にそれを読み取り専用ラッパーで包んでいます。

ソート済のコレクションではソースコレクションから要素を削除した際にちゃんと対応する違うインデックスの場所のアイテムが削除されていることに注目してください。

ToSyncedXXXメソッドは内部的にStatefulModelのWeakEventListenerを使用しています(Livetと同じもの)。従って作り元コレクションの変更通知を監視しているにも関わらずきちんとGCの回収対象になります。メモリリークの心配はありません。ToSynceXXXをつなげられるのはこういった理由からです。

元のコレクションの変更通知監視をやめるにはDisposeを呼びます。

f:id:m_onoue:20150716011200p:plain

ちゃんとDispose後はデタッチされていますね?。

いくらメモリーリークはないとはいえちゃんとDisposeを読んでデタッチしてほしいです。SortedObservableCollection以外はすべてソースコレクションから要素を変換して同期する機能がついています。例で二乗しているようなところです。

各コレクションでは変換後の要素がIDisposableであった場合、デタッチ時にちゃんとコレクションをクリアして各要素をDisposeするようになっています。

なのでDisposeはきちんと呼んでくださいね。

また、ToSyncedXXXを呼び出す際にちゃんと元コレクションの書き込み操作はすべて自動でロックされます。同期コレクション作成中に元コレクションに要素が追加されて同期コレクションの要素に漏れがあることはありません。LivetのViewModelHelper.CreateReadOnlyDispatcherCollectionにあった弱点は解消されています。(ViewModelはModelより生存期間が短いので当然すべき配慮でした。これがStatefulModelでLivetの一部を置き換えていく理由でもあります)

StatefulModelの機能 - イベントリスナー

EventLitener群についてはLivetからまるごともってきました。

最初に説明した通り、C#においてこういった形でイベントの購読解除は行えません。

f:id:m_onoue:20150715213823p:plain

なので煩雑化するイベント購読管理を簡単にするため、StatefulModelではPropertyChanged/CollectionChangedそれぞれのイベントリスナーと汎用イベントリスナーを用意してあります。

f:id:m_onoue:20150716012024p:plain

CollectionChangedEventListenerはそのコレクション変更通知版です。PropertyChangedEventListenerほど多機能ではありませんが似た様な事が可能です。

また、汎用EventListenerとしてEventListenerを用意してあります。

f:id:m_onoue:20150716012126p:plain

EventListenerやPropertyChangedEventListener/CollectionChangedEventListenerはIDisposable型であり、Disposeする事でハンドラの監視を解除します。

あるクラスでのすべての同期変更通知コレクションやイベントリスナは、StatefulModel内に用意してあるCompositeDisposable(Rxのものと同名)に放り込んでおけば、CompositeDisposableのDisposeを一度呼ぶだけですべてのイベントハンドラの解除と同期の停止が行えます。

あまり使うことはないかもしれませんが、.NET標準のものと違い派生クラスをいちいち定義することのないWeakEventListenerも用意してあります。汎用のものとプロパティ変更通知のものとコレクション変更通知のものです。

f:id:m_onoue:20150716012600p:plain

前述した通りコレクション同期の仕組みでも使用されています。

StatefulModelの機能 - その他の機能

よく使うAnonymous型を用意してあります。

  • AnonymousComparer < T >
  • AnonymousDisposable
  • AnonymousSynchronizationContext etc

まとめ

以上がStatefulModelの機能になります。

作ってみて「これ無しで今までどうやって開発してたんだっけ??」という気分になりました。今後も積極的に機能拡充していく予定です。Nugetでも提供していますので是非使ってみてください!