none
ADO.NET 2.0 で複数のスレッドからのアクセス RRS feed

  • 質問

  • マルチスレッドで取得したデータを DataSet に保存することを考えています。

    スレッドプールを使うときマルチスレッド処理のメソッドは static なので
    DataSet も static で宣言することになります(勘違いじゃなければ)。

    取得したデータはその内容によって
    DaraSet 内の DataTable へ新規に行を挿入、または既存行を更新することになります。
    そこで新規行挿入用のメソッドと既存行更新用のメソッドを作り、
    それぞれのメソッドのなかで lock ステートメントを使うことで
    それぞれのメソッドが複数スレッドで同時に実行できないようにすることを考えていますが、
    行挿入メソッドと既存行更新メソッドが同時に実行されることが安全かどうかわかりません。
    感覚的には大丈夫かもしれへん、とは思うのですが
    実際のところどうなのかわかる方おられますか?

    2010年11月22日 1:35

回答

  • スレッドプールを使うときマルチスレッド処理のメソッドは static なので
    DataSet も static で宣言することになります(勘違いじゃなければ)。

    DataSet は絶対に static にしなければいけない、というわけではありません。たとえば、

    public class Foo
    {    
      public void InsertAndUpdate(DataSet data)
      {
        // インスタンスメソッドをスレッドプールで実行できる
        // メソッドに渡すデータを第2引数で指定
        ThreadPool.QueueUserWorkItem(new WaitCallback(InsertData), data);
      }
    
      // データベースにデータを挿入するインスタンスメソッド
      private void InsertData(object o)
      {
        DataSet data = (DataSet)o;
    
        // data の内容をデータベースに挿入
      }
    }
    
    

    という風に、スレッドプールで実行するメソッドに引数として渡すこともできます。

     

     


    なかむら(http://d.hatena.ne.jp/griefworker)
    • 回答としてマーク SweetSmile 2010年11月22日 9:04
    2010年11月22日 2:43
  • > スレッドプールを使うときマルチスレッド処理のメソッドは static なので
    > DataSet も static で宣言することになります(勘違いじゃなければ)。

    そういう制限はなく、コーディング次第です。

    > 行挿入メソッドと既存行更新メソッドが同時に実行されることが安全かどうかわかりません。

    MSDN の DataSet や DataTable の説明には(追記:DataRow の説明にも)、
    ---引用
    この型は、マルチスレッド読み取り操作に対して安全です。すべての書き込み操作の同期をとる必要があります。
    ---
    と書かれていますので、安全ではないです。

    各メソッドでのロック対象を同じにするか、各スレッドで発生する追加や更新の要求は単にキューに溜めるだけにしておいてそのキューを別の専用スレッドで処理してはいかがでしょうか。キューを使う方がロック時間(キューのロック)を最小限にできそうなので。

    なお、もしその DataTable 等がコントロールにバインドしている場合、そのバインド先のコントロールに対しては別スレッドからの操作が発生してしまうことになり不都合が生じますので、その点は気を付けてください。

    • 編集済み TH01 2010年11月22日 6:42 追記
    • 回答としてマーク SweetSmile 2010年11月22日 9:04
    2010年11月22日 2:04
  • 更新するDataRowと新規に追加するDataRowはそれぞれ別のインスタンスですから、そこだけ見れば更新と追加が同時に発生しても問題が発生しないように思えます。ただし、タイミングによって追加なのか更新なのかを正しく判断できず、同じデータを追加しようとすることなどが発生するのであれば、DataTable全体として一貫性を保つようにしなければならないでしょう。


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

     

    2010年11月22日 3:21
    モデレータ
  • ThreadPoolですが、確かキューの実行開始間隔が0.5秒?とかだったと思うので、

    これを利用してDataRowの更新をUIスレッドで行うとなると、重たい処理になって

    しまう可能性もあります。

    • 回答としてマーク SweetSmile 2010年11月22日 9:04
    2010年11月22日 5:59

すべての返信

  • > スレッドプールを使うときマルチスレッド処理のメソッドは static なので
    > DataSet も static で宣言することになります(勘違いじゃなければ)。

    そういう制限はなく、コーディング次第です。

    > 行挿入メソッドと既存行更新メソッドが同時に実行されることが安全かどうかわかりません。

    MSDN の DataSet や DataTable の説明には(追記:DataRow の説明にも)、
    ---引用
    この型は、マルチスレッド読み取り操作に対して安全です。すべての書き込み操作の同期をとる必要があります。
    ---
    と書かれていますので、安全ではないです。

    各メソッドでのロック対象を同じにするか、各スレッドで発生する追加や更新の要求は単にキューに溜めるだけにしておいてそのキューを別の専用スレッドで処理してはいかがでしょうか。キューを使う方がロック時間(キューのロック)を最小限にできそうなので。

    なお、もしその DataTable 等がコントロールにバインドしている場合、そのバインド先のコントロールに対しては別スレッドからの操作が発生してしまうことになり不都合が生じますので、その点は気を付けてください。

    • 編集済み TH01 2010年11月22日 6:42 追記
    • 回答としてマーク SweetSmile 2010年11月22日 9:04
    2010年11月22日 2:04
  • スレッドプールを使うときマルチスレッド処理のメソッドは static なので
    DataSet も static で宣言することになります(勘違いじゃなければ)。

    DataSet は絶対に static にしなければいけない、というわけではありません。たとえば、

    public class Foo
    {    
      public void InsertAndUpdate(DataSet data)
      {
        // インスタンスメソッドをスレッドプールで実行できる
        // メソッドに渡すデータを第2引数で指定
        ThreadPool.QueueUserWorkItem(new WaitCallback(InsertData), data);
      }
    
      // データベースにデータを挿入するインスタンスメソッド
      private void InsertData(object o)
      {
        DataSet data = (DataSet)o;
    
        // data の内容をデータベースに挿入
      }
    }
    
    

    という風に、スレッドプールで実行するメソッドに引数として渡すこともできます。

     

     


    なかむら(http://d.hatena.ne.jp/griefworker)
    • 回答としてマーク SweetSmile 2010年11月22日 9:04
    2010年11月22日 2:43
  • 更新するDataRowと新規に追加するDataRowはそれぞれ別のインスタンスですから、そこだけ見れば更新と追加が同時に発生しても問題が発生しないように思えます。ただし、タイミングによって追加なのか更新なのかを正しく判断できず、同じデータを追加しようとすることなどが発生するのであれば、DataTable全体として一貫性を保つようにしなければならないでしょう。


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

     

    2010年11月22日 3:21
    モデレータ
  • TH01さん
    > > スレッドプールを使うときマルチスレッド処理のメソッドは static なので
    > > DataSet も static で宣言することになります(勘違いじゃなければ)。
    > そういう制限はなく、コーディング次第です。

    なかむらさん
    > DataSet は絶対に static にしなければいけない、というわけではありません。たとえば、
    > という風に、スレッドプールで実行するメソッドに引数として渡すこともできます。

    あぁー、そうでした。
    この間自転車に乗りながら「どうしようかぁ」と考えているときに
    インスタンスへの参照を引数で渡せばできると思ったのをすっかりわすれてました。

     

    Th01さん
    > と書かれていますので、安全ではないです。

    trapemiyaさん
    > ただし、タイミングによって追加なのか更新なのかを正しく判断できず、
    > 同じデータを追加しようとすることなどが発生するのであれば、DataTable全体として一貫性を保つようにしなければならないでしょう。

    trapemiya さんご指摘の状況は発生しないはずですが、
    内部でどういう処理をしているかよくわからないので
    やっぱり安全と決めてかかるのはやめた方がよさそうですね。

    TH01さん
    > 各メソッドでのロック対象を同じにするか、各スレッドで発生する追加や更新の要求は
    > 単にキューに溜めるだけにしておいてそのキューを別の専用スレッドで処理してはいかがでしょうか。
    > キューを使う方がロック時間(キューのロック)を最小限にできそうなので。
    そうですね。予想される追加行数を考えると挿入と更新が同時に行えたとしても
    パフォーマンス上のメリットはあまりなさそうなので、その方法で考えてみます。

    TH01さん
    > なお、もしその DataTable 等がコントロールにバインドしている場合、
    > そのバインド先のコントロールに対しては別スレッドからの操作が発生してしまうことになり
    > 不都合が生じますので、その点は気を付けてください。

    そうでした。どこかでちらっとそんな記事か何かを見たような記憶があります。
    追加された行、更新された行に関してはポイントとなるデータ列だけでそれぞれ dataTable を作って
    それぞれを dataGridView からバインドする予定でした。
    control.invoke を使ってUIスレッド上で追加した方がよいですね。
    この場合、次々と invoke が実行されるわけですがキューなどを意識する必要はなく、
    invoke を実行した分から勝手に順番に処理してくれると理解していいのでしょうか。

    2010年11月22日 4:51
  • trapemiya さん
    > 更新するDataRowと新規に追加するDataRowはそれぞれ別のインスタンスですから、そこだけ見れば更新と追加が同時に発生しても問題が発生しないように思えます。

    DataRow が別インスタンスだったとしても、DataRow への操作時に、DataTable 等の別オブジェクトに影響が全くない仕組みになっているとは限りませんので、問題が発生しないとは言い切れないと考えます(追記:MSDN にも書かれていましたので、前の私の返信に追記しました。さらに追記:MSDN の説明は、最低限、1つの DataRow インスタンスに関することなハズですから、それだけではわかりませんね。やっぱり、DataTable に書かれていることが該当すると思います)。

    SweetSmile さん
    > そうでした。どこかでちらっとそんな記事か何かを見たような記憶があります。

    私は最近、これの問題にハマって調べたところでした。
    ここのどこかのフォーラムに体験談を書いちゃおうかなとか思ってやめたんですけど、DataGridView の場合は、編集モードを使わないのでしたら、問題がすぐに表面化するわけではないみたいです。

    > control.invoke を使ってUIスレッド上で追加した方がよいですね。
    > この場合、次々と invoke が実行されるわけですがキューなどを意識する必要はなく、
    > invoke を実行した分から勝手に順番に処理してくれると理解していいのでしょうか。

    そうですね。そう思います。
    ただ、invoke が頻繁になるのは少し心配です。以前の次のスレッドでなんとなくそう思いました。
    (当時は別スレッドで操作するデータにバインドしてはいけないことを知りませんでした。。)

    DataGridViewの高速化
    http://social.msdn.microsoft.com/Forums/ja-JP/csharpexpressja/thread/8a8331ec-7912-4ccb-b57c-a726c01caaf9

    別スレッドで処理される目的次第では意味がなくなるのかもしれませんが、以下の処理はどうでしょうか?

    ・各スレッドではキューに発生データを溜めるだけ。
    ・UIスレッドでは定期的にキューからデータを取得してバインド先(DataTable)を更新する。

    ちなみに先の私の返信時点では、なかむらさんが書かれた ThreadPool.QueueUserWorkItem を知らなかったので調べました。
    自分でスレッドを作成する代わりに、プールされたものが使用できる便利な機能なんですね。
    (知らないまま返信していまして失礼しました。)

    これを使うとロックが不要になるのかも、なんてちょっとだけ思って(私がそう思っただけです)以下のコードで試しましたが、コールバックは各スレッドで実行されるのでロックは必要ですね。
    5秒後とその1秒後にビープがなります。

    ThreadPool.QueueUserWorkItem(
        new WaitCallback(o =>
        {
            System.Threading.Thread.Sleep(5000);
            System.Media.SystemSounds.Beep.Play();
        }));

    ThreadPool.QueueUserWorkItem(
        new WaitCallback(o =>
        {
            System.Threading.Thread.Sleep(6000);
            System.Media.SystemSounds.Beep.Play();
        }));

    • 編集済み TH01 2010年11月22日 6:47 さらに追記
    2010年11月22日 5:47
  • ThreadPoolですが、確かキューの実行開始間隔が0.5秒?とかだったと思うので、

    これを利用してDataRowの更新をUIスレッドで行うとなると、重たい処理になって

    しまう可能性もあります。

    • 回答としてマーク SweetSmile 2010年11月22日 9:04
    2010年11月22日 5:59
  • そうじゃないかもしれないです。
    ThreadPool の MSDN に次の説明がありました。

    ---引用
    スレッド プールのすべてのスレッドがタスクに割り当てられている場合、スレッド プールは新しいアイドル スレッドの作成をすぐには開始しません。スレッドのスタック領域の不要な割り当てを避けるために、新しいアイドル スレッドは間隔を置いて作成されます。この間隔は現在 0.5 秒ですが、.NET Framework の将来のバージョンでは変更される可能性があります。
    ---

    SweetSmile さんの要件にあうかわからないですが、私の目論見としては、表示は遅延してもデータは滞りなく処理されるという感じです。(^^;

    2010年11月22日 6:16
  • >SweetSmile さんの要件にあうかわからないですが、

    >私の目論見としては、表示は遅延してもデータは滞りなく処理されるという感じです。(^^;

     

    たしかにこの場合は要件がわからないので、私もチラシの裏的な感じで書いてます、

    以前スレッドプールを使ってこの間隔で痛い目を見たので(汗


    2010年11月22日 6:30
  • 個人でのプログラミングなので、要件というほどのものはなく
    こういう処理をしたいぐらいのイメージだけで場当たり的に作っちゃってます(汗

    スレッドプールは、@IT の記事でマルチスレッドを扱う方法の一つとして書かれていて
    便利そうだったのであまり何も考えずに採用しました。

    UI に表示されるデータはマルチスレッドで処理されたデータのごく一部になる見込みなので
    特に重い処理にはならないかなと思います。

    プログラムの処理は
    Web 上からマルチスレッドでデータを取得

    過去に取得済みのデータを入れた datat\table をみて、追加されているか更新されているかを確認

    その dataTable にデータを追加または更新
    (これは UIスレッドで行う必要ないですね)

    UI表示用の dataTable(追加・更新のふたつ)にUIで確認できればいいデータだけを挿入
    (これは UIスレッドを使うのが理想かな)

    というかんじです。

    で、
    > 各スレッドで発生する追加や更新の要求は単にキューに溜めるだけにしておいて
    > そのキューを別の専用スレッドで処理してはいかがでしょうか。
    ということでどうすればいいのかがよくわか…。
    ちょっと考えてみます。

    2010年11月22日 9:01
  • DataRow が別インスタンスだったとしても、DataRow への操作時に、DataTable 等の別オブジェクトに影響が全くない仕組みになっているとは限りませんので、問題が発生しないとは言い切れないと考えます(追記:MSDN にも書かれていましたので、前の私の返信に追記しました。さらに追記:MSDN の説明は、最低限、1つの DataRow インスタンスに関することなハズですから、それだけではわかりませんね。やっぱり、DataTable に書かれていることが該当すると思います)。

    一般的にはMSDNに書かれているように安全ではありませんが、今回のケースに限定すれば、DataRowのそれぞれ別インスタンスに対する操作になるため、安全である可能性も十分にあるのではないかと思いました。しかし、あくまで個人的な推測であるため、安全ではない可能性ももちろんあると思います。結局、

    >行挿入メソッドと既存行更新メソッドが同時に実行されることが安全かどうかわかりません。

    に対する回答は、現状のMSDNの情報だけでははっきりわからないというのが私の感想です。DataTableはProposedな行が一時的に生成されたり、BeginEditメソッドなどによっても振る舞いが変わるなど複雑な動きをしますし、元々トランザクション的な考え方も無いと思いますので、なかなか難しい問題のような気がします。
    よって、TH01さんが提案されているような方法で対処するのが確実で現実的だと思います。

     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    2010年11月22日 15:16
    モデレータ
  • SweetSmile さん
    > > そのキューを別の専用スレッドで処理してはいかがでしょうか。
    > ということでどうすればいいのかがよくわか…。

    マルチスレッド化の目的として、大きく分けると次の2つがあると思います。

    a. 速度重視
    b. UIが固まらないように

    今回は a ではなさそうですから、更新については SweetSmile さんが書かれたように invoke でも問題なさそうに思いました。
    また、DataView を活用すれば DataTable も分ける必要もないかも、とも思いました。

    それと、考えると書かれてますが、私の最初の返信について補足させてください。
    最初はバインドの有無は不明でしたし、上記 a の場合として、DataTable を追加・更新する負荷によって本来の処理(発生データを取得する処理)が遅延しないように、取得と格納のスレッドを分けた方がよいと考えたのでした。
    ただし、DataTable はバインドされているとのことですから、b の場合で表示の遅延はそれほど問題にならない場合でしたら、1つ前の返信に書きました方針がよいと思ってます。
    表示の遅延についても、適切に更新頻度を制御する仕組みを設ければ、最小限に抑えることもできると思います。

    trapemiya さん
    > 現状のMSDNの情報だけでははっきりわからないというのが私の感想です。

    私としては、「すべての書き込み操作の同期をとる必要があります」という内容で十分に、安全ではないと考える材料になるととらえました。
    一応、以下のコードで検証してみましたところ、例外が発生することを確認しました。
    問題があっても例外が発生するとは限りませんけど、今回は発生しましたので、これでハッキリしましたよね。

    var dt = new DataTable();
    dt.Columns.Add("col1", typeof(string));
    dt.Rows.Add("a");
    dt.Rows.Add("b");
    var row0 = dt.Rows[0];
    var row1 = dt.Rows[1];

    ThreadPool.QueueUserWorkItem(new WaitCallback(state =>
    {
        for (var i = 0; i < 100000; i++)
            row0[0] = "a";
    }));

    ThreadPool.QueueUserWorkItem(new WaitCallback(state =>
    {
        for (var i = 0; i < 100000; i++)
            row1[0] = "b";
    }));

    2010年11月23日 1:28
  • 問題があっても例外が発生するとは限りませんけど、今回は発生しましたので、これでハッキリしましたよね。

    DataTableに限らず一般論で言えば、同じデータを同時にいじらなければ問題が発生しないはずです。今回のケースではこれに該当するため、本質的に問題が発生しない可能性があると思いました。もし問題が発生するとすれば、DataRowを束ねているDataTable側に何らかの影響がある場合です。今回ご提示されたサンプルコードでは例外が発生しますから、影響があることは明らかです。では、この影響を排除できないでしょうか?
    カラムへセットしている部分をBeginEditメソッドとEndEditメソッドで囲むと、少なくとも例外は発生しなくなりました。しかし、ご指摘の通り、これで問題が発生していないと言い切ることはできません。
    MSDNの、「すべての書き込み操作の同期をとる必要があります」の前提条件を単純に興味本位で疑っているだけであり、あんまり疑っても仕方ないですね。性格悪いかしらん(^^; 

     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

     

    2010年11月23日 15:14
    モデレータ