C#多線程操作界面控件的解決方案

轉載:https://www.cnblogs.com/runner/archive/2011/12/30/2307576.html

C#中利用委託實現多線程跨線程操作

在使用VS2005的時候,如果你從非創建這個控件的線程中訪問這個控件或者操作這個控件的話就會拋出這個異常。這是微軟爲了保證線程安全以及提高代碼的效率所做的改進,但是也給大家帶來很多不便。
其實解決這個問題有兩種方法:
一,是通過設置
System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;
在你的程序初始化的時候設置了這個屬性,而且在你的控件中使用的都是微軟Framework類庫中的控件的話,系統就不會再拋出你上面所說的這個錯誤了。
二,就是委託了,個人建議用這種方法
首先在WinForm窗體中拖入ListBox控件,然後參照以下代碼:

Thread t1;//聲明一個全局線程 private void Form1_Load(object sender, EventArgs e) { t1 = new Thread(new ThreadStart(BackgroundProcess)); t1.Start(); //開始 } delegate void aa(); private void BackgroundProcess() { // 將委託實例化 aa a= delegate() { for (int i = 0; i < 50; i++) { listBox1.Items.Add("Iterations: " + i.ToString()); Thread.Sleep(300); listBox1.Refresh(); } }; listBox1.Invoke(a); }



來源:http://www.huomo.cn/developer/article-1aa8.html

C#多線程操作界面控件的解決方案

我們在做winform應用的時候,大部分情況下都會碰到使用多線程控制界面上控件信息的問題。然而我們並不能用傳統方法來做這個問題,下面我將詳細的介紹。­
      首先來看傳統方法:

public partial class Form1 : Form­ {­ public Form1()­ {­ InitializeComponent();­ }­ private void Form1_Load(object sender, EventArgs e)­ {­ Thread thread = new Thread(ThreadFuntion);­ thread.IsBackground = true;­ thread.Start();­ }­ private void ThreadFuntion()­ {­ while (true)­ {­ this.textBox1.Text = DateTime.Now.ToString();­ Thread.Sleep(1000);­ }­ }­ }­



運行這段 代碼,我們會看到系統拋出一個異常:Cross-thread operation not valid:Control 'textBox1' accessed from a thread other than the thread it was created on . 這是因爲.net 2.0以後加強了安全機制,不允許在winform中直接跨線程訪問控件的屬性。那麼怎麼解決這個問題呢,下面提供幾種方案。­
      第一種方案,我們在Form1_Load()方法中加一句代碼:­

private void Form1_Load(object sender, EventArgs e)­ {­ Control.CheckForIllegalCrossThreadCalls = false;­ Thread thread = new Thread(ThreadFuntion);­ thread.IsBackground = true;­ thread.Start();­ }­


加入這句代碼以後發現程序可以正常運行了。這句代碼就是說在這個類中我們不檢查跨線程的調用是否 合法(如果沒有加這句話運行也沒有異常,那麼說明系統以及默認的採用了不檢查的方式)。然而,這種方法不可取。我們查看 CheckForIllegalCrossThreadCalls 這個屬性的定義,就會發現它是一個static的,也就是說無論我們在項目的什麼地方修改了這個值,他就會在全局起作用。而且像這種跨線程訪問是否存在異 常,我們通常都會去檢查。如果項目中其他人修改了這個屬性,那麼我們的方案就失敗了,我們要採取另外的方案。­
      下面來看第二種方案,就是使用delegate和invoke來從其他線程中控制控件信息。網上有很多人寫了這種控制方式,然而我看了很多這種帖子,表明上看來是沒有什麼問題的,但是實際上並沒有解決這個問題,首先來看網絡上的那種不完善的方式:­

public partial class Form1 : Form­ {­ private delegate void FlushClient();//代理­ public Form1()­ {­ InitializeComponent();­ }­ private void Form1_Load(object sender, EventArgs e)­ {­ Thread thread = new Thread(CrossThreadFlush);­ thread.IsBackground=true;­ thread.Start();­ }­ private void CrossThreadFlush()­ {­ //將代理綁定到方法 ­ FlushClient fc = new FlushClient(ThreadFuntion);­ this.BeginInvoke(fc);//調用代理­ }­ private void ThreadFuntion()­ {­ while (true)­ {­ this.textBox1.Text = DateTime.Now.ToString();­ Thread.Sleep(1000);­ }­ }­ }­



使用這種方式我們可以看到跨線程訪問的異常沒有了。但是新問題出現了,界面沒有響應了。爲什麼會出現這個問題,我們只是讓新開的線程無限循環刷新,理論上 應該不會對主線程產生影響的。其實不然,這種方式其實相當於把這個新開的線程“注入”到了主控制線程中,它取得了主線程的控制。只要這個線程不返回,那麼 主線程將永遠都無法響應。就算新開的線程中不使用無限循環,使可以返回了。這種方式的使用多線程也失去了它本來的意義。 ­
      現在來讓我們看看推薦的解決方案:­

public partial class Form1 : Form­ {­ private delegate void FlushClient();//代理­ public Form1()­ {­ InitializeComponent();­ }­ private void Form1_Load(object sender, EventArgs e)­ {­ Thread thread = new Thread(CrossThreadFlush);­ thread.IsBackground = true;­ thread.Start();­ }­ private void CrossThreadFlush()­ {­ while (true)­ {­ //將sleep和無限循環放在等待異步的外面­ Thread.Sleep(1000);­ ThreadFunction();­ }­ }­ private void ThreadFunction()­ {­ if (this.textBox1.InvokeRequired)//等待異步­ {­ FlushClient fc = new FlushClient(ThreadFunction);­ this.Invoke(fc);//通過代理調用刷新方法­ }­ else­ {­ this.textBox1.Text = DateTime.Now.ToString();­ }­ }­ }­


運行上述代碼,我們可以看到問題已經被解決了,通過等待異步,我們就不會總是持有主線程的控制,這樣就可以在不發生跨線程調用異常的情況下完成多線程對winform多線程控件的控制了。­

      對於深山老林提出的問題,我最近找到了更優的解決方案,利用了delegate的異步調用,大家可以看看:­

public partial class Form1 : Form­ {­ private delegate void FlushClient();//代理­ public Form1()­ {­ InitializeComponent();­ }­ private void Form1_Load(object sender, EventArgs e)­ {­ Thread thread = new Thread(CrossThreadFlush);­ thread.IsBackground = true;­ thread.Start();­ }­ private void CrossThreadFlush()­ {­ FlushClient=new FlushClient(ThreadFunction);­ FlushClient.BeginInvoke(null,null);­ }­ private void ThreadFunction()­ {­ while (true)­ {­ this.textBox1.Text = DateTime.Now.ToString();­ Thread.Sleep(1000);­ }­ ­ }­ }­


這種方法也可以直接簡化爲(因爲delegate的異步就是開了一個異步線程): ­

public partial class Form1 : Form­ {­ private delegate void FlushClient();//代理­ public Form1()­ {­ InitializeComponent();­ }­ private void Form1_Load(object sender, EventArgs e)­ {­ Thread thread = new Thread(CrossThreadFlush);­ FlushClient=new FlushClient(ThreadFunction); ­ FlushClient.BeginInvoke(null,null);­ }­ private void ThreadFunction()­ {­ while (true)­ {­ this.textBox1.Text = DateTime.Now.ToString();­ Thread.Sleep(1000);­ }­ ­ }­ }

 

 

文章摘自:

C #中的幾個線程同步對象方法

- 張小魚 2010-10-22 08:40

在編寫多線程程序時無可避免會遇到線程的同步問題。什麼是線程的同步呢?

舉 個例子:如果在一個公司裏面有一個變量記錄某人T的工資count=100,有兩個主管A和B(即工作線程)在早一些時候拿了這個變量的值回去 ,過了一段時間A主管將T的工資加了5塊,並存回count變量,而B主管將T的工資減去3塊,並存回count變量。好了,本來T君可以得到102塊的 工資的,現在就變成98塊了。這就是線程同步要解決的問題。

在C#裏面用於實現線程同步的常用類有如下幾類 1、Mutex類(互斥器),Monitor類,lock方法 
2、ManualResetEvent類,AutoResetEvent類(這兩個都是由EventWaitHandle類派生出來的) 
3、ReaderWriterLock類

同 一類的作用都差不多:其中 代碼執行爲止。就好比一堆人同時上一個公共廁所一樣,使用這個方法就可以解決文章一開始時提出的問題:主管A要處理T君的工資之前,先lock一下T君, 然後取出目前的count值,處理完之後再解除T君的鎖定。如果主管B在主管A處理工資時也想取出count值,那麼它只能是一直地等待A處理完之後才能 繼續。使用這個方法的一個缺點就是會降低程序的效率。本來是一個多個線程的操作,一旦遇到lock的語句時,那麼這些線程只要排隊處理,形同一個單線程操 作。

下面舉個例子說明一下這三個方法的使用: 
假定有一個Tools類,裏面一個int變量,還有Add和Delete方法,其中Add方法會使int變量的值增加,Delete方法使int變量值減少:

public class Tools private int count = 100; 
public void Add(int n) count+=n; 
}

public void Delete(int n) count-=n;

}

   在多個線程同時訪問這段代碼時,因爲一個語句會被編譯器編譯成多個指令,所以會可能出現這種情況:但某個線程調用Add方法時,這時的count值爲 100,而正當要加上n的時候,另外一個線程調用了Delete,它要減去m,結果count加上了n,然後又在原先count=100的值的情況 
下減掉了m,最後的結果是count被減去了m,而沒有加上n。很明顯Add方法和Delete方法是不能同時被調用的,所以必須進行線程同步處理。簡單的方法是用lock語句:

public class Tools private object abcde = new object(); 
private int count = 100;

public void Add(int n) lock(abcde) count+=n; 
}

public void Delete(int n) lock(abcde) count-=n; }

  其中abcde是一個private級的內部變量,它不表示任何的意義,只是作爲一種“令牌”的角色。 
當執行Add方法中的lock(abcde)方法時,這個令牌就在Add方法的手中了,如果這時有第二個線程也想拿這個令牌,沒門,惟有等待。一旦第一 
個lock語句的花括號範圍結束之後,這時令牌就被釋放了,同時會迅速落到第二個線程的手中,並且排除其他後來的人。

使用Monitor類的方法大致一樣:

public class Tools private object abcde = new object(); 
private int count = 100;

public void Add(int n) Monitor.Enter(abcde); 
count+=n; 
Monitor.Exit(abcde); 
}

public void Delete(int n) Monitor.Enter(abcde); 
count-=n; 
Monitor.Exit(abcde); 
}

  Monitor的常用方法:Enter和Exit都是靜態方法,作用跟lock語句的兩個花括號一樣。 
而使用 Mutex 就不需聲明一個“令牌”對象了,但要實例化之後纔可以使用:

public class Tools


private Mutex mut = new Mutex(); 
private int count = 100;

public void Add(int n) mut.WaitOne(); 
count+=n; 
mut.ReleaseMutex(); 
}

public void Delete(int n) mut.WaitOne(); 
count-=n; 
mut.ReleaseMutex(); 
}

其中的WaitOne爲等待方法,一直等到Mutex 被釋放爲止。初始的情況下,Mutex 對象是處於釋放狀態的,而一旦執行了WaitOne方法之後,它 就被捕獲了,一直到被調用了ReleaseMutex方法之後才被釋放。 
使用這三種方法都有一個要注意的問題,就是在獨佔代碼段裏面如果引起了異常,可能會使“令牌”對象不被釋放,這樣程序就會一直地死等下去了。

  所以要在獨佔代碼段裏面處理好異常。例如下面這樣的代碼就是錯誤的:

public void Add(int n) try mut.WaitOne(); 
count+=n; mut.ReleaseMutex(); catch Console.Writeline("error."); }

  上面的代碼一旦在try和catch裏面發生了異常,那麼Mutex就不能被釋放,後面的程序就會卡死在WaitOne()一行,而應該改成這樣:

public void Add(int n) mut.WaitOne(); 
try count+=n; } 
catch Console.Writeline("error."); mut.ReleaseMutex(); 
}

現在談一下第二種: 
ManualResetEvent類,AutoResetEvent類

  上面這兩個類都是由EventWaitHandle類派生出來的,所以功能和調用方法都很相似。 舉個例子,你想送花給一個MM,託了一個送花的小夥子送了過去,而你希望當MM收到花之後就立即打個電話過去告訴她。

  但問題是你不知道花什麼時候才送到MM的手裏,打早了打遲了都不好,這時你可以使用ManualResetEvent對象幫忙。

當委託小夥子送花過去的時候,使用ManualResetEvent的WaitOne方法進行等待。當小夥子把花送到MM的手中時,再調用一下ManualResetEvent的Set方法,你就可以準時地打電話過去了。

另外ManualResetEvent還有一個Reset方法,用來重新阻斷調用者執行的,情況就好比你委託了這個小夥子送花給N個MM,

  而又想準時地給這N個MM打電話的情況一樣。

using System; 
using System.Threading;

public class TestMain private static ManualResetEvent ent = new ManualResetEvent(false);

public static void Main() Boy sender = new Boy(ent); 
Thread th = new Thread(new ThreadStart(sender.SendFlower)); 
th.Start();

ent.WaitOne(); //等待工作 
Console.WriteLine("收到了吧,花是我送嘀:)"); 
Console.ReadLine();

public class Boy ManualResetEvent ent;

public Boy(ManualResetEvent e) ent = e; 
}

public void SendFlower() Console.WriteLine("正在送花的途中"); 
Thread.Sleep(2000);

Console.WriteLine("\r\n花已經送到MM手中了,boss");

ent.Set(); //通知阻塞程序 
}

  而AutoResetEvent類故名思意,就是在每次Set完之後自動Reset。讓執行程序重新進入阻塞狀態。 
即AutoResetEvent.Set() 相當於 ManualResetEvent.Set() 之後又立即 ManualResetEvent.Reset(), 舉個送花給N個MM的例子:

using System; 
using System.Threading;

public class TestMain private static AutoResetEvent ent = new AutoResetEvent(false);

public static void Main() Boy sender = new Boy(ent);

for (int i = 0; i < 3; i++) Thread th = new Thread(new ThreadStart(sender.SendFlower)); 
th.Start(); 
ent.WaitOne(); //等待工作 
Console.WriteLine("收到了吧,花是我送嘀:)\r\n\r\n"); 
}

Console.ReadLine(); 
}

public class Boy AutoResetEvent ent;

public Boy(AutoResetEvent e) ent = e; 
}

public void SendFlower() Console.WriteLine("正在送花的途中"); 
Thread.Sleep(2000); 
Console.WriteLine("\r\n花已送到MM手中了,boss");

//通知阻塞程序,這裏的效果相當於 ManualResetEvent的Set()方法+Reset()方法

ent.Set();

}

要注意的是ManualResetEvent和AutoResetEvent 的構造函數都有一個bool的參數,用這個參數可以指定初始情況下,同步對象的處於阻塞(設置爲false)還是非阻塞(設置爲true)的狀態。

另外WaitOne方法也可以帶兩個參數: 
WaitOne (int millisecondsTimeout,bool exitContext) 
millisecondsTimeout:等待的毫秒數,或爲 Timeout.Infinite (-1),表示無限期等待。 
exitContext:爲 true,則等待之前先退出上下文的同步域(如果在同步上下文中),然後在稍後重新獲取它;否則爲false。

  就是說,等待是可以加上一個期限的,如果等待的同步對象一直都不Set()的話,那麼程序就會卡死,所以在WaitOne方法裏面可以放置一個時間期限,單位是毫秒。

C#不同線程間通信,使用Control的Invoke和BeginInvoke的方法

- 張小魚 2010-10-22 08:41

Control的Invoke和BeginInvoke Control的Invoke和BeginInvoke
近日,被Control的Invoke和BeginInvoke搞的頭大,就查了些相關的資料,整理如下。
(一)Control的Invoke和BeginInvoke (1)Control的Invoke和BeginInvoke與Delegate的Invoke和BeginInvoke是不同的。
(2)Control的Invoke和BeginInvoke的參數爲delegate,委託的方法是在Control的線程上執行的,也就是我們平時所說的UI線程。
我們以代碼(一)來看(Control的Invoke)
private delegate void InvokeDelegate();
private void InvokeMethod(){
   //C代碼段 private void butInvoke_Click(object sender, EventArgs e) {
   //A代碼段.......
   this.Invoke(new InvokeDelegate(InvokeMethod));
   //B代碼段...... 你覺得代碼的執行順序是什麼呢?記好Control的Invoke和BeginInvoke都執行在主線程即UI線程上
A------>C---------------->B
解釋:(1)A在UI線程上執行完後,開始Invoke,Invoke是同步
(2)代碼段B並不執行,而是立即在UI線程上執行InvokeMethod方法,即代碼段C。
(3)InvokeMethod方法執行完後,代碼段C纔在UI線程上繼續執行。
看看代碼(二),Control的BeginInvoke
private delegate void BeginInvokeDelegate();
private void BeginInvokeMethod(){
   //C代碼段 private void butBeginInvoke_Click(object sender, EventArgs e) {
   //A代碼段.......
   this.BeginInvoke(new BeginInvokeDelegate(BeginInvokeMethod));
   //B代碼段...... 你覺得代碼的執行順序是什麼呢?記好Control的Invoke和BeginInvoke都執行在主線程即UI線程上
A----------->B--------------->C慎重,這個只做參考。。。。。,我也不肯定執行順序,如果有哪位達人知道的話請告知。
解釋::(1)A在UI線程上執行完後,開始BeginInvoke,BeginInvoke是異步
(2)InvokeMethod方法,即代碼段C不會執行,而是立即在UI線程上執行代碼段B。
(3) 代碼段B執行完後(就是說butBeginInvoke_Click方法執行完後),InvokeMethod方法,即代碼段C纔在UI線程上繼續執行。 Control的Invoke和BeginInvoke的委託方法是在主線程,即UI線程上執行的。也就是說如果你的委託方法用來取花費時間長的數據,然 後更新界面什麼的,千萬別在UI線程上調用Control.Invoke和 Control.BeginInvoke,因爲這些是依然阻塞UI線程的,造成界面的假死。異步是指相對於調用BeginInvoke的線程異步,而不是 相對於UI線程異步,你在UI線程上調用BeginInvoke ,當然不行了。----摘自"Invoke和BeginInvoke的真正涵義"一文中的評論。
BeginInvoke的原理是將調用的方法Marshal成消息,然後調用Win32 API中的RegisterWindowMessage()向UI窗口發送消息。----摘自"Invoke和BeginInvoke的真正涵義"一文中的評論。
(二)我們用Thread來調用BeginInvoke和Invoke
      我們開一個線程,讓線程執行一些耗費時間的操作,然後再用Control.Invoke和Control.BeginInvoke回到用戶UI線程,執行界面更新。
代碼(三) Thread調用Control的Invoke
private Thread invokeThread;
private delegate void invokeDelegate();
private void StartMethod(){
  //C代碼段......
   Control.Invoke(new invokeDelegate(invokeMethod));
//D代碼段...... private void invokeMethod(){
//E代碼段 private void butInvoke_Click(object sender, EventArgs e) {
   //A代碼段.......
  invokeThread = new Thread(new ThreadStart(StartMethod));
  invokeThread.Start();
   //B代碼段......
你覺得代碼的執行順序是什麼呢?記好Control的Invoke和BeginInvoke都執行在主線程即UI線程上
A------> (Start一開始B和StartMethod的C就同時執行)---->(C執行完了,不管B有沒有執行完,invokeThread把消息封送 (invoke)給UI線程,然後自己等待)---->UI線程處理完butInvoke_Click消息後,處理invokeThread封送過 來的消息,執行invokeMethod 方法,即代碼段E,處理往後UI線程切換到invokeThread線程。
這個Control.Invoke是相對於invokeThread線程同步的,阻止了其運行。 1。UI執行A
2。UI開線程InvokeThread,B和C同時執行,B執行在線程UI上,C執行在線程invokeThread上。
3。invokeThread封送消息給UI,然後自己等待,UI處理完消息後,處理invokeThread封送的消息,即代碼段E
4。UI執行完E後,轉到線程invokeThread上,invokeThread線程執行代碼段D
代碼(四) Thread調用Control的BeginInvoke
private Thread beginInvokeThread;
private delegate void beginInvokeDelegate();
private void StartMethod(){
  //C代碼段......
   Control.BeginInvoke(new beginInvokeDelegate(beginInvokeMethod));
//D代碼段...... private void beginInvokeMethod(){
//E代碼段 private void butBeginInvoke_Click(object sender, EventArgs e) {
   //A代碼段.......
  beginInvokeThread = new Thread(new ThreadStart(StartMethod));
  beginInvokeThread .Start();
   //B代碼段...... 你覺得代碼的執行順序是什麼呢?記好Control的Invoke和BeginInvoke都執行在主線程即UI線程上
A 在UI線程上執行----->beginInvokeThread線程開始執行,UI繼續執行代碼段B,併發地invokeThread執行代碼段 C-------------->不管UI有沒有執行完代碼段B,這時 beginInvokeThread線程把消息封送給UI,單自己並不等待,繼續向下執行-------->UI處理完 butBeginInvoke_Click消息後,處理beginInvokeThread線程封送過來的消息。解釋:
1。UI執行A
2。UI開線程beginInvokeThread,B和C同時執行,B執行在線程UI上,C執行在線程beginInvokeThread上。
3。beginInvokeThread封送消息給UI,然後自己繼續執行代碼D,UI處理完消息後,處理invokeThread封送的消息,即代碼段E
有點疑問:如果UI先執行完畢,是不是有可能過了段時間beginInvokeThread才把消息封送給UI,然後UI才繼續執行封送的消息E。如圖淺綠的部分。
Control 的BeginInvoke是相對於調用它的線程,即beginInvokeThread相對是異步的。 (1)如果你想阻止調用線程,那麼調用代碼(三),代碼段D刪掉,C改爲耗費長時間的操作,因爲這個操作是在另外一個線程中做的。代碼段E改爲更新界面的 方法。
(2)如果你不想阻止調用線程,那麼調用代碼(四),代碼段D刪掉,C改爲耗費長時間的操作,因爲這個操作是在另外一個線程中做的。代碼段E改爲更新界面的方法。
http://www.huomo.cn/developer/article-1aa5.html

C#多線程編程(4)-多線程與UI操作

- 張小魚 2010-10-22 09:09



爲了讓程序儘快響應用戶操作,在開發Windows應用程序時經常會使用到線程。對於耗時的操作如果不使用線程將會是UI界面長時間處於停滯狀態,這種情況是用戶非常不願意看到的,在這種情況下我們希望使用線程來解決這個問題。
下面是一個使用多線程操作界面UI的代碼:
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; 

namespacepublicpartialclasspublic ThreadForm() 

InitializeComponent(); 


privatevoid btnThread_Click(object sender, EventArgs e) 

Thread thread =new Thread(new ThreadStart(Run)); 
thread.Start(); 


privatevoidwhile (progressBar.Value < progressBar.Maximum) 

progressBar.PerformStep(); 



}
程序的界面如下:

我 們的本意是點擊“啓動”按鈕來啓動模擬一個操作,在進度條中顯示操作的總體進度。不過如果我們真的點擊“啓動”按鈕會很失望,因爲它會拋出一個 System.InvalidOperationException異常,異常描述就是“線程間操作無效: 從不是創建控件‘progressBar’的線程訪問它。”
CheckForIllegalCrossThreadCalls屬性
之所以 會出現這樣的情況是因爲在.NET中做了限制,不允許在調試環境下使用線程訪問並非它自己創建的UI控件,這麼做可能是怕在多線程環境下對界面控件進行操 作會出現不可預知的情況,如果開發者可以確認自己的代碼操作界面不會出現問題,可以用比較簡單的方法解決,那就是設置 CheckForIllegalCrossThreadCalls這個靜態屬性,它默認是true,如果將其設爲false的話,以後在多線程環境下操作 界面也不會拋出異常了,我們上面的代碼可以修改爲:
using System; 
using System.Collections.Generic; 
using System.ComponentModel; 
using System.Data; 
using System.Drawing; 
using System.Linq; 
using System.Text; 
using System.Windows.Forms; 
using System.Threading; 

namespacepublicpartialclasspublic ThreadForm() 

InitializeComponent(); 


privatevoid btnThread_Click(object//指示是否對錯誤線程的調用,即是否允許在創建UI的線程之外訪問線程
CheckForIllegalCrossThreadCalls =false=new Thread(new ThreadStart(Run)); 
thread.Start(); 


privatevoidwhile (progressBar.Value < progressBar.Maximum) 

progressBar.PerformStep(); 



}
這樣再執行程序就不會拋出異常了。
不過使用上面的代碼我們可能還有些犯嘀咕,畢竟是不允許直接在線程中直接操作界面的,那麼我們還可以用Invoke方法。
Invoke方法來操作界面


下面是一個例子:
using System; 
using System.Collections.Generic; 
using System.ComponentModel; 
using System.Data; 
using System.Drawing; 
using System.Linq; 
using System.Text; 
using System.Windows.Forms; 
using System.Threading; 

namespacepublicpartialclass//定義delegate以便Invoke時使用
privatedelegatevoid SetProgressBarValue(int value); 
public ThreadForm() 

InitializeComponent(); 


privatevoid btnThread_Click(object sender, EventArgs e) 

progressBar.Value =0; 
//指示是否對錯誤線程的調用,即是否允許在創建UI的線程之外訪問線程 
//CheckForIllegalCrossThreadCalls = false;
Thread thread =new Thread(new ThreadStart(Run)); 
thread.Start(); 

//使用線程來直接設置進度條
privatevoidwhile (progressBar.Value < progressBar.Maximum) 

progressBar.PerformStep(); 



privatevoid btnInvoke_Click(object sender, EventArgs e) 

progressBar.Value =0=new Thread(new ThreadStart(RunWithInvoke)); 
thread.Start(); 

//使用Invoke方法來設置進度條
privatevoidint value = progressBar.Value; 
while (value<//如果是跨線程調用
ifthis.Invoke(new SetProgressBarValue(SetProgressValue), value++else=++//跟SetProgressBarValue委託相匹配的方法
privatevoid SetProgressValue(int value) 

progressBar.Value =
這個方法的功能跟上面的操作是一樣的,只不過不需要設置CheckForIllegalCrossThreadCalls屬性,而且還不會拋出異常,當然除了上面的方法之外,還可以使用BackgroundWorker類來完成同樣的功能。

BackgroundWorker類操作界面
因爲使用BackgroundWorker類操作UI界面的例子周公博客上已經有過例子,所以這裏的例子代碼註釋比較簡單,讀者可以看周公以前的示例,這次所使用的代碼示例如下:
using System; 
using System.Collections.Generic; 
using System.ComponentModel; 
using System.Data; 
using System.Drawing; 
using System.Linq; 
using System.Text; 
using System.Windows.Forms; 
using System.Threading; 

namespacepublicpartialclass//定義delegate以便Invoke時使用
privatedelegatevoid SetProgressBarValue(int value); 
private BackgroundWorker worker; 
public ThreadForm() 

InitializeComponent(); 


privatevoid btnThread_Click(object sender, EventArgs e) 

progressBar.Value =0; 
//指示是否對錯誤線程的調用,即是否允許在創建UI的線程之外訪問線程 
//CheckForIllegalCrossThreadCalls = false;
Thread thread =new Thread(new ThreadStart(Run)); 
thread.Start(); 

//使用線程來直接設置進度條
privatevoidwhile (progressBar.Value < progressBar.Maximum) 

progressBar.PerformStep(); 



privatevoid btnInvoke_Click(object sender, EventArgs e) 

progressBar.Value =0=new Thread(new ThreadStart(RunWithInvoke)); 
thread.Start(); 

//使用Invoke方法來設置進度條
privatevoidint value = progressBar.Value; 
while (value<//如果是跨線程調用
ifthis.Invoke(new SetProgressBarValue(SetProgressValue), value++else=++//跟SetProgressBarValue委託相匹配的方法
privatevoid SetProgressValue(int value) 

progressBar.Value =privatevoid btnBackgroundWorker_Click(object sender, EventArgs e) 

progressBar.Value =0=new BackgroundWorker(); 
worker.DoWork +=new DoWorkEventHandler(worker_DoWork); 
//當工作進度發生變化時執行的事件處理方法
worker.ProgressChanged +=new ProgressChangedEventHandler(worker_ProgressChanged); 
//當事件處理完畢後執行的方法
worker.RunWorkerCompleted +=new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted); 
worker.WorkerReportsProgress =true;//支持報告進度更新
worker.WorkerSupportsCancellation =false;//不支持異步取消
worker.RunWorkerAsync();//啓動執行
btnBackgroundWorker.Enabled =false//當事件處理完畢後執行的方法
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) 

btnBackgroundWorker.Enabled=true//當工作進度發生變化時執行的事件處理方法
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e) 

//可以在這個方法中與界面進行通訊
progressBar.Value =//開始啓動工作時執行的事件處理方法
void worker_DoWork(object sender, DoWorkEventArgs e) 

int value = progressBar.Value; 
while (value < progressBar.Maximum) 

worker.ReportProgress(++value);//彙報進度

當然,除了BackgroundWorker可以完成上面的功能之外,利用System.Windows.Forms.Timer類也能完場上面的功能,代碼如下: using System; 
using System.Collections.Generic; 
using System.ComponentModel; 
using System.Data; 
using System.Drawing; 
using System.Linq; 
using System.Text; 
using System.Windows.Forms; 
using System.Threading; 

namespacepublicpartialclass//定義delegate以便Invoke時使用
privatedelegatevoid SetProgressBarValue(int value); 
private BackgroundWorker worker; 
public ThreadForm() 

InitializeComponent(); 


privatevoid btnThread_Click(object sender, EventArgs e) 

progressBar.Value =0; 
//指示是否對錯誤線程的調用,即是否允許在創建UI的線程之外訪問線程 
//CheckForIllegalCrossThreadCalls = false;
Thread thread =new Thread(new ThreadStart(Run)); 
thread.Start(); 

//使用線程來直接設置進度條
privatevoidwhile (progressBar.Value < progressBar.Maximum) 

progressBar.PerformStep(); 



privatevoid btnInvoke_Click(object sender, EventArgs e) 

progressBar.Value =0=new Thread(new ThreadStart(RunWithInvoke)); 
thread.Start(); 

//使用Invoke方法來設置進度條
privatevoidint value = progressBar.Value; 
while (value<//如果是跨線程調用
ifthis.Invoke(new SetProgressBarValue(SetProgressValue), value++else=++//跟SetProgressBarValue委託相匹配的方法
privatevoid SetProgressValue(int value) 

progressBar.Value =privatevoid btnBackgroundWorker_Click(object sender, EventArgs e) 

progressBar.Value =0=new BackgroundWorker(); 
worker.DoWork +=new DoWorkEventHandler(worker_DoWork); 
//當工作進度發生變化時執行的事件處理方法
worker.ProgressChanged +=new ProgressChangedEventHandler(worker_ProgressChanged); 
//當事件處理完畢後執行的方法
worker.RunWorkerCompleted +=new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted); 
worker.WorkerReportsProgress =true;//支持報告進度更新
worker.WorkerSupportsCancellation =false;//不支持異步取消
worker.RunWorkerAsync();//啓動執行
btnBackgroundWorker.Enabled =false//當事件處理完畢後執行的方法
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) 

btnBackgroundWorker.Enabled=true//當工作進度發生變化時執行的事件處理方法
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e) 

//可以在這個方法中與界面進行通訊
progressBar.Value =//開始啓動工作時執行的事件處理方法
void worker_DoWork(object sender, DoWorkEventArgs e) 

int value = progressBar.Value; 
while (value < progressBar.Maximum) 

worker.ReportProgress(++value);//彙報進度
//使用System.Windows.Forms.Timer來操作界面能
privatevoid btnTimer_Click(object sender, EventArgs e) 

progressBar.Value =0; 
//注意在.net中有多個命名空間下存在Timer類,爲了便於區別,使用了帶命名空間形式
System.Windows.Forms.Timer timer =new System.Windows.Forms.Timer(); 
timer.Interval =1+=new EventHandler(timer_Tick); 
timer.Enabled =true//Timer中要定期執行的方法
void timer_Tick(objectint value = progressBar.Value; 
if (value < progressBar.Maximum) 

progressBar.Value = value+100
總結:本篇主要講述了使用線程操作Windows應用程序界面的方法,這些方法在編寫多線程的UI程序時可以參考。

http://www.csharpwin.com/csharpspace/10234r1033.shtml

多線程和異步的一些看法(異步寫文件)

- 張小魚 2010-10-22 09:40



本文章是小弟的處女文章.以前只在園子裏看文章. 
正方開始 

在工作中有一個項目由於寫文件操作比較頻繁 
以前用的是同步寫文件方式,有人提出要多線程寫文件. 
多線程寫文件的任務分給別一個同事, 
因別一個同事對多線程方面不太熟 我給他提供了下面這種試 

//多線程操作同步寫文件方法 public ThreadOperate() { Thread t = new Thread(new ThreadStart(ThreadProc)); t.Start(); } //同步寫文件方法 public static void ThreadProc() { }



後來看了一些資料 http://dev.yesky.com/135/3030135.shtml(多核時代對併發程序設計的探索) 
總結了下面這句: 

線程: 需要操作系統投入CPU資源來運行和調度 
異步: 無須消耗CPU時間的操作 如 I/O操作不僅包括了直接的文件、網絡的讀寫,還包括數據庫操作、Web Service、HttpRequest以及.net Remoting等跨進程的調用。 
目前還不能很確定 上面這句是否正確. 請指點. 

根據上面那句話我的想法改變了,我認爲用異步寫文件比多線程更合適於是用了下面方式

//異步寫文件 public void WriteFile() { FileStream fs = new FileStream(@"C:\Test.txt", FileMode.Create, FileAccess.Write, FileShare.Write); byte[] buffer = new byte[100]; for (int i = 0; i < buffer.Length; i++) { buffer[i] = 2; } fs.BeginWrite(buffer, 0, buffer.Length, FileCallBack, fs); } static void FileCallBack(IAsyncResult asyncResult) { FileStream fs = asyncResult.AsyncState as FileStream; if (fs != null) { fs.EndWrite(asyncResult); fs.Close(); } }



來源:http://www.chenjiliang.com/Article/View.aspx?ArticleID=2295

C#異步多線程(訪問主線程控件)

- 張小魚 2010-10-22 09:42


在多線程的情況下,其它線程無法直接調用到主線程上的控件,只能通過代理來實現主線程上控件的調用。

1、定義委託
  // 執行任務的委託聲明(解決長任務死假)
  delegate void RunTaskDelegate(int seconds);
  // 顯示進度條的委託聲明(跨線程調用控件)
  delegate void ShowProgressDelegate(int totalStep, int currentStep);
2、定義方法
  private void ShowProgress(int totalStep, int currentStep)
  {
    progressBar1.Maximum = totalStep;
    progressBar1.Value = currentStep;
  }
3、定義線程
  private void RunTask(int seconds)
  {
    ShowProgressDelegate showProgress = new ShowProgressDelegate(ShowProgress);
    for (int i = 0; i < seconds * 4; i++)
    {
      Thread.Sleep(250);
      // 在基礎窗口上調用顯示進度條的委託
      this.Invoke(showProgress, new object[] { seconds * 4, i + 1 });
    }
  }
4、執行
  RunTaskDelegate runTask = new RunTaskDelegate(RunTask);
  // 異步調用執行任務的委託
  runTask.BeginInvoke(20, null, null);

.Net多線程完美講解

- 張小魚 2010-10-22 09:47

怎樣創建一個線程 

我只簡單列舉幾種常用的方法,詳細可參考.Net多線程總結(一) 

一)使用Thread類 

ThreadStart threadStart=new ThreadStart(Calculate);//通過ThreadStart委託告訴子線程講執行什麼方法,這裏執行一個計算圓周長的方法
Thread thread=new Thread(threadStart);
thread.Start(); //啓動新線程

public void Calculate(){
double Diameter=0.5;
Console.Write("The perimeter Of Circle with a Diameter of {0} is {1}"Diameter,Diameter*Math.PI);
}


二)使用Delegate.BeginInvoke 

delegate double CalculateMethod(double Diameter); //申明一個委託,表明需要在子線程上執行的方法的函數簽名
static CalculateMethod calcMethod = new CalculateMethod(Calculate);//把委託和具體的方法關聯起來
static void Main(string[] args)
{
//此處開始異步執行,並且可以給出一個回調函數(如果不需要執行什麼後續操作也可以不使用回調)
calcMethod.BeginInvoke(5, new AsyncCallback(TaskFinished), null);
Console.ReadLine();
}

//線程調用的函數,給出直徑作爲參數,計算周長
public static double Calculate(double Diameter)
{
return Diameter * Math.PI;
}

//線程完成之後回調的函數
public static void TaskFinished(IAsyncResult result)
{
double re = 0;
re = calcMethod.EndInvoke(result);
Console.WriteLine(re);
}

三)使用ThreadPool.QueueworkItem 

WaitCallback w = new WaitCallback(Calculate);
//下面啓動四個線程,計算四個直徑下的圓周長
ThreadPool.QueueUserWorkItem(w, 1.0);
ThreadPool.QueueUserWorkItem(w, 2.0);
ThreadPool.QueueUserWorkItem(w, 3.0);
ThreadPool.QueueUserWorkItem(w, 4.0);
public static void Calculate(double Diameter)
{
return Diameter * Math.PI;
}

下面兩條來自於http://www.cnblogs.com/tonyman/archive/2007/09/13/891912.html

受託管的線程與 Windows線程

必 須要瞭解,執行.NET應用的線程實際上仍然是Windows線程。但是,當某個線程被CLR所知時,我們將它稱爲受託管的線程。具體來說,由受託管的代 碼創建出來的線程就是受託管的線程。如果一個線程由非託管的代碼所創建,那麼它就是非託管的線程。不過,一旦該線程執行了受託管的代碼它就變成了受託管的 線程。 

一個受託管的線程和非託管的線程的區別在於,CLR將創建一個System.Threading.Thread類的實例來代表並操作前者。在內部實現中,CLR將一個包含了所有受託管線程的列表保存在一個叫做ThreadStore地方。 

CLR確保每一個受託管的線程在任意時刻都在一個AppDomain中執行,但是這並不代表一個線程將永遠處在一個AppDomain中,它可以隨着時間的推移轉到其他的AppDomain中。 

從安全的角度來看,一個受託管的線程的主用戶與底層的非託管線程中的Windows主用戶是無關的。


前臺線程與後臺線程

啓 動了多個線程的程序在關閉的時候卻出現了問題,如果程序退出的時候不關閉線程,那麼線程就會一直的存在,但是大多啓動的線程都是局部變量,不能一一的關 閉,如果調用Thread.CurrentThread.Abort()方法關閉主線程的話,就會出現ThreadAbortException 異常,因此這樣不行。
後來找到了這個辦法: Thread.IsBackground 設置線程爲後臺線程。

msdn對前臺線程 和後臺線程的解釋:託管線程或者是後臺線程,或者是前臺線程。後臺線程不會使託管執行環境處於活動狀態,除此之外,後臺線程與前臺線程是一樣的。一旦所有 前臺線程在託管進程(其中 .exe 文件是託管程序集)中被停止,系統將停止所有後臺線程並關閉。通過設置 Thread.IsBackground 屬性,可以將一個線程指定爲後臺線程或前臺線程。例如,通過將 Thread.IsBackground 設置爲 true,就可以將線程指定爲後臺線程。同樣,通過將 IsBackground 設置爲 false,就可以將線程指定爲前臺線程。從非託管代碼進入托管執行環境的所有線程都被標記爲後臺線程。通過創建並啓動新的 Thread 對象而生成的所有線程都是前臺線程。如果要創建希望用來偵聽某些活動(如套接字連接)的前臺線程,則應將 Thread.IsBackground 設置爲 true,以便進程可以終止。 
所以解決辦法就是在主線程初始化的時候,設置:Thread.CurrentThread.IsBackground = true; 

這樣,主線程就是後臺線程,在關閉主程序的時候就會關閉主線程,從而關閉所有線程。但是這樣的話,就會強制關閉所有正在執行的線程,所以在關閉的時候要對線程工作的結果保存。



經常看到名爲BeginXXX和EndXXX的方法,他們是做什麼用的 

這是.net的一個異步方法名稱規範
.Net 在設計的時候爲異步編程設計了一個異步編程模型(APM),這個模型不僅是使用.NET的開發人員使用,.Net內部也頻繁用到,比如所有的Stream 就有BeginRead,EndRead,Socket,WebRequet,SqlCommand都運用到了這個模式,一般來講,調用 BegionXXX的時候,一般會啓動一個異步過程去執行一個操作,EndEnvoke可以接收這個異步操作的返回,當然如果異步操作在 EndEnvoke調用的時候還沒有執行完成,EndInvoke會一直等待異步操作完成或者超時 

.Net的異步編程模型(APM)一般包含BeginXXX,EndXXX,IAsyncResult這三個元素,BeginXXX方法都要返回一個IAsyncResult,而EndXXX都需要接收一個IAsyncResult作爲參數,他們的函數簽名模式如下 

IAsyncResult BeginXXX(...);

<返回類型> EndXXX(IAsyncResult ar); 

BeginXXX 和EndXXX中的XXX,一般都對應一個同步的方法,比如FileStream的Read方法是一個同步方法,相應的 BeginRead(),EndRead()就是他的異步版本,HttpRequest有GetResponse來同步接收一個響應,也提供了 BeginGetResponse和EndGetResponse這個異步版本,而IAsynResult是二者聯繫的紐帶,只有把BeginXXX所返 回的IAsyncResult傳給對應的EndXXX,EndXXX才知道需要去接收哪個BeginXXX發起的異步操作的返回值。

這個 模式在實際使用時稍顯繁瑣,雖然原則上我們可以隨時調用EndInvoke來獲得返回值,並且可以同步多個線程,但是大多數情況下當我們不需要同步很多線 程的時候使用回調是更好的選擇,在這種情況下三個元素中的IAsynResult就顯得多餘,我們一不需要用其中的線程完結標誌來判斷線程是否成功完成 (回調的時候線程應該已經完成了),二不需要他來傳遞數據,因爲數據可以寫在任何變量裏,並且回調時應該已經填充,所以可以看到微軟在新的.Net Framework中已經加強了對回調事件的支持,這總模型下,典型的回調程序應該這樣寫

a.DoWork+=new SomeEventHandler(Caculate);
a.CallBack+=new SomeEventHandler(callback);
a.Run();


(注: 我上面講的是普遍的用法,然而BeginXXX,EndXXX僅僅是一種模式,而對這個模式的實現完全取決於使用他的開發人員,具體實現的時候你可以使用 另外一個線程來實現異步,也可能使用硬件的支持來實現異步,甚至可能根本和異步沒有關係(儘管幾乎沒有人會這樣做)-----比如直接在Beginxxx 裏直接輸出一個"Helloworld",如果是這種極端的情況,那麼上面說的一切都是廢話,所以上面的探討並不涉及內部實現,只是告訴大家微軟的模式, 和框架中對這個模式的經典實現) 





異步和多線程有什麼關聯 

有一句話總結的很好:多線程是實現異步的一種手段和工具 

我們通常把多線程和異步等同起來,實際是一種誤解,在實際實現的時候,異步有許多種實現方法,我們可以用進程來做異步,或者使用纖程,或者硬件的一些特性,比如在實現異步IO的時候,可以有下面兩個方案:

1)可以通過初始化一個子線程,然後在子線程裏進行IO,而讓主線程順利往下執行,當子線程執行完畢就回調

2)也可以根本不使用新線程,而使用硬件的支持(現在許多硬件都有自己的處理器),來實現完全的異步,這是我們只需要將IO請求告知硬件驅動程序,然後迅速返回,然後等着硬件IO就緒通知我們就可以了

實 際上DotNet Framework裏面就有這樣的例子,當我們使用文件流的時候,如果制定文件流屬性爲同步,則使用BeginRead進行讀取時,就是用一個子線程來調 用同步的Read方法,而如果指定其爲異步,則同樣操作時就使用了需要硬件和操作系統支持的所謂IOCP的機制





WinForm多線程編程篇



我的多線程WinForm程序老是拋出InvalidOperationException ,怎麼解決? 

在WinForm中使用多線程時,常常遇到一個問題,當在子線程(非UI線程)中修改一個控件的值:比如修改進度條進度,時會拋出如下錯誤 

Cross-thread operation not valid: Control ‘XXX‘ accessed from a thread other than the thread it was created on. 

在 VS2005或者更高版本中,只要不是在控件的創建線程(一般就是指UI主線程)上訪問控件的屬性就會拋出這個錯誤,解決方法就是利用控件提供的 Invoke和BeginInvoke把調用封送回UI線程,也就是讓控件屬性修改在UI線程上執行,下面列出會報錯的代碼和他的修改版本 


ThreadStart threadStart=new ThreadStart(Calculate);//通過ThreadStart委託告訴子線程講執行什麼方法
Thread thread=new Thread(threadStart);
thread.Start();
public void Calculate(){
double Diameter=0.5;
double result=Diameter*Math.PI;
CalcFinished(result);//計算完成需要在一個文本框裏顯示
}
public void CalcFinished(double result){
this.TextBox1.Text=result.ToString();//會拋出錯誤
}
上面加粗的地方在debug的時候會報錯,最直接的修改方法是修改Calculate這個方法如下 


delegate void changeText(double result);

public void Calculate(){
double Diameter=0.5;
double result=Diameter*Math.PI;
this.BeginInvoke(new changeText(CalcFinished),t.Result);//計算完成需要在一個文本框裏顯示


這樣就ok了,但是最漂亮的方法是不去修改Calculate,而去修改CalcFinished這個方法,因爲程序裏調用這個方法的地方可能很多,由於加了是否需要封送的判斷,這樣修改還能提高非跨線程調用時的性能


delegate void changeText(double result);

public void CalcFinished(double result){
if(this.InvokeRequired){
this.BeginInvoke(new changeText(CalcFinished),t.Result);
}
else{
this.TextBox1.Text=result.ToString();
}
}
上面的做法用到了Control的一個屬性InvokeRequired(這個屬性是可以在其他線程裏訪問的),這個屬性表明調用是否來自另非UI線程,如果是,則使用BeginInvoke來調用這個函數,否則就直接調用,省去線程封送的過程




Invoke,BeginInvoke幹什麼用的,內部是怎麼實現的?

這兩個方法主要是讓給出的方法在控件創建的線程上執行

Invoke使用了Win32API的SendMessage,

UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);

BeginInvoke使用了Win32API的PostMessage

UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);

這兩個方法向UI線程的消息隊列中放入一個消息,當UI線程處理這個消息時,就會在自己的上下文中執行傳入的方法,換句話說凡是使用BeginInvoke和Invoke調用的線程都是在UI主線程中執行的,所以如果這些方法裏涉及一些靜態變量,不用考慮加鎖的問題




每個線程都有消息隊列嗎?

不是,只有創建了窗體對象的線程纔會有消息隊列(下面給出<Windows 核心編程>關於這一段的描述)

當 一個線程第一次被建立時,系統假定線程不會被用於任何與用戶相關的任務。這樣可以減少線程對系統資源的要求。但是,一旦這個線程調用一個與圖形用戶界面有 關的函數(例如檢查它的消息隊列或建立一個窗口),系統就會爲該線程分配一些另外的資源,以便它能夠執行與用戶界面有關的任務。特別是,系統分配一個T H R E A D I N F O結構,並將這個數據結構與線程聯繫起來。 

這個T H R E A D I N F O結構包含一組成員變量,利用這組成員,線程可以認爲它是在自己獨佔的環境中運行。T H R E A D I N F O是一個內部的、未公開的數據結構,用來指定線程的登記消息隊列(posted-message queue)、發送消息隊列( send-message queue)、應答消息隊列( r e p l y -message queue)、虛擬輸入隊列(virtualized-input queue)、喚醒標誌(wake flag)、以及用來描述線程局部輸入狀態的若干變量。圖2 6 - 1描述了T H R E A D I N F O結構和與之相聯繫的三個線程。 






爲什麼Winform不允許跨線程修改UI線程控件的值

在vs2003下,使用子線程調用ui線程創建的控件的屬性是不會有問題的,但是編譯的時候會出現警告,但是vs2005及以上版本就會有這樣的問題,下面是msdn上的描述 

" 當您在 Visual Studio 調試器中運行代碼時,如果您從一個線程訪問某個 UI 元素,而該線程不是創建該 UI 元素時所在的線程,則會引發 InvalidOperationException。調試器引發該異常以警告您存在危險的編程操作。UI 元素不是線程安全的,所以只應在創建它們的線程上進行訪問" 

從上面可以看出,這個異常實際是debugger耍的花招,也就是說,如果 你直接運行程序的exe文件,或者利用運行而不調試(Ctrl+F5)來運行你的程序,是不會拋出這樣的異常的.大概ms發現v2003的警告對廣大開發 者不起作用,所以用了一個比較狠一點的方法. 

不過問題依然存在:既然這樣設計的原因主要是因爲控件的值非線程安全,那麼DotNet framework中非線程安全的類千千萬萬,爲什麼偏偏跨線程修改Control的屬性會有這樣嚴格的限制策略呢? 

這個問題我還回答不好,希望博友們能夠予以補充 


有沒有什麼辦法可以簡化WinForm多線程的開發

使用backgroundworker,使用這個組建可以避免回調時的Invoke和BeginInvoke,並且提供了許多豐富的方法和事件

參見.Net多線程總結(二)-BackgroundWorker,我在這裏不再贅訴




線程池


線程池的作用是什麼 

作用是減小線程創建和銷燬的開銷 

創建線程涉及用戶模式和內核模式的切換,內存分配,dll通知等一系列過程,線程銷燬的步驟也是開銷很大的,所以如果應用程序使用了完一個線程,我們能把線程暫時存放起來,以備下次使用,就可以減小這些開銷 

所有進程使用一個共享的線程池,還是每個進程使用獨立的線程池? 

每 個進程都有一個線程池,一個Process中只能有一個實例,它在各個應用程序域(AppDomain)是共享的,.Net2.0 中默認線程池的大小爲工作線程25個,IO線程1000個,有一個比較普遍的誤解是線程池中會有1000個線程等着你去取,其實不然, ThreadPool僅僅保留相當少的線程,保留的線程可以用SetMinThread這個方法來設置,當程序的某個地方需要創建一個線程來完成工作時, 而線程池中又沒有空閒線程時,線程池就會負責創建這個線程,並且在調用完畢後,不會立刻銷燬,而是把他放在池子裏,預備下次使用,同時如果線程超過一定時 間沒有被使用,線程池將會回收線程,所以線程池裏存在的線程數實際是個動態的過程 

爲什麼不要手動線程池設置最大值? 

當我首次看到線程池的時候,腦袋裏的第一個念頭就是給他設定一個最大值,然而當我們查看ThreadPool的SetMaxThreads文檔時往往會看到一條警告:不要手動更改線程池的大小,這是爲什麼呢? 

其 實無論FileStream的異步讀寫,異步發送接受Web請求,甚至使用delegate的beginInvoke都會默認調用 ThreadPool,也就是說不僅你的代碼可能使用到線程池,框架內部也可能使用到,更改的後果影響就非常大,特別在iis中,一個應用程序池中的所有 WebApplication會共享一個線程池,對最大值的設定會帶來很多意想不到的麻煩 

線程池的線程爲何要分類? 

線 程池有一個方法可以讓我們看到線程池中可用的線程數量:GetAvaliableThread(out workerThreadCount,out iocompletedThreadCount),對於我來說,第一次看到這個函數的參數時十分困惑,因爲我期望這個函數直接返回一個整形,表明還剩多少 線程,這個函數居然一次返回了兩個變量. 

原來線程池裏的線程按照公用被分成了兩大類:工作線程和IO線程,或者IO完成線程,前者用於 執行普通的操作,後者專用於異步IO,比如文件和網絡請求,注意,分類並不說明兩種線程本身有差別,線程就是線程,是一種執行單元,從本質上來講都是一樣 的,線程池這樣分類,舉例來說,就好像某施工工地現在有1000把鐵鍬,規定其中25把給後勤部門用,其他都給施工部門,施工部門需要大量使用鐵鍬來挖地 基(例子土了點,不過說明問題還是有效的),後勤部門用鐵鍬也就是剷剷雪,剷剷垃圾,給工人師傅修修臨時住房,所以用量不大,顯然兩個部門的鐵鍬本身沒有 區別,但是這樣的劃分就爲管理兩個部門的鐵鍬提供了方便 

線程池中兩種線程分別在什麼情況下被使用,二者工作原理有什麼不同? 

下面這個例子直接說明了二者的區別,我們用一個流讀出一個很大的文件(大一點操作的時間長,便於觀察),然後用另一個輸出流把所讀出的文件的一部分寫到磁盤上 

我們用兩種方法創建輸出流,分別是 

創建了一個異步的流(注意構造函數最後那個true)

FileStream outputfs=new FileStream(writepath, FileMode.Create, FileAccess.Write, FileShare.None,256,true);

創建了一個同步的流 

FileStream outputfs = File.OpenWrite(writepath); 

然後在寫文件期間查看線程池的狀況
string readpath = "e:\\RHEL4-U4-i386-AS-disc1.iso";
string writepath = "e:\\kakakak.iso";
byte[] buffer = new byte[90000000];

//FileStream outputfs=new FileStream(writepath, FileMode.Create, FileAccess.Write, FileShare.None,256,true);
//Console.WriteLine("異步流");
//創建了一個同步的流

FileStream outputfs = File.OpenWrite(writepath);
Console.WriteLine("同步流");

//然後在寫文件期間查看線程池的狀況

ShowThreadDetail("初始狀態");

FileStream fs = File.OpenRead(readpath);

fs.BeginRead(buffer, 0, 90000000, delegate(IAsyncResult o)
{

outputfs.BeginWrite(buffer, 0, buffer.Length,

delegate(IAsyncResult o1)
{

Thread.Sleep(1000);

ShowThreadDetail("BeginWrite的回調線程");

}, null);

Thread.Sleep(500);//this is important cause without this, this Thread and the one used for BeginRead May seem to be same one
},

null);


Console.ReadLine();

public static void ShowThreadDetail(string caller)
{
int IO;
int Worker;
ThreadPool.GetAvailableThreads(out Worker, out IO);
Console.WriteLine("Worker: {0}; IO: {1}", Worker, IO);
}

輸出結果
異步流
Worker: 500; IO: 1000
Worker: 500; IO: 999
同步流
Worker: 500; IO: 1000
Worker: 499; IO: 1000
這兩個構造函數創建的流都可以使用BeginWrite來異步寫數據,但是二者行爲不同,當使用同步的流進行異步寫時,通過回調的輸出我們可以看到,他使用的是工作線程,而非IO線程,而異步流使用了IO線程而非工作線程 

其 實當沒有制定異步屬性的時候,.Net實現異步IO是用一個子線程調用fs的同步Write方法來實現的,這時這個子線程會一直阻塞直到調用完成.這個子 線程其實就是線程池的一個工作線程,所以我們可以看到,同步流的異步寫回調中輸出的工作線程數少了一,而使用異步流,在進行異步寫時,採用了 IOCP方法,簡單說來,就是當BeginWrite執行時,把信息傳給硬件驅動程序,然後立即往下執行(注意這裏沒有額外的線程),而當硬件準備就緒, 就會通知線程池,使用一個IO線程來讀取 

.Net線程池有什麼不足

沒有提供方法控制加入線程池的線程:一旦加入線程池,我們沒有辦法掛起,終止這些線程,唯一可以做的就是等他自己執行

1)不能爲線程設置優先級
2) 一個Process中只能有一個實例,它在各個AppDomain是共享的。ThreadPool只提供了靜態方法,不僅我們自己添加進去的 WorkItem使用這個Pool,而且.net framework中那些BeginXXX、EndXXX之類的方法都會使用此Pool。 
3)所支持的Callback不能有返回值。WaitCallback只能帶一個object類型的參數,沒有任何返回值。
4)不適合用在長期執行某任務的場合。我們常常需要做一個Service來提供不間斷的服務(除非服務器down掉),但是使用ThreadPool並不合適。

下面是另外一個網友總結的什麼不需要使用線程池,我覺得挺好,引用下來
如果您需要使一個任務具有特定的優先級。 
如果您具有可能會長時間運行(並因此阻塞其他任務)的任務。 
如果您需要將線程放置到單線程單元中(所有 ThreadPool 線程均處於多線程單元中)。 
如果您需要與該線程關聯的穩定標識。例如,您應使用一個專用線程來中止該線程、將其掛起或按名稱發現它。





鎖定與同步 

CLR怎樣實現lock(obj)鎖定? 

從原理上講,lock和Syncronized Attribute都是用Moniter.Enter實現的,比如如下代碼



object lockobj=new object();
lock(obj){ 
//do things 
}

在編譯時,會被編譯爲類似 

try{
Moniter.Enter(obj){
//do things
}
}
catch{}
finally{
Moniter.Exit(obj);
}


而[MethodImpl(MethodImplOptions.Synchronized)]標記爲同步的方法會在編譯時被lock(this)語句所環繞
所以我們只簡單探討Moniter.Enter的實現

(注:DotNet並非使用Win32API的CriticalSection來實現Moniter.Enter,不過他爲託管對象提供了一個類似的結構叫做Syncblk) 

每 個對象實例頭部都有一個指針,這個指針指向的結構,包含了對象的鎖定信息,當第一次使用Moniter.Enter(obj)時,這個obj對象的鎖定結 構就會被初時化,第二次調用Moniter.Enter時,會檢驗這個object的鎖定結構,如果鎖沒有被釋放,則調用會阻塞 




WaitHandle是什麼,他和他的派生類怎麼使用 

WaitHandle是Mutex,Semaphore,EventWaitHandler,AutoResetEvent,ManualResetEvent共同的祖先,他們包裝了用於同步的內核對象,也就是說是這些內核對象的託管版本。 

Mutex:類似於一個接力棒,拿到接力棒的線程纔可以開始跑,當然接力棒一次只屬於一個線程(Thread Affinity),如果這個線程不釋放接力棒(Mutex.ReleaseMutex),那麼沒辦法,其他所有需要接力棒運行的線程都知道能等着看熱鬧 

Semaphore: 類似於一個小桶,裏面裝了幾個小球,凡是拿到小球就可以跑,比如指定小桶裏最初有四個小球,那麼開始的四個線程就可以直接拿着自己的小球開跑,但是第五個 線程一看,小球被拿光了,就只好乖乖的等着有誰放一個小球到小桶裏(Semophore.Release),他才能跑,但是這裏的遊戲規則比較特殊,我們 可以隨意向小桶裏放入小球,也就是說我可以拿走一個小球,放回去倆,甚至一個都不拿,放回去5個,這樣就有五個線程可以拿着這些小球運行了.我們可以規定 小桶裏有開始有幾個小球(構造函數的第一個參數),也可以規定最多不能超過多少小球(構造函數的第二個參數) 

ManualResetEvent,AutoResetEvent可以參考http://www.cnblogs.com/uubox/archive/2007/12/18/1003953.html


什麼是用雙鎖實現Singleton,爲什麼要這樣做,雙鎖檢驗是不安全的嗎? 

使用雙鎖檢驗技巧來實現單件,來自於Java社區

public static MySingleton Instance{
get{
if(_instance!=null)}{
lock(_instance){
if(s_value==null){
_instance= new MySingleton();
}
}
}
}




這樣做其實是爲了提高效率,比起 
public static MySingleton Instance{ 

get{ 

lock(_instance){ 

if(s_value==null){ 

_instance= new MySingleton(); 



}

前一種方法在instance創建的時候不需要用lock同步,從而增進了效率

在java中這種技巧被證明是不安全的詳細見http://www.cs.umd.edu/~pugh/java/memoryModel/

但是在.Net下,這樣的技巧是成立的,因爲.Net使用了改進的內存模型

並且在.Net下,我們可以使用LazyInit來實現單件

private static readonly _instance=new MySingleton()

public static MySingleton Instance{

get{return _instance}

}

當第一此使用_instance時,CLR會生成這個對象,以後再訪問這個字段,將會直接返回

互斥對象(Mutex),信號量(Semaphore),事件(Event)對象與lock語句的比較 

首先這裏所謂的事件對象不是System.Event,而是一種用於同步的內核機制 

互斥對象和事件對象屬於內核對象,利用內核對象進行線程同步,線程必須要在用戶模式和內核模式間切換,所以一般效率很低,但利用互斥對象和事件對象這樣的內核對象,可以在多個進程中的各個線程間進行同步。 

lock或者Moniter是.net用一個特殊結構實現的,不涉及模式切換,也就是說工作在用戶方式下,同步速度較快,但是不能跨進程同步




什麼時候需要鎖定? 
剛剛接觸鎖定的程序員往往覺得這個世界非常的危險,每個靜態變量似乎都有可能產生競爭 

首先鎖定是解決競爭條件的,也就是多個線程同時訪問某個資源,造成意想不到的結果,比如,最簡單的情況,一個計數器,如果兩個線程同時加一,後果就是損失了一個計數,但是頻繁的鎖定又可能帶來性能上的消耗,還有最可怕的情況,死鎖 

到底什麼情況下我們需要使用鎖,什麼情況下不用呢? 

只有共享資源才需要鎖定
首先,只有可以被多線程訪問的共享資源才需要考慮鎖定,比如靜態變量,再比如某些緩存中的值,屬於線程內部的變量不需要鎖定 

把鎖定交給數據庫
數據庫除了存儲數據之外,還有一個重要的用途就是同步,數據庫本身用了一套複雜的機制來保證數據的可靠和一致性,這就爲我們節省了很多的精力.保證了數據源頭上的同步,我們多數的精力就可以集中在緩存等其他一些資源的同步訪問上了 

瞭解你的程序是怎麼運行的
實 際上在web開發中大多數邏輯都是在單個線程中展開的,無論asp.net還是php,一個請求都會在一個單獨的線程中處理,其中的大部分變量都是屬於這 個線程的,根本沒有必要考慮鎖定,當然對於asp.net中的application對象中的數據,我們就要小心一些了

WinForm中凡是使用BeginInvoke和Invoke調用的方法也都不需要考慮同步,因爲這用這兩個方法調用的方法會在UI線程中執行,因此實際是同步的,所以如果調用的方法中存在某些靜態變量,不需要考慮鎖定

業務邏輯對事務和線程安全的要求
這 條是最根本的東西,開發完全線程安全的程序是件很費時費力的事情,在電子商務等涉及金融系統的案例中,許多邏輯都必須嚴格的線程安全,所以我們不得不犧牲 一些性能,和很多的開發時間來做這方面的工作,而一般的應用中,許多情況下雖然程序有競爭的危險,我們還是可以不使用鎖定,比如有的時候計數器少一多一, 對結果無傷大雅的情況下,我們就可以不用去管他 

計算一下衝突的可能性
我以前曾經談到過,架構不要過設計,其實在這裏也一樣,假 如你的全局緩存裏的某個值每天只有幾百或者幾千個訪問,並且訪問時間很短,並且分佈均勻(實際上這是大多數的情況),那麼衝突的可能性就非常的少,也許每 500天才會出現一次或者更長,從7*24小時安全服務的角度來看,也完全符合要求,那麼你還會爲這樣萬分之一的可能性花80%的精力去設計嗎? 

請多使用lock,少用Mutex
如 果你一定要使用鎖定,請儘量不要使用內核模塊的鎖定機制,比如.net的 Mutex,Semaphore,AutoResetEvent,ManuResetEvent,使用這樣的機制涉及到了系統在用戶模式和內核模式間的切 換,所以性能差很多,但是他們的優點是可以跨進程同步線程,所以應該清楚的瞭解到他們的不同和適用範圍 






Web和IIS
應用程序池,WebApplication,和線程池之間有什麼關係

一個應用程序池是一個獨立的進程,擁有一個線程池,應用程序池中可以有多個WebApplication,每個運行在一個單獨的AppDomain中,這些WebApplication公用一個線程池

不同的AppDomain保證了每個WebApplication的靜態變量不會互相干擾,不同的應用程序池保證了一個網站癱瘓,其他不同進程中的站點還能正常運行

下圖說明了他們的關係




Web頁面怎麼調用異步WebService

把Page的Async屬性設置爲true,就可以調用異步的方法,但是這樣調用的效果可能並不如我們的相像,請參考Web中使用多線程來增強用戶體驗

.Net多線程總結(一)

- 張小魚 2010-10-22 09:48

.Net提供了許多多線程編程工具,可能是因爲太多了,所以掌握起來總是有一些頭疼,我在這裏講講我總結的一些多線程編程的經驗,希望對大家有幫助



不需要傳遞參數,也不需要返回參數


我們知道啓動一個線程最直觀的辦法是使用Thread類,具體步驟如下


ThreadStart threadStart=new ThreadStart(Calculate);
Thread thread=new Thread(threadStart);
thread.Start(); 

public void Calculate(){
double Diameter=0.5;
Console.Write("The perimeter Of Circle with a Diameter of {0} is {1}"Diameter,Diameter*Math.PI);




例1 

上 面我們用定義了一個ThreadStart類型的委託,這個委託制定了線程需要執行的方法:Calculate,在這個方法裏計算了一個直徑爲0.5的圓 的周長,並輸出.這就構成了最簡單的多線程的例子,在很多情況下這就夠用了,然後ThreadStart這個委託定義爲void ThreadStart(),也就是說,所執行的方法不能有參數,這顯然是個很大的不足,爲了彌補這個缺陷,聰明的程序員想出了許多好的方法,我們將在需 要傳遞多個參數一節中進行介紹,這裏我們先介紹.Net爲了解決這個問題而設定的另外一個委託:就是ParameterizedThreadStart ,我會在下面詳細講述 



需要傳遞單個參數


ParameterThreadStart的定義爲void ParameterizedThreadStart(object state)??使用這個這個委託定義的線程的啓動函數可以接受一個輸入參數,具體例子如下
ParameterizedThreadStart threadStart=new ParameterizedThreadStart(Calculate)
Thread thread=new Thread()
thread.Start(0.9); 

public void Calculate(object arg){
double Diameter=double(arg);
Console.Write("The perimeter Of Circle with a Diameter of {0} is {1}"Diameter,Diameter*Math.PI);
}




例2
Calculate方法有一個爲object類型的參數,雖然只有一個參數,而且還是object類型的,使用的時候尚需要類型轉換,但是好在可以有參數了,並且通過把多個參數組合到一個類中,然後把這個類的實例作爲參數傳遞,就可以實現多個參數傳遞



需要傳遞多個參數


雖然通過把需要的參數包裝到一個類中,委託ParameterizedThreadStart就可以傳遞多個參數,但是由於這個委託的傳入參數是object,所以不可避免的需要進行參數轉換,下面還有幾個常用的參數傳遞方法,讓我們來一一看來



使用專門的線程類


這 是許多程序員愛使用的經典模式,簡單來說,就是把需要另起線程執行的方法,和他需要的參數放到一個類中,參數作爲了類的屬性,調用時聲明此類的實例,然後 初始化屬性,方法執行時直接使用類裏初始化好的屬性來執行,這樣方法本身就可以不需要參數,而又起到了多參數傳遞的效果,於是使用本文最開始提到的不帶參 數的ThreadStart委託就可以了,並且由於需要執行的方法和參數都放在一個類中,充分體現了面向對象的特點.具體方法如下 

還是計算面積的方法的例子,我們把這個方法用一個類包裝起來,輸入參數Diameter(直徑)是這個類的一個字段

public class MyThread
{
public double Diameter=10; 

public double Result=0; 

public MyThread(int Diameter)
{
this.Diameter = Diameter;


public void Calculate()
{
Console.WriteLine( "Calculate Start");
Thread.Sleep(2000);
Result = Diameter*Math.PI;;
Console.WriteLine("Calculate End, Diameter is {0},Result is {1}" ,this.Diameter, Result);
}
}

MyThread t=new MyThread(5.0);

ThreadStart threadStart=new ThreadStart(t.Calculate)
Thread thread=new Thread(threadStart);
thread.Start(); 




例3

這種方法把參數傳遞變成了屬性共享,想傳遞多少個變量都可以,從封裝上講,把邏輯和邏輯涉及的數據封裝在一起,也很不錯,這個方法還有一個聰明的變體,利用了匿名方法,這種變體連獨立的類都省掉了,我現在給出這個方法

double Diameter = 6;

double Result=0;
Thread ta = new Thread(new ThreadStart(delegate()
{
Thread.Sleep(2000);

Result=Diameter * Math.PI;
Console.WriteLine("匿名 Calculate End, Diameter is {0},Result is {1}", Diameter, Result); ;
})); 
ta.Start();




例4 

這 個方法和上例道理相同,都是把參數傳遞變成了對變量的調用,從而取消了參數傳遞,但是,後者充分利用了匿名方法的一個性質,就是可以直接使用當前上下文的 局部變量,比如委託中的Diameter,和Result.當然,這樣做的缺點是如果匿名方法太長,程序的可讀性會降低,所以一般很少有人這樣做,這裏給 出這個方法供大家參考,關於匿名委託的資料可以參見 

聰明的讀者肯定想,既然可以用字段來傳入變量,當然也可以用字段傳出變量,比如在上面兩個例子裏我們看到計算結果都寫進了一個叫Result(加亮的地方)的變量裏,我們直接訪問這個變量不就可以得到計算結果了嗎? 

這樣做有一個致命的問題:既然是異步執行,主線程怎麼知道分線程什麼時候完成了計算呢?比如上兩個例子中,我們的線程都睡眠了2000毫秒,然後才進行計算,那麼如果主線程在沒有完成計算前訪問Result,只能得到一個0值.於是我們就有了下面的一系列解決方法. 



需要傳遞參數且需要返回參數 



剛纔說到主線程需要知道子線程什麼時候執行完成,可以使用Thread.ThreadState枚舉來判斷

當 線程的ThreadState==ThreadState.Stop時,一般就說明線程完成了工作,這時結果就可用了,如果不是這個狀態,就繼續執行別的 工作,或者等待一會,然後再嘗試.倘若需要等有多個子線程需的返回,並且需要用他們的結果來進行進異步計算,那就叫做線程同步了,下面我們介紹另外一種我 比較推薦的方法,能夠自定義參數個數,並且返回數據,而且使用起來也相對方便

使用委託的異步調用方法和回調

首先我們要把需要異步調用的方法定義爲一個委託,然後利用BeginInvoke來異步調用,BeginInvoke的第一個參數就是直徑,第二個是當線程執行完畢後的調用的方法

delegate double CalculateMethod(double Diameter); 

static CalculateMethod calcMethod;
double result = 0; 

static void Main(string[] args)


calcMethod = new CalculateMethod(Calculate);

calcMethod.BeginInvoke(5, new AsyncCallback(TaskFinished), null); 



///<summary>
///線程調用的函數
///<summary> 

public static double Calculate(double Diameter)
{
return Diameter * Math.PI;


///<summary>
///線程完成之後回調的函數
///<summary>
public static void TaskFinished(IAsyncResult result)
{
result=calcMethod.EndInvoke(result);
}




例5 

注意,再線程執行完畢後執行的方法TaskFinished中,我們使用了EndInvoke來取得這個函數的返回值



線程池



線程雖然是個好東西,但是也是個資源消耗大戶,許多時候,我們需要用多線程,但是又不希望線程的數量過多,這就是線程池的作用,.Net爲我們提供了現成的線程池ThreadPool,他的使用如下

WaitCallback w = new WaitCallback(Calculate);
ThreadPool.QueueUserWorkItem(w, 1.0);
ThreadPool.QueueUserWorkItem(w, 2.0);

ThreadPool.QueueUserWorkItem(w, 3.0);
ThreadPool.QueueUserWorkItem(w, 4.0);

public static void Calculate(double Diameter)
{
return Diameter * Math.PI;





例6

首 先定義一個WaitCallback委託,WaitCallback的格式是void WaitCallback(object state),也就是說你的方法必須符合這個格式,接着調用QueueUserWorkItem,將這個任務加入線程池,當縣城池有空閒線時,將會調度並 運行你的代碼

每一個進程都有一個線程池,線程池的默認大小是25,我們可以通過SetMaxThreads方法來設置其最大值.

[注]由於每個進程只有一個線程池,所以如果是在iis進程,或者sqlserver的進程中使用線程池,並且需要設置線程池的最大容量的話,會影響到iis進程或sql進程,所以這兩種情況下要特別小心



控制權 



在 和大家交談的時候我發現凡是習慣了面向對象思維的同事,總是對多線程情況下的執行上下文很困擾,比如例5中,主程序啓動了子線程執行Calculate方 法,執行完畢後回調TaskFinished,假如主線程id是1,子線程id是2,那麼Calculate肯定是在id=2的線程中執行,那麼他的回調 函數TaskFinished呢? 同樣也是在id=2的線程上下文中執行,不信你輸出線程id試試,這通常不是什麼問題,但是當我們需要在Winform編程中使用子線程時,就有可能會引 起問題了,我們將在下面講這個問題 



窗體程序多線程編程的特殊性 



當我們把例5的回調代碼稍加修改,搬到winform裏面,就可以看到問題所在了

public static void TaskFinished(IAsyncResult result)
{
result=calcMethod.EndInvoke(result);
this.TextBox1.Text=result;
}


程 序的原意是在線程執行完畢後講結果寫入一個TextBox,然而當程序執行到this.TextBox1.Text=result這裏的時候就抱錯了.原 來WinForm對線程有很嚴格的要求,除了創建這些控件的線程,其他線程想跨線程訪問WinForm上的控件的屬性和方法是不允許(除了幾個特殊屬 性),在有的版本系統上,比如XP,對這個問題進行了處理,跨線程控件訪問可以被執行,但是大多數windows系統都是不可以的,那麼如果我們確實需要 跨線程修改控件屬性,或者調用控件的方法,就必須用到控件的一個方法Invoke,這個方法可以把執行上下文切換回創建這些控件的線程,具體操作如下

delegate void changeText(string result);

public static void TaskFinished(IAsyncResult result)
{
result=calcMethod.EndInvoke(result); 

this.BeginInvoke(new changeText(this.textBox1.AppendText),t.Result.ToString())
}

//代碼

 public partial class Form1 : Form
    {
        private delegate void ThreadWork(int i);
        Thread thread;

        public Form1()
        {
            InitializeComponent();

            // CheckForIllegalCrossThreadCalls = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            this.richTextBox1.Text = "程序開始:\r\n";
            UpdateText();
        }

        public void UpdateText()
        {
            thread = new Thread(new ThreadStart(CrossThreadFlush));
            thread.IsBackground = true;
            thread.Start();
        }


        private void CrossThreadFlush()
        {
            while (true)
            { //將sleep和無限循環放在等待異步的外面 
                
                for (int i = 1; i < 100; i++)
                {
                    ThreadFunction(i);
                    Thread.Sleep(500);
                }
            }
        }

        private void ThreadFunction(int i)
        {
            if (this.richTextBox1.InvokeRequired)//等待異步 
            {
                ThreadWork fc = new ThreadWork(ThreadFunction);
               // this.Invoke(fc);//通過代理調用刷新方法 
                this.Invoke(fc, new object[1] { i });
                
            }
            else
            {
                this.richTextBox1.Text = string.Format("第{0}個\t{1:mm-ss}\r\n", i, DateTime.Now) + this.richTextBox1.Text;
                this.richTextBox1.Refresh();

            }
        }

    }
}

由於委託中必須使用方法,所以我用AppendTex方法t,而不是直接設置Text屬性,你如果想設置text屬性,就必須自己包裝一個方法,然後連接到委託了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章