咱們先來看一段運行時會拋出 InvalidOperationException 異常的代碼段:安全
private void btnThreadA_Click(object sender, EventArgs e)
{
Thread thread = new Thread(ChangeTextBox);
thread.IsBackground = true;
thread.Start();
}
void ChangeTextBox()
{
for (int i = 0; i < 10000; i++)
{
int num = Int32.Parse(txtNum.Text);
num++;
txtNum.Text = num.ToString();
}
}
微軟在子線程修改 UI 線程的控件值時給出的安全限制方案爲:在 VS2005 或者更高版本中,只要不是在控件的建立線程(通常就是指UI主線程)上訪問控件的屬性就會拋出這個錯誤,解決方法就是利用控件提供的 Invoke 和 BeginInvoke 把調用封送回 UI 線程,也就是讓控件屬性修改在UI線程上執行;或者 禁用此安全限制。多線程
解決方案一:解除該控件上對錯誤線程調用的檢查(謹慎使用)。app
public Form2()
{
InitializeComponent();
// 解除 TextBox 對錯誤線程調用的檢查
// 若是要捕獲了對錯誤線程的調用,則爲 true(默認值);不然爲 false
// 對控件權限能夠開放的更大 例如 Control、Form 等
TextBox.CheckForIllegalCrossThreadCalls = false;
}
解決方案二:異步
void ChangeTextBox(string str)
{
txtNum.Text = str;
}
// 增長一個委託
delegate void ChangeTextBoxEventHandler(string str);
// 次循環必須在子線程上運行,而後將最新值傳遞到 UI 線程 文本框纔會即時變化
// 若是循環寫在 ChangeTextBox 函數中,那麼循環真實運行權會交由 UI 線程,你只會直接看見結果,看不到過程
void ThreadRun1()
{
for (int i = 0; i < 10000; i++)
{
int num = int.Parse(txtNum.Text);
num++;
// 使用 Invoke 方法,將函數運行勸交回給 UI 線程
this.Invoke(new ChangeTextBoxEventHandler(ChangeTextBox), num.ToString());
}
}
private void btnThreadB_Click(object sender, EventArgs e)
{
Thread thread = new Thread(ThreadRun1);
thread.IsBackground = true;
thread.Start();
}
這樣已經可以達到效果,但不是微軟案例推薦的寫法。考慮到 ChangeTextBox 方法除了被子線程調用外,也可能被程序其它部分調用。所以,再次修改代碼以下:函數
void ChangeTextBox(string str)
{
// InvokeRequired 值判斷當前修改文本框的請求是否有必要交由 UI 線程來完成
// 若是爲 Ture,說明次訪問控件的行爲來自子線程,則調用 Invoke 方法將代碼執行權交給 UI 線程
// 注意,下面實質上是進行了一次方法回調自身的行爲,區別在於再次調用自身時,已是 UI 線程在執行了
if (this.InvokeRequired)
{
this.Invoke(new ChangeTextBoxEventHandler(ChangeTextBox), str);
}
else
{
txtNum.Text = str;
}
}
// 增長一個委託
delegate void ChangeTextBoxEventHandler(string str);
// 次循環必須在子線程上運行,而後將最新值傳遞到 UI 線程 文本框纔會即時變化
// 若是循環寫在 ChangeTextBox 函數中,那麼循環真實運行權會交由 UI 線程,你只會直接看見結果,看不到過程
void ThreadRun1()
{
for (int i = 0; i < 10000; i++)
{
int num = int.Parse(txtNum.Text);
num++;
ChangeTextBox(num.ToString());
}
}
private void btnThreadB_Click(object sender, EventArgs e)
{
Thread thread = new Thread(ThreadRun1);
thread.IsBackground = true;
thread.Start();
}
與 Invoke 方法相對應的還有 BeginInvoke ()、EndInvoke () 這些異步方法。不管是同步仍是異步,這些方法老是會經過代理從新回到 UI 線程上執行。ui
這些方法向 UI 線程的消息隊列中放入一個消息,當 UI 線程處理這個消息時,就會在本身的上下文中執行傳入的方法。換句話說,凡是使用 BeginInvoke 和 Invoke 調用的線程都是在UI主線程中執行的,因此即便這些方法裏涉及到一些靜態變量,也不用考慮加鎖的問題。this
在咱們應用程序開發過程當中,常常會遇到一些問題,須要使用多線程技術來加以解決。spa
許多種類的應用程序都須要長時間操做,好比:執行一個打印任務,請求一個 Web Service 調用等。用戶在這種狀況下通常會去轉移作其餘事情來等待任務的完成,同時還但願隨時能夠監控任務的執行進度。 線程
爲何在咱們切換應用程序後,會發生屏幕假死的現象呢?代理
這是由於當你切換當前應用程序到後臺再切換回前臺時,系統須要在屏幕上重畫整個用戶界面。可是應用程序正在執行長任務,根本沒有時間處理用戶界面的重畫,問題就會發生。如何解決問題呢?咱們須要將長任務放在後臺運行,把用戶界面線程解放出來,所以咱們須要另一個線程。
如何避免多線程的窗體資源訪問的安全問題呢?其實很是簡單,有兩種方法:
狀況一:
delegate void ShowProgressDelegate(int totalStep, int currentStep);
delegate void RunTaskDelegate(int seconds);
void ShowProgress(int totalStep, int currentStep)
{
progressBar1.Maximum = totalStep;
progressBar1.Value = currentStep;
}
void RunTask(int seconds)
{
ShowProgressDelegate showProgress = new ShowProgressDelegate(ShowProgress);
// 每 1/4 秒顯示一次進度
for (int i = 0; i < seconds * 4; i++)
{
Thread.Sleep(250);
this.Invoke(showProgress, new object[] { seconds * 4, i + 1 });
}
}
private void button1_Click(object sender, EventArgs e)
{
RunTaskDelegate runTask = new RunTaskDelegate(RunTask);
// 委託異步調用方式
runTask.BeginInvoke(Convert.ToInt32(this.textBox1.Text), null, null);
}
狀況二:
delegate void ShowProgressDelegate(int totalStep, int currentStep);
delegate void RunTaskDelegate(int seconds);
void ShowProgress(int totalStep, int currentStep)
{
if (progressBar1.InvokeRequired)
{
ShowProgressDelegate showProgress = new ShowProgressDelegate(ShowProgress);
this.BeginInvoke(showProgress, new object[] { totalStep, currentStep });
}
else
{
progressBar1.Maximum = totalStep;
progressBar1.Value = currentStep;
}
}
void RunTask(int seconds)
{
// 每 1/4 秒顯示一次進度
for (int i = 0; i < seconds * 4; i++)
{
Thread.Sleep(250);
ShowProgress(seconds * 4, i + 1);
}
}
private void button1_Click(object sender, EventArgs e)
{
RunTaskDelegate runTask = new RunTaskDelegate(RunTask);
// 委託異步調用方式
runTask.BeginInvoke(Convert.ToInt32(this.textBox1.Text), null, null);
}