2017年8月15日 星期二

.NET C# event EventHandler 與 delegate 之同步與非同步研究與測試

當您需要實作一個具有事件訂閱的功能,您可以使用 event 這個關鍵字或者 delegate 來實踐,不過,這兩者在當要訂閱事件的時候,卻有著不同的用法與注意事項;另外,當要執行訂閱事件的方法時候,究竟是採用多執行續的方式,也就是可以同時執行多個訂閱事件方法,還是採用同步的方式來執行?

了解更多關於 [使用委派的使用方式
了解更多關於 [使用事件的使用方式
了解更多關於 [C# 程式設計手冊]


關於這些疑問,我們將會透過底下的測試程式,來進行說明。

測試程式

    public class Worker
    {
        public delegate void MyHandler(object sender, EventArgs args);
        public MyHandler MyDelegateHandler;
        public event MyHandler MyDelegateEventHandler;
        public event EventHandler MyEventHandler;

        public void Dowork()
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine($"Dowork {i}");
                OnWorkByEvent(i);
                OnWorkByDelegate(i);
                OnWorkByDelegateEvent(i);
            }
        }

        private void OnWorkByEvent(int i)
        {
            var fooHandler = MyEventHandler;
            if (fooHandler != null)
            {
                Console.WriteLine($"OnWorkByEvent raise {i}");
                fooHandler(this, EventArgs.Empty);
            }
        }

        private void OnWorkByDelegate(int i)
        {
            var fooHandler = MyDelegateHandler;
            if (fooHandler != null)
            {
                Console.WriteLine($"OnWorkByDelegate raise {i}");
                fooHandler(this, EventArgs.Empty);
            }
        }

        private void OnWorkByDelegateEvent(int i)
        {
            var fooHandler = MyDelegateEventHandler;
            if (fooHandler != null)
            {
                Console.WriteLine($"OnWorkByDelegateEvent raise {i}");
                fooHandler(this, EventArgs.Empty);
            }
        }

    }
    class Program
    {
        public static Worker fooWorker = new Worker();
        public static Random Rm = new Random(DateTime.Now.Millisecond);

        static void Main(string[] args)
        {
            fooWorker.MyDelegateHandler += MyDelegateListener;
            fooWorker.MyEventHandler += MyDelegateListener;
            fooWorker.MyDelegateEventHandler += MyDelegateListener;
            fooWorker.Dowork();
            Console.ReadKey();
        }

        private static void MyDelegateListener(object sender, EventArgs args)
        {
            var foo = Rm.Next(500, 2000);
            Console.WriteLine($"Sleep {foo}");
            Thread.Sleep(Rm.Next(500, 2000));
            Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
        }
    }
底下是這個範例程式的執行結果
Dowork 0
OnWorkByEvent raise 0
Sleep 1930
1
OnWorkByDelegate raise 0
Sleep 882
1
OnWorkByDelegateEvent raise 0
Sleep 1169
1
Dowork 1
OnWorkByEvent raise 1
Sleep 579
1
OnWorkByDelegate raise 1
Sleep 1355
1
OnWorkByDelegateEvent raise 1
Sleep 1734
1
Dowork 2
OnWorkByEvent raise 2
Sleep 1570
1
OnWorkByDelegate raise 2
Sleep 1035
1
OnWorkByDelegateEvent raise 2
Sleep 539
1
Dowork 3
OnWorkByEvent raise 3
Sleep 642
1
OnWorkByDelegate raise 3
Sleep 1499
1
OnWorkByDelegateEvent raise 3
Sleep 1151
1
Dowork 4
OnWorkByEvent raise 4
Sleep 651
1
OnWorkByDelegate raise 4
Sleep 1163
1
OnWorkByDelegateEvent raise 4
Sleep 1536
1

解釋說明

在測試程式中,我們產生一個 Worker 類別,在這個類別中,宣告了
  • MyDelegateHandler
    這是一個委派物件
  • MyDelegateEventHandler
    這是一個事件,使用 event 來宣告,不過,使用一個 delegate 委派來宣告該事件的事件方法。
  • MyEventHandler
    這是一個事件,使用 event 來宣告,不過,使用的是 EventHandler 這個類別,來宣告該事件的事件方法。
關於這三者的差異點在於,MyDelegateEventHandler & MyEventHandler 的這兩種做法,其實是一樣的,因為 EventHandler 的定義是:
public delegate void EventHandler(object sender, EventArgs e);
因此,EventHandler 其實就是一個委派的類別宣告,這與我們自己宣告的 MyHandler 其實表示的都是一樣的。因此,MyDelegateEventHandler & MyEventHandler 作法與結果都是相同的。
不論是否有使用 event 關鍵字,當使用了 += 運算子要把訂閱事件加入的時候,我們從下圖看到,這個時候 MyDelegateHandler 這個物件當時是 null 空值,不過,因為使用了 += 運算子,卻是可以把 MyDelegateListener 監聽事件方法加入進來。
MyDelegateHandler
當繼續執行下一行程式碼,並不會因為 MyDelegateHandler 為空值,就產生了例外異常,我們可以從下圖看到,這個時候,MyDelegateHandler 已經有值了。
MyDelegateHandler2
最後,對於當我們在執行 Dowork 方法的時候,我們會判斷這些事件或者委派物件是否為空值,若不為空值,則就會依序執行這些訂閱的方法;不過,您會看到,這是採用同步的執行方式,若某次的監聽事件訂閱方法執行過久,例如,需要10分鐘的時間, Dowork 的下一次迴圈動作,將也會延遲10分鐘。
若您不想要這麼處理,您可以使用多執行續的方式,讓這些監聽訂閱事件可以非同步的執行,這樣,Dowrk的迴圈就可以很快速地繼續執行下去。
想要做到這樣,我們可以使用 ThreadPool.QueueUserWorkItem 來啟動多執行續的運行,而 ThreadPool.QueueUserWorkItem 方法需要一個 WaitCallback 委派物件,其定義如下:
public delegate void WaitCallback(object state);
也就是說,WaitCallback 這個委派物件,需要一個可以傳入一個引數的方法,因此,我們使用底下的方式來啟動多執行續來運行。
                ThreadPool.QueueUserWorkItem(s =>
                {
                    fooHandler(this, EventArgs.Empty);

                });
最後,請將 Worker 這個類別修改成為底下的定義,當要執行這些訂閱事件的時候,就是使用不同執行續來執行。
    public class Worker
    {
        public delegate void MyHandler(object sender, EventArgs args);
        public MyHandler MyDelegateHandler;
        public event MyHandler MyDelegateEventHandler;
        public event EventHandler MyEventHandler;

        public void Dowork()
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine($"Dowork {i}");
                OnWorkByEvent(i);
                OnWorkByDelegate(i);
                OnWorkByDelegateEvent(i);
            }
        }

        private void OnWorkByEvent(int i)
        {
            var fooHandler = MyEventHandler;
            if (fooHandler != null)
            {
                Console.WriteLine($"OnWorkByEvent raise {i}");
                //fooHandler(this, EventArgs.Empty);
                ThreadPool.QueueUserWorkItem(s =>
                {
                    fooHandler(this, EventArgs.Empty);
                });
            }
        }

        private void OnWorkByDelegate(int i)
        {
            var fooHandler = MyDelegateHandler;
            if (fooHandler != null)
            {
                Console.WriteLine($"OnWorkByDelegate raise {i}");
                //fooHandler(this, EventArgs.Empty);
                ThreadPool.QueueUserWorkItem(s =>
                {
                    fooHandler(this, EventArgs.Empty);
                });
            }
        }

        private void OnWorkByDelegateEvent(int i)
        {
            var fooHandler = MyDelegateEventHandler;
            if (fooHandler != null)
            {
                Console.WriteLine($"OnWorkByDelegateEvent raise {i}");
                //fooHandler(this, EventArgs.Empty);
                ThreadPool.QueueUserWorkItem(s =>
                {
                    fooHandler(this, EventArgs.Empty);
                });
            }
        }

    }
    class Program
    {
        public static Worker fooWorker = new Worker();
        public static Random Rm = new Random(DateTime.Now.Millisecond);

        static void Main(string[] args)
        {
            fooWorker.MyDelegateHandler += MyDelegateListener;
            fooWorker.MyEventHandler += MyDelegateListener;
            fooWorker.MyDelegateEventHandler += MyDelegateListener;
            fooWorker.Dowork();
            Console.WriteLine("Please Wait...");
            Console.ReadKey();
        }

        private static void MyDelegateListener(object sender, EventArgs args)
        {
            var foo = Rm.Next(500, 2000);
            Console.WriteLine($"Sleep {foo}");
            Thread.Sleep(Rm.Next(500, 2000));
            Console.WriteLine($"Thread ID is : {Thread.CurrentThread.ManagedThreadId}");
        }
    }
此時,執行結果如下,我們可以看到, Dowork 很快就執行完畢,不過,訂閱的事件,卻是非同步的執行,因為每個訂閱監聽事件要停留的時間都不一致,因此,結束執行時間也不太一樣。
Dowork 0
OnWorkByEvent raise 0
OnWorkByDelegate raise 0
OnWorkByDelegateEvent raise 0
Dowork 1
OnWorkByEvent raise 1
OnWorkByDelegate raise 1
OnWorkByDelegateEvent raise 1
Dowork 2
OnWorkByEvent raise 2
OnWorkByDelegate raise 2
Sleep 1240
Sleep 667
Sleep 797
Sleep 1124
Sleep 1441
Sleep 583
Sleep 1060
OnWorkByDelegateEvent raise 2
Dowork 3
OnWorkByEvent raise 3
OnWorkByDelegate raise 3
OnWorkByDelegateEvent raise 3
Dowork 4
OnWorkByEvent raise 4
OnWorkByDelegate raise 4
OnWorkByDelegateEvent raise 4
Please Wait...
Sleep 769
Thread ID is : 8
Sleep 1224
Thread ID is : 10
Sleep 837
Thread ID is : 5
Sleep 708
Thread ID is : 8
Sleep 1077
Thread ID is : 6
Sleep 747
Thread ID is : 5
Sleep 1061
Thread ID is : 3
Sleep 1925
Thread ID is : 4
Thread ID is : 10
Thread ID is : 9
Thread ID is : 7
Thread ID is : 3
Thread ID is : 6
Thread ID is : 8
Thread ID is : 5

了解更多關於 [使用委派的使用方式
了解更多關於 [使用事件的使用方式
了解更多關於 [C# 程式設計手冊]







2017年8月14日 星期一

.NET & C# 研究筆記今天正式開張

由於某些問題,不得不開啟一個新的部落格,用來將準備研究與測試的 .NET / C# 相關心得與筆記,在這裡記錄下來。