書いてて思ったが、Reaml の実装が悪いのかもしれない。上記チケットで改善提案が出ており、将来的には単一スレッド制限がなくなるかもしれない。
- https://ufcpp.net/study/csharp/misc_task.html
- https://ufcpp.wordpress.com/2012/11/12/asyncawait%e3%81%a8%e5%90%8c%e6%99%82%e5%ae%9f%e8%a1%8c%e5%88%b6%e5%be%a1/
- https://tech.blog.aerie.jp/entry/2015/09/17/110258
ワーカースレッドは各々キューを持っていて、その中に追加されたタスクをひたすら実行する。 手持ち無沙汰なスレッドを作らないようにするため、手持ちのキューが空になった場合は他のワーカースレッドのキューからタスクを奪って実行する。
await はその時点での同期コンテキストを拾う。現在のスレッドに関連付いてる同期コンテキストは SynchronizationContext.Current から取得できる。
上記ブログによると、同期コンテキストとは「スレッドをまたがる際の問題をいい感じにしてくれるクラス」のことらしい。いい感じの実態はプラットフォーム依存。ただし、どのプラットフォームも UI スレッドの問題を解決するために利用しているので、結果的に同期コンテキストとは「UI スレッドの問題を解決してくれるクラス」になっている。
一方、UI スレッドと違って問題が起きないので、ワーカースレッドにおいては同期コンテキストは意味がない。
そのため、UI スレッド上で await した場合は、同期コンテキストにより再び UI スレッドで await より後ろのコードが実行される。しかし、ワーカースレッド上で await した場合は、後ろのコードが再び同じスレッドで実行されることが保証されない。
Realm は別のスレッドで生成された Realm インスタンスを参照すると例外が発生する (データの整合性を守るための仕様) ので、ワーカースレッド上で await する場合はスレッドが切り替わる可能性を考慮する必要がある。UI スレッド上での await はスレッドが切り替わらないことが保証されているため考慮が不要。
例えば、次のコードは await の前後でスレッドが切り替わる可能性があるので、Realm の例外が発生する場合がある。
Task.Run(async () => {
// Thread A
var realm = Realm.GetInstance();
realm.Refresh();
var foo = realm.All<FooRealmObject>().First();
await AddFoo(foo.Name);
// Thread A or others
// Realm accessed from incorrect thread
foo.IsEnable = true;
});