C# Control.Invoke 與 Control.BeginInvoke 差異

最近寫 threading 時遇到的問題,就整理一下吧 :Q

Control.BeginInvoke() 與 Delegate.BeginInvoke() 的差異

首先先說 Control 的 BeginInvoke() 跟 Delegate 的 BeginInvoke() 是不一樣的。

根據 msdn的說法,Control.BeginInvoke() 之後可以不用作 Control.EndInvoke()。

You can call EndInvoke to retrieve the return value from the delegate, if neccesary, but this is not required. EndInvoke will block until the return value can be retrieved.

如果有呼叫 Control.EndInvoke() 那麼在 BeginInvoke() 的事情做完之前,EndInvoke() 之後的程式會先停住不執行。以下面的程式為例

private delegate void BeginInvokeDelegate(int _i);
private IAsyncResult iar;

private void button4_Click(object sender, EventArgs e)
{
    for (int i = 0; i < 10; i++)
    {
        tbMsg.Text = "a\r\n" + tbMsg.Text;
    }

    iar = this.BeginInvoke(new BeginInvokeDelegate(BeginInvokeMethod), 10);

    this.EndInvoke(iar);

    for (int i = 0; i < 10; i++)
    {
        tbMsg.Text = "b\r\n" + tbMsg.Text;
    }
}

private void BeginInvokeMethod(int _i)
{
    for (int i = 0; i < _i; i++)
    {
        tbMsg.Text = "c\r\n" + tbMsg.Text;
   }
}

在 iar = this.BeginInvoke(new BeginInvokeDelegate(BeginInvokeMethod), 10); 之後,因為有 this.EndInvoke(iar);,所以後面那個印出10個 b 的迴圈會等到印出 c 的 delegate function 做完之後才被執行。

如果把 this.EndInvoke(iar); 拿掉,那麼會發現 b 會先被印出然後才是 c 被印出。

Control.Invoke() 與 Control.BeginInvoke() 的差異

簡單地說,Invoke()是synchronous,BeginInvoke()是asynchronous。繼續拿前面的程式碼做例子

private void button4_Click(object sender, EventArgs e)
{
    for (int i = 0; i < 10; i++)
    {
        tbMsg.Text = "a\r\n" + tbMsg.Text;
    }

    this.Invoke(new BeginInvokeDelegate(BeginInvokeMethod), 10);

    for (int i = 0; i < 10; i++)
    {
        tbMsg.Text = "b\r\n" + tbMsg.Text;
    }
}

上面這段是呼叫Invoke(),所以會先印出 a ,然後同步(synchronous)等到Invoke的事情(印出 c )做完,才繼續把 b 印出。

private void button4_Click(object sender, EventArgs e)
{
    for (int i = 0; i < 10; i++)
    {
        tbMsg.Text = "a\r\n" + tbMsg.Text;
    }

    this.BeginInvoke(new BeginInvokeDelegate(BeginInvokeMethod), 10);

    for (int i = 0; i < 10; i++)
    {
        tbMsg.Text = "b\r\n" + tbMsg.Text;
    }
}

上面這段是呼叫BeginInvoke(),會先印出 a ,然後因為是非同步(asynchronous),所以透過BeginInvoke呼叫BeginInvokeMethod這個函數之後,就會繼續執行印出 b 的程式,最後才印出 c。

但是因為是非同步(asynchronous),那為什麼不會印出 b 的同時也印出 c 呢?因為 button4_Click 這個函數是在 UI thread 裡頭被執行,所以被 Invoke 的 BeginInvokeMethod 這個函數必須要等到 button4_Click 把事情做完之後才能拿到 UI thread 的控制權,才能開始做印出資料的動作。

Thread 與 Invoke()

當新產生出來的 Thread 要對 UI control 進行存取,就需要用到 Invoke(),要不然會有 un-safe access 的問題發生,雖在「大部分」時候不會怎麼樣,但為了以防萬一以及在 debug 時的方便,還是用 Invoke() 比較好。之前有稍微整理過,這邊再把safety invoke()寫清楚一點好了。

private void mainUIThreadFunction()
{
	// other code
	Thread t = new ThreadStart(ThreadProc));
	t.Start();
	// other code
}

public void ThreadProc()
{
	// other code
	IncProgressBar(progressBar1, iIncrement);  // Call function(UIControl, parameters)
	// other code
}

// Define delegate function, the parameters must be the same to the original function
private delegate void IncProgressBarCallback(ProgressBar pb, int inc);

private void IncProgressBar(ProgressBar pb, int inc)  // Define function(UIControl, parameters)
{
	if (pb.InvokeRequired)	// test the UIcontrol requires invoke or not
	{
		// if yes, invoke
		IncProgressBarCallback d = new IncProgressBarCallback(IncProgressBar);
		pb.Invoke(d, new object[] { pb, inc });  // Organize the parameters as an object array
	}
	else
	{
		// if invoke is not necessary, process it directly.
		pb.Increment(inc);
	}
}

在上面的例子中從 UI Thread 產生一個新的 Thread 去執行 ThreadProc(),而在 ThreadProc() 中,想要把進度條 progressBar1 的進度增加,所以呼叫 IncProgressBar() 函數。

在 IncProgressBar() 函數中先去檢查目前這個進度條需不需要做 Invoke,結果因為是在非 UI thread 中呼叫的,所以需要做 invoke,於是把 IncProgressBar() 封裝起來成為一個 delegate function IncProgressBarCallback(),做了 Invoke() 之後,則會把這個封裝丟給 UI thread 去處理。

等到 UI thread 來處理這個封裝時,它會知道裡頭包的是 IncProgressBar() 這個函數,而執行這個函數時,因為是 UI thread,所以這時就不需要 invoke() 了。

Thread.Join()

從 UI thread 中呼叫 Thread.Start() 之後如果用了 Thread.Join(),則程式會被 block 住,直到 Thread 做完 Abort() 為止。那麼這個時候在 Thread 裡頭就不可以再使用 Control.Invoke() 了,要不然會出現死結(deadlock)。因為這時候 UI Thread 要等 Thread.Join(),而Control.Invoke()所呼叫的函數要等 UI Thread 空閒出來處理 delegate function 封裝。

遇到這樣的狀況時,我們要改用非同步(asynchronous)的 BeginInvoke()。如下面的例子

因此這時候要改用 Control.BeginInvoke()。如下面的例子...

private void mainUIThreadFunction()
{
	// other code
	Thread t = new ThreadStart(ThreadProc));
	t.Start();
	// other code
	t.Join(); // thread join. Block the program after thread abort.
	// other code
}

public void ThreadProc()
{
	// other code
	IncProgressBar(progressBar1, iIncrement);  // Call function(UIControl, parameters)
	// other code
	Thread.CurrentThread.Abort();  // thread abort
}

// Define delegate function, the parameters must be the same to the original function
private delegate void IncProgressBarCallback(ProgressBar pb, int inc);

private void IncProgressBar(ProgressBar pb, int inc)  // Define function(UIControl, parameters)
{
	if (pb.InvokeRequired)	// test the UIcontrol requires invoke or not
	{
		// if yes, invoke
		IncProgressBarCallback d = new IncProgressBarCallback(IncProgressBar);
		pb.BeginInvoke(d, new object[] { pb, inc });  // Organize the parameters as an object array
	}
	else
	{
		// if invoke is not necessary, process it directly.
		pb.Increment(inc);
	}
}