- PR -

イベントハンドラーの中でさらにイベントを起こすと、再帰的に呼び出されてしまう

投稿者投稿内容
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2008-04-11 19:48
もうひとつのやりかたとして、「0秒後に発火するようなタイマー」を使ったものです。
System.Timers.Timer クラスを使っています。
コード:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace HogeApp
{
    public partial class Form1 : Form
    {
        private Hoge hoge;

        public Form1()
        {
            InitializeComponent();
            hoge = new Hoge(this);
            hoge.HogeHandler += new HogeEventHandler(hoge_HogeHandler);
        }

        private void hoge_HogeHandler(object sender, HogeEventArgs e)
        {
            Console.WriteLine("hoge_HogeHandler");
            hoge.foo();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            hoge.foo();
        }
    }

    class HogeEventArgs : EventArgs
    {
        public HogeEventArgs()
        {
        }
    }

    delegate void HogeEventHandler(object sender, HogeEventArgs e);

    class Hoge
    {
        public event HogeEventHandler HogeHandler;

        private System.Timers.Timer timer = new System.Timers.Timer();

        public Hoge(ISynchronizeInvoke obj)
        {
            timer.AutoReset = false;
            timer.Interval = double.Epsilon;
            timer.SynchronizingObject = obj;

            timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);
        }

        private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            if (HogeHandler != null)
            {
                HogeHandler(this, new HogeEventArgs());
            }
        }

        public void foo()
        {
            timer.Start();
        }
    }
}

unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2008-04-12 19:43
引用:

otfさんの書き込み (2008-04-08 12:03) より:
要するに非同期処理を同期的に行いたいって事ですか?
その上で繰り返しを行う。


ありがとうございます。非同期ではなくても良いのですが、そのような感じのことです。

同じ人が、複数のやりかたで書くとこうなる、という対比も兼ねて、教えていただいたコードを、私が書いているクラス名やメソッド名に換えてみました。
コード:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace HogeApp
{
    public partial class Form1 : Form
    {
        private Hoge hoge = new Hoge();

        public Form1()
        {
            InitializeComponent();
            hoge.HogeHandler += new HogeEventHandler(hoge_HogeHandler);
        }

        private void hoge_HogeHandler(object sender, HogeEventArgs e)
        {
            Console.WriteLine("hoge_HogeHandler");
            hoge.foo();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            hoge.foo();
        }
    }

    class HogeEventArgs : EventArgs
    {
        public HogeEventArgs()
        {
        }
    }

    delegate void HogeEventHandler(object sender, HogeEventArgs e);

    class Hoge
    {
        public event HogeEventHandler HogeHandler;
        private ThreadStart ts;

        public Hoge()
        {
            ts = NavigateWork;
        }

        public void foo()
        {
            IAsyncResult ar = ts.BeginInvoke(OnCompleted, ts);
            ar.AsyncWaitHandle.WaitOne();
        }

        private void NavigateWork()
        {
        }

        protected virtual void OnCompleted(IAsyncResult ar)
        {
            ts.EndInvoke(ar);
            if (HogeHandler != null)
            {
                HogeHandler(this, new HogeEventArgs());
            }
        }
    }
}



引用:

indigo-xさんの書き込み (2008-04-08 21:41) より:
すでに出ていますが、(Windows系)Control.Invoke(or MethodInvoker)の
(非同期)を使用すればMainスレッドにキューしたのと同じ動作になったはず
(ひょっとしたら同一スレッドの場合はならないかも?知れませんが)
これをうまく使うのも手と思います。


上述のコードを動かして、コールスタックを見てみると、おっしゃるように、スレッドとしては「Mainスレッド」とは別のものになっていました。しかし、そのスレッドが「Mainスレッド」に同期して使えるので使用上は問題ないと思います。むしろバリバリのマルチスレッドを使うのならば、そちらのほうが都合が良いですよね。

ただ、私が前回書いた、「0秒後に発火するようなタイマー」で、System.Timers.Timer クラスを使い、SynchronizingObject プロパティーで Form を指定すると、コールスタックを見る限り、スレッドは「Mainスレッド」のようです。(間にいろいろと挟まってはいますが。)
これはどういう仕組みで実現できているのかが不思議です。
otf
ベテラン
会議室デビュー日: 2006/08/04
投稿数: 91
投稿日時: 2008-04-12 21:54
うーん。
なんでループでなく再帰である必要があるんでしょうか?

引用:

unibonさんの書き込み (2008-04-12 19:43) より:
ただ、私が前回書いた、「0秒後に発火するようなタイマー」で、System.Timers.Timer クラスを使い、SynchronizingObject プロパティーで Form を指定すると、コールスタックを見る限り、スレッドは「Mainスレッド」のようです。(間にいろいろと挟まってはいますが。)
これはどういう仕組みで実現できているのかが不思議です。


ISynchronizeInvokeのBeginInvokeがControlではWin32APIのPostMessageによって実装されているので
この動作はウインドウメッセージのキューイングによるものです。たぶん・・・。
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2008-04-12 23:27
引用:

otfさんの書き込み (2008-04-12 21:54) より:
うーん。
なんでループでなく再帰である必要があるんでしょうか?


私が想定しているアプリケーションの例としては、たとえば多人数でできるチャットプログラムみたいなものです。
Hoge クラスのひとつのインスタンスという共有される掲示板に対して、Aさんが発言(Hoge クラスの foo メソッドを呼ぶ)すると、チャットに参加している全員にその発言が通知(HogeHandler イベントがレイズ)されます。
また、全員に通知するのが自然だと考え、BさんやCさん以外にも、発言した本人であるAさん自身にも通知されます。
ここで、参加者が過去の発言の履歴を状態を持たない場合もあるとします。(Aさんが発言しても、Aさんがイベントを受けた時点で、自分の発言によるものだということが分からない。)
Aさんの発言、という通知を受けたら、その発言内容をもとにして、今度はBさん(やCさんやはたまたAさん自身)が別の発言をする、という感じです。

こういうアプリケーションをイベントを使って作ると、私のような再帰の問題が出てくると思います。

引用:

otfさんの書き込み (2008-04-12 21:54) より:
ISynchronizeInvokeのBeginInvokeがControlではWin32APIのPostMessageによって実装されているので
この動作はウインドウメッセージのキューイングによるものです。たぶん・・・。


ありがとうございます。
渋木宏明(ひどり)
ぬし
会議室デビュー日: 2004/01/14
投稿数: 1155
お住まい・勤務地: 東京
投稿日時: 2008-04-12 23:50
引用:

こういうアプリケーションをイベントを使って作ると、私のような再帰の問題が出てくると思います。



そういう場合、イベント引数とかに「誰が発したイベントなのか」を示すヒントを入れておいて、それを元にイベントハンドラの先頭で再帰しないようにガードするもんじゃないですか?
otf
ベテラン
会議室デビュー日: 2006/08/04
投稿数: 91
投稿日時: 2008-04-13 00:09
チャットプログラムの例は渋木宏明様が言っているように
再帰にならない(なるべきでない)ので別問題だと思います。

私が問題と考えているのは巡回アプリケーションの例のように
繰り返し処理をするのに再帰を使用してる場面です。
このような場合、私が前回提示したサンプルコードのように非同期処理を同期的に行うことによって
再帰ではなくループを使うことができると思うのですが
れい
ぬし
会議室デビュー日: 2005/11/01
投稿数: 346
投稿日時: 2008-04-13 05:23
うーん。
なんか、いろいろ難儀ですねぇ。
簡単なことは簡単にやりましょう。

長い投稿になりますが。

まず…
引用:

unibonさんの書き込み (2008-04-12 19:43) より:
上述のコードを動かして、コールスタックを見てみると、おっしゃるように、スレッドとしては「Mainスレッド」とは別のものになっていました。しかし、そのスレッドが「Mainスレッド」に同期して使えるので使用上は問題ないと思います。むしろバリバリのマルチスレッドを使うのならば、そちらのほうが都合が良いですよね。



indigo-xさんは正しいのですが、
unibonさんの理解は間違っています。

Control.Invoke/Control.BeginInvokeを用いた場合、
メッセージループをまわしているスレッド=
フォームを開いたスレッド=
コントロールにアクセスしていい唯一のスレッドで
指定したメソッドが呼ばれます。

Delegete.BeginInvokeの場合は全然関係ない他のスレッドで呼ばれるのは当然です。

また、Contol.Invokeの場合は同期、Contol.BeginInvokeの場合は非同期で呼ばれます。
これは呼び出し元スレッドがメッセージループをまわしているスレッドであっても同じです。
内部的には、indigo-xさんの言うとおり、ウィンドウメッセージで処理されています。

Control.BeginInvokeを用いれば任意のメソッドを同一スレッドで非同期に実行できます。
その際、戻り値のIAsyncResult.EndInvokeは呼ばなくても問題ありません。

言っていたWebBrowserの件をBeginInvokeで実装すると、
こんな感じになります。
URI取得部分以外はごく簡単。実質3行とか?

#VBだけど。
コード:
Public Class Form1

    Private Delegate Sub StartNavigationDelegate(ByVal target As Uri)

    Private Sub StartNavigation(ByVal target As Uri)
        Me.TextBox1.Text = target.ToString()
        Me.WebBrowser1.Navigate(target)
    End Sub

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Me.StartNavigation(New Uri(Me.TextBox1.Text))
    End Sub

    Private Sub WebBrowser1_DocumentCompleted(ByVal sender As Object, ByVal e As System.Windows.Forms.WebBrowserDocumentCompletedEventArgs) Handles WebBrowser1.DocumentCompleted
        Dim target As UriBuilder
        Try
            For Each element As HtmlElement In Me.WebBrowser1.Document.Links
                target = New UriBuilder(Me.WebBrowser1.Document.Links(0).GetAttribute("href"))
                If target.Scheme <> "http" AndAlso target.Scheme <> "https" Then Continue For
                Me.BeginInvoke(New StartNavigationDelegate(AddressOf StartNavigation), target.Uri)
            Next
        Catch ex As Exception
            MessageBox.Show(ex.ToString())
        End Try
    End Sub

End Class



ただ、このBeginInvokeには問題があって、
フォーカスの移動をしたりとか、
フォームを閉じたりとか、
IMEがごにょごにょしたりとか、
そういったことと重なるとたまにエラーを吐きます。
(昔はかなりの頻度でフリーズでした。今はたまに例外を吐く程度ですが、ごくまれに…。)
他の細かい事情なども考えると、私はあまりオススメしません。

Application.Idleを使うと以下のようになります。
見ればすぐわかりますが、Timerでもほぼ同じコードになります。
ですが、意味的にもパフォーマンス的にもApplication.Idleのほうがいいでしょう。

コード:
Public Class Form1

    Private nexttarget As Uri

    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        AddHandler Application.Idle, New EventHandler(AddressOf Application_Idle)
    End Sub

    Private Sub Application_Idle(ByVal sender As Object, ByVal e As EventArgs)
        If Me.nexttarget IsNot Nothing Then StartNavigation()
    End Sub

    Private Sub StartNavigation()
        Me.TextBox1.Text = nexttarget.ToString()
        Me.WebBrowser1.Navigate(nexttarget)
        nexttarget = Nothing
    End Sub

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Me.nexttarget = New Uri(Me.TextBox1.Text)
    End Sub

    Private Sub WebBrowser1_DocumentCompleted(ByVal sender As Object, ByVal e As System.Windows.Forms.WebBrowserDocumentCompletedEventArgs) Handles WebBrowser1.DocumentCompleted
        Dim target As UriBuilder
        Try
            For Each element As HtmlElement In Me.WebBrowser1.Document.Links
                target = New UriBuilder(Me.WebBrowser1.Document.Links(0).GetAttribute("href"))
                If target.Scheme <> "http" AndAlso target.Scheme <> "https" Then Continue For
                nexttarget = target.Uri
                Exit Sub
            Next
        Catch ex As Exception
            MessageBox.Show(ex.ToString())
        End Try
    End Sub

End Class



メッセージループやイベントドリブンの仕組みを考えれば簡単にわかりますが、
どの手法を使うにしろ、(シングルスレッドで行うかぎり)
何らかの手法でメッセージループからイベントを起こしてもらわないといけません。
これはイベントドリブン一般に言えることです。
そのため、イベントドリブンな仕組みでは
「ユーザーメッセージ」や「ユーザーイベント」をキューに入れられるのが普通です。

Win32ではユーザーメッセージをPostMessageするというのが普通でした。
.NetではおそらくControl.BeginInvokeを使うことを想定していたのだと思います。
残念ながらうまく動きませんので、私はTimerやApplication.Idleを使います。

キューにタスクをいれずに、かつUIをブロックせずに、
なおかつスタックが膨らまないよう処理をするのは
原理的に不可能です。
indigo-x
大ベテラン
会議室デビュー日: 2008/02/21
投稿数: 207
お住まい・勤務地: 太陽の塔近く
投稿日時: 2008-04-13 07:37
引用:

れいさんの書き込み (2008-04-13 05:23) より:

Win32ではユーザーメッセージをPostMessageするというのが普通でした。
.NetではおそらくControl.BeginInvokeを使うことを想定していたのだと思います。
残念ながらうまく動きませんので、私はTimerやApplication.Idleを使います。




時間がちょっとあったのでControl.BeginInvokeを使って作ってみましたが
確かにDispatcherがイマイチうまく動かないですね(仕様と思いますが)

(スタックは消費しないですがCALLとよく似た動作にしてますね
 多分、将来ウィンドウメッセージを消したい意向があるのでしょう。。。。多分)

れいさんに言われる方が.NETらしい作りかもしれません。
(私のやり方が強引かもしれません。強引はやめて、やさしく。。)

スキルアップ/キャリアアップ(JOB@IT)