2010年3月22日月曜日

第28回 マルチスレッド処理する

UIの処理とタイムライン取得の処理が重なったら重くなるのでマルチスレッド化します。
今回は先にソースコードを紹介して、あとで説明します。

TimelineクラスのUpdate()の代わりに
非同期版の以下のメソッドを呼び出すようにします。



  /// <summary>
  /// Update()の非同期版
  /// </summary>
  public virtual void UpdateAsync() {
   _backgroundWorker = new BackgroundWorker();
   _backgroundWorker.DoWork += this.getItemsAsync;
   _backgroundWorker.RunWorkerCompleted += this.getItemsCompleted;
   _backgroundWorker.RunWorkerAsync();
  }


  /// <summary>
  /// BackgroundWorkerのDoWork
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  protected virtual void getItemsAsync(object sender, DoWorkEventArgs e) {
   //タイムラインからアイテムを取得する
   //※別スレッドで実行される
   using (var response = WebUtility.GetResponse(_url, "GET", new RequestParameter[] { }, _userInfo))
   using (var stream = response.GetResponseStream()) {
    e.Result = _itemsReader.Read(stream).ToList<ITimelineItem>();
   }
  }

  /// <summary>
  /// BackgroundWorkerのCompleted
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  protected virtual void getItemsCompleted(object sender, RunWorkerCompletedEventArgs e) {
   //取得したアイテムを通知する
   //※UIスレッドで実行される
   var result = e.Result as List<ITimelineItem>;
   foreach (var item in result) {
    if (!AllItems.Contains(item)) {
     OnReadNewItem(item);
    }
   }
  }


今回の変更はほぼそれだけです。
マルチスレッドの場合はそれに応じた例外処理も必要ですが、
それはまだしていません。

さて、コードは簡単ですが、起こっていることを理解するのは難しいかもしれませんので
少々詳しく説明します。
BackgroundWorkerをすでに知っていてコードだけでわかった人は以下不要です。

ちなみに、私自身もマルチスレッドはあまり書いたことがないので、
正直以下の説明に自信がありません。
信頼性50%以下くらいだと思って読んでください(汗

読んでいる人がスレッドくらいは知っているものとして進めます。
でもすごく簡単に説明すると、スレッドは「処理の流れ」です。
スレッドを2つ作ると2つの処理を同時に走らせることができます。
実際には2つの処理をかなり短い単位に区切って交互に実行して
平行動作しているように見せる場合もあれば、
マルチコアのCPUでは本当に2つの処理が同時に走っているかもしれません。

WPFはプログラム側でスレッドを作らない限り、
2つのスレッドが動いています。
UIスレッドとレンダリングスレッドです。(用語は正しくないかもしれません)
レンダリングスレッドは描画の高速化のため使われているらしく、
あんまり気にしなくて良さそうです。
つまり今まで書いてきたコードは全部UIスレッド上で動いています。
1つのスレッドで動いているということは
1つの処理が滞れば全部滞るということです。

UIスレッドはDispatcherで処理の順番が管理されていて、
例えばClickイベント処理や描画処理などがキューに登録されて、
優先度を考慮して順番に処理されていきます。
その中にTimerイベント呼び出される、タイムラインをtwitterAPIで取ってくる処理も登録されるので、
twitterが重くてAPIが反応を返さない(返すのが遅い)ときは、
UIの更新やClickの反応の処理の順番がなかなか回ってこないことになり、
アプリケーションの反応が鈍かったりフリーズしたりしているように見えることになります。

ということで、twitter APIの反応を待つところを別スレッドにします。

別スレッドで処理するときは主に3つのことを考えないといけません。
まず、一般的なマルチスレッド処理で問題になる
共有リソースへのアクセスの排他処理です。
あるスレッドがタイムラインのアイテムのリストを読み込んでいる最中に
別のスレッドがリストを追加・削除したりするとおかしなことになるかもしれません。
2つめは、WindorsFormアプリケーションやWPFアプリケーションでは、
UIのControl(Window自体やその上のテキストボックスやラベルなど)を作ったスレッドしか
そのControlを更新できません。
つまり、別スレッドを作ってそこでtwitterのデータを読み込んでいる時に、
直接Windowのラベルに進捗状況やエラーメッセージを書くようなことはできません。
3つめは、別スレッドでの例外は元スレッドで直接catchできません。

で、スレッドを使用するにはいろいろな方法がありますが、
今回はBackgroundWorkerを使います。
More Effective C#でもスレッドプールやBackgroundWorkerを使えと書いてありますので、
興味のある方は読んでみてください。

このクラスを使うと、上に書いた3つの問題のうち最初の2つは現時点では
ほとんど何もしないでもあまり問題が起こらなそうです。

ちょっとためしてみたところ、
BackgroundWorkerのDoWorkイベントは新しく作られた別スレッドで実行され、
RunWorkerCompletedは元スレッドで実行されるようです。

最初のコードに戻って、getItemsAsync()は自分のTimelineのデータにしかアクセスしないので、
他に同時に実行されているタイムラインのデータに干渉しません。
getItemsCompleted()の方はアイテム追加のイベントをViewModelのクラスが受け取るので、
MultiSourceTimelineとかで複数のタイムラインからイベントがあがってくると
問題が起こるかなと思いましたが、
元のUIスレッドで実行されるっていうことは、
(タイムラインに要求を出した順番とは違うかもしれませんが)
同時にではなくおそらく反応が戻ってきた順にDispatcherのキューに登録され順次実行されるので、
同時アクセスの問題は起こらないかなと思います。

タイムラインの読込待ちがすごく長くなって、
同じタイムラインの次のTimerでの呼び出しが起こると同時アクセスでおかしなことになるかもしれませんが、
そこまで長引くことはあまりないかとおもい、今回はやっていません。

ちなみにvirtualにして、TwitterTimelineクラスでoverrideしたものも書いています。
また、もともと1つのUpdate()を2つのメソッドに分けたため、
ToList()を使っています。(そうしなければUsingの外でstreamを読んでおかしなことになります)

問題の3つめの例外の件は今回対処していませんので、
処理を書いたらそのうちまたブログで書きます。

今回のソースコード
http://wtwitter.codeplex.com/SourceControl/changeset/view/43563

0 件のコメント:

コメントを投稿