次の方法で共有


ObservableCollectionからListにToArrayで変換するときのエラー

質問

2016年2月18日木曜日 3:05

こんにちは。
下図のように、以下の部分でエラーが発生しています。
ただし、毎回発生するエラーではなくて、プログラムを実行すると時々(10回に1回くらい?)発生してしまいます。

MonitorData -> ObservableCollection<MonitorDataRecord>
MonitorDataは適当なデータ(MonitorDataRecord)を2件持っていました。

図のとおり、ObservableCollectionをListに変換するToListをする際にArgumentExceptionが発生しているのですが、時々発生することになる理由と、エラーの追加情報の内容がちょっとどういうことなのかよくわからなくて困っています。

どういうことが考えられるのか、ご意見を頂戴したいです。
何か思いつくことがある方いましたら、ぜひ教えてください。よろしくお願いします。

すべての返信 (5)

2016年2月18日木曜日 9:48 ✅回答済み | 1 票

BindingOperations.EnableCollectionSynchronization(MonitorData, new object());

ロックオブジェクトになるnew object()をその場限りで使い捨てては意味がありません。

BindingOperationsというクラスをみれば推測できると思いますが、WPFの「バインディング機構で」コレクションの読み出しや追加削除をする処理の間、指定されたロックオブジェクトを使ってロックを掛けることで排他処理を行わせているのです。
たとえばDataGridに表示のために読み出したり、編集で新規行を追加したりしているタイミングで、別スレッドから追加削除するのは問題があるというのは分かりますよね。
ですからバインディング機構以外で行うコレクション操作はすべてEnableCollectionSynchronizationに渡したのと同じロックオブジェクトを使ってロックしないと排他処理になりません。

ToList()のような列挙では列挙全体を読み出す必要があるので、列挙の開始から列挙の破棄までロックしておく必要があります。。
IList<T>を継承したコレクションの多くは列挙で内部的にはインデックスで0からcount-1までアクセスするために、「取り出し」にListの長さが短くなると末尾以降へアクセスしてしまいArgumentExceptionが発生することになります。
列挙中に挿入が行われたりした場合はズレて読みだされたりすることになります。

あと、Linqをつなげていくと後段はインデックスアクセスではなくなるのでArgumentExceptionではなくInvalidOperationExceptionが出ることになります。

個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)


2016年2月18日木曜日 3:38 | 1 票

検索すると同じような方がおられました。
Destination Array not long enough?

原因はObservableCollectionがスレッドセーフではないからです。
MonitorDataを操作する部分を、lock(MonitorData){...}で囲ってみてどうでしょうか?


2016年2月18日木曜日 3:40 | 1 票

ToList()を行うタイミングに別スレッドでコレクションの追加削除をやってたりしませんか?

別スレッドで操作するとエラーになるサンプル

class MonitorDataRecord { }

private async void Test()
{
    var MonitorData = new ObservableCollection<MonitorDataRecord>();
    for (int i = 0; i < 10000; i;;)
    {
        MonitorData.Add(new MonitorDataRecord());
    }
    Task.Run(() =>
        {
            Random rnd = new Random();
            for (int i = 0; i < 1000000; i;;)
            {
                if ((rnd.Next(1000) & 0x10) != 0)
                {
                    MonitorData.Add(new MonitorDataRecord());
                }
                else if (MonitorData.Count > 0) 
                {
                    MonitorData.RemoveAt(rnd.Next(MonitorData.Count-1));
                }
            }

        });

    {
        Random rnd = new Random();
        for (int i = 0; i < 10000; i;;)
        {
            await Task.Delay(1);
            try
            {
                var list = MonitorData.ToList();
            }
            catch(ArgumentException)
            {
            }
        }
    }
}

この場合は、lockを使って読み書きするのを排他処理すると回避できると思います。

個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)


2016年2月18日木曜日 4:18

kenjinoteさん、gekkaさん、返信ありがとうございます。

たしかに、ボタンイベントやタイマーイベントなどから呼び出しているため、別スレッドからコレクション操作をしていました。

なので、これが問題のようにも思うのですが、ただ、以下の設定をコンストラクタで呼び出していたため、複数スレッド間のアクセスを許容しているものだと思っていました。

リスト化するときなどは、例外になるのでしょうか。

BindingOperations.EnableCollectionSynchronization(MonitorData, new object());


2016年2月18日木曜日 9:35 | 1 票

この設定は、コレクションをViewにバインディングしている時にViewで例外が出ないようにする設定ですね。

ToListは内部でList<T>のコピーコンストラクタを呼んでいるのですが、元のコレクションのサイズのListを確保してからコピーしているみたいです。

確保してから、コピーするまでの間に別スレッドで要素を追加すると当該の例外が出そうな気がします。

List.csで検索したらMSのリファレンスサイトが上の方に出てくるので、ご自分の目でご確認下さい。