それmicrothreadで書くべきだよ


C#でWebBrowserクラスというWebブラウザのclassがある。これが大変使いにくいclassで、何かするごとにwindow messageを処理しないといけない。


WebBrowser.Navigateを呼び出したあとApplication.DoEventsを呼び出しているコードをときどき見かける。古くはMicrosoftの公式のサンプルでもそんなコードが書いてあったと思う。


しかし、Application.DoEventsは本来、単なる画面の一部品ごときが明示的に呼び出して良いようなmethodではない。ボタンのハンドラのなかで、WebBrowser.Navigateして、さらにそのあとApplication.DoEventsを呼び出していると、ボタンのイベント処理中なのに、次のボタンイベントの処理が出来てしまう。これでただしく機能するプログラムが書けるほうがおかしい。


よって、WebBrowser.Navigateのあと、Application.DoEventsを呼び出しているようなコードを書く奴は豆腐の角に頭ぶつけて死んじまって欲しい。


技術系の掲示板で見かけるWebBrowserに関する質問(悩み)の大半は、いつDoEventsを呼び出すべきかと、どうやってDoEventsを呼び出すべきかの二つに集約される。


そうは言っても、ではどうやって回避するのかと言うと私も自信はないのだが、普通は、Timerでも使って、そのcallbackでWebBrowser.Navigateを呼び出したり、別threadを起こして、WebBrowserへのアクセスだけBeginInvokeとか使ってGUI threadに処理させるのかな。たぶん。


でも、そういうイベント駆動型のコードにしてしまうと、コードを直列的に(一連の処理をひとつのmethodのなかで)書きたいのに、コードが散り散りになってしまう。そんなコードを保守するのはまっぴら御免である。


私はこういうのはcontinuationで書いたほうがいいんじゃないかと思う。C#ならyieldを使って次のように疑似的にmicrothreadを用意する。



private IEnumerable<int> SiteCheck(WebBrowser webBrowser, string url)
{
// urlを表示
webBrowser.Url = new Uri(threadUrl);
// 表示が完了するまで待つ
while (webBrowser.ReadyState != WebBrowserReadyState.Complete)
{
yield return 100;
// Application.DoEvents();
// ThreadSleep(100);
// と等価
}


}


こういうことをしようとしたとき、IEnumerable<void>が何故使えないのかとちょっとブルーになるのだけど、せっかくなのでyield returnの時にSleepする時間(単位は[ms])を指定できるようにした。以下に実験のため書き殴ったソースを掲載しておくので適当に改造して使っていただきたい。


あと、GUI threadはThread.SleepするとUIが固まってしまうのでThread.Sleep出来なくて、Thread.Sleepしたいなら別threadを起こしなさいというのが常識だとは思うが、このようにmicrothreadを用いるなら、GUI threadでもThread.Sleep相当のことが可能になる。



public Form1()
{
InitializeComponent();
Application.Idle += new EventHandler(OnIdle);
}

public void OnIdle(object o, EventArgs arg)
{
// 登録されているtaskを順番に実行する。
var removes = new List<TaskContext>();

DateTime Now = DateTime.Now;
foreach (var work in tasks)
{
bool mustRemove = false;
if (work.Task.MoveNext())
{
try
{
DateTime? TimeToExpire = work.Date;
if (TimeToExpire == null || Now <= TimeToExpire)
{
int time = work.Task.Current;
if (time != 0)
{ // この間は再度呼び出されない。
work.Date = DateTime.Now.AddMilliseconds(time);
}
}
}
catch
{
// taskが最後まで実行された扱いにしてこれを除去
mustRemove = true;
}
}
else
{
mustRemove = true;
}


// taskが最後まで実行できたのでこれを除去
if (mustRemove)
removes.Add(work);

}
foreach (var v in removes)
{
tasks.Remove(v);
}
}

private class TaskContext
{
public IEnumerator<int> Task;
public DateTime? Date;
}

private List<TaskContext> tasks = new List<TaskContext>();

private void AddWork(IEnumerable<int> task)
{
tasks.Add(new TaskContext() { Task = task.GetEnumerator(), Date = null });
}


.NET Frameworkでは、thread越えの例外は受け取れないが、microthreadなら同一threadなので、そのなかで起きた例外を捕捉できる。上のプログラムでは、microthread内で例外が発生すると、そのmicrothreadのタスクが終了したと見なしている。


個人的には、microthread側でも大域的な(大きく囲った)try~catchを書きたいことがあるのだが、yieldをtry~catchで挟むことは出来ないというC#の制限(C#3.0でもこの制限は依然として存在する)があるので書くことが出来ない。