當前位置:首頁 > 資訊 > info6 > 正文

編程資料 -C# 多線程

發表于: 2011-04-19   作者:chclvzxx   來源:轉載   瀏覽:
摘要: 編程資料-多線程C#多線程編程實例實戰作者:劉彈www.ASPCool.com時間:2003-5-17上午10:24:05閱讀次數:10996單個寫入程序/多個閱讀程序在.Net類庫中其實已經提供了實現,即System.Threading.ReaderWriterLock類。本文通過對常見的單個寫入/多個閱讀程序的分析來探索c#的多線程編程。問題的提出所謂單個寫入程序/多個閱讀程序的線程同步問題,

編程資料 - 多線程
C#多線程編程實例實戰
作者: 劉彈 www.ASPCool.com 時間:2003-5-17 上午 10:24:05 閱讀次數:10996
單個寫入程序/多個閱讀程序在.Net 類庫中其實已經提供了實現,即
System.Threading.ReaderWriterLock 類。本文通過對常見的單個寫入/多個閱讀程序的分析來探索c#
的多線程編程。
問題的提出
所謂單個寫入程序/多個閱讀程序的線程同步問題,是指任意數量的線程訪問共享資源時,寫入程序
(線程)需要修改共享資源,而閱讀程序(線程)需要讀取數據。在這個同步問題中,很容易得到下面二
個要求:
1) 當一個線程正在寫入數據時,其他線程不能寫,也不能讀。
2) 當一個線程正在讀入數據時,其他線程不能寫,但能夠讀。
在數據庫應用程序環境中經常遇到這樣的問題。比如說,有n 個最終用戶,他們都要同時訪問同一
個數據庫。其中有m 個用戶要將數據存入數據庫,n-m 個用戶要讀取數據庫中的記錄。
很顯然,在這個環境中,我們不能讓兩個或兩個以上的用戶同時更新同一條記錄,如果兩個或兩個
以上的用戶都試圖同時修改同一記錄,那么該記錄中的信息就會被破壞。
我們也不讓一個用戶更新數據庫記錄的同時,讓另一用戶讀取記錄的內容。因為讀取的記錄很有可
能同時包含了更新和沒有更新的信息,也就是說這條記錄是無效的記錄。
實現分析
規定任一線程要對資源進行寫或讀操作前必須申請鎖。根據操作的不同,分為閱讀鎖和寫入鎖,操
作完成之后應釋放相應的鎖。將單個寫入程序/多個閱讀程序的要求改變一下,可以得到如下的形式:
一個線程申請閱讀鎖的成功條件是:當前沒有活動的寫入線程。
一個線程申請寫入鎖的成功條件是:當前沒有任何活動(對鎖而言)的線程。
因此,為了標志是否有活動的線程,以及是寫入還是閱讀線程,引入一個變量m_nActive,如果
m_nActive > 0,則表示當前活動閱讀線程的數目,如果m_nActive=0,則表示沒有任何活動線程,m_nActive
<0,表示當前有寫入線程在活動,注意m_nActive<0,時只能取-1 的值,因為只允許有一個寫入線程活動。
為了判斷當前活動線程擁有的鎖的類型,我們采用了線程局部存儲技術(請參閱其它參考書籍),
將線程與特殊標志位關聯起來。
申請閱讀鎖的函數原型為:public void AcquireReaderLock( int millisecondsTimeout ),其中
的參數為線程等待調度的時間。函數定義如下:
public void AcquireReaderLock( int millisecondsTimeout )
{
// m_mutext 很快可以得到,以便進入臨界區
m_mutex.WaitOne( );
// 是否有寫入線程存在
bool bExistingWriter = ( m_nActive < 0 );
if( bExistingWriter )
{ //等待閱讀線程數目加1,當有鎖釋放時,根據此數目來調度線程
m_nWaitingReaders++;
}
else
{ //當前活動線程加1
m_nActive++;
}
m_mutex.ReleaseMutex();
//存儲鎖標志為Reader
System.LocalDataStoreSlot slot = Thread.GetNamedDataSlot(m_strThreadSlotName);
object obj = Thread.GetData( slot );
LockFlags flag = LockFlags.None;
if( obj != null )
flag = (LockFlags)obj ;
if( flag == LockFlags.None )
{
Thread.SetData( slot, LockFlags.Reader );
}
else
{
Thread.SetData( slot, (LockFlags)((int)flag | (int)LockFlags.Reader ) );
}
if( bExistingWriter )
{ //等待指定的時間
this.m_aeReaders.WaitOne( millisecondsTimeout, true );
}
}
它首先進入臨界區(用以在多線程環境下保證活動線程數目的操作的正確性)判斷當前活動線程的
數目,如果有寫線程(m_nActive<0)存在,則等待指定的時間并且等待的閱讀線程數目加1。如果當前活動
線程是讀線程(m_nActive>=0),則可以讓讀線程繼續運行。
申請寫入鎖的函數原型為:public void AcquireWriterLock( int millisecondsTimeout ),其中
的參數為等待調度的時間。函數定義如下:
public void AcquireWriterLock( int millisecondsTimeout )
{
// m_mutext 很快可以得到,以便進入臨界區
m_mutex.WaitOne( );
// 是否有活動線程存在
bool bNoActive = m_nActive == 0;
if( !bNoActive )
{
m_nWaitingWriters++;
}
else
{
m_nActive--;
}
m_mutex.ReleaseMutex();
//存儲線程鎖標志
System.LocalDataStoreSlot slot = Thread.GetNamedDataSlot( "myReaderWriterLockDataSlot" );
object obj = Thread.GetData( slot );
LockFlags flag = LockFlags.None;
if( obj != null )
flag = (LockFlags)Thread.GetData( slot );
if( flag == LockFlags.None )
{
Thread.SetData( slot, LockFlags.Writer );
}
else
{
Thread.SetData( slot, (LockFlags)((int)flag | (int)LockFlags.Writer ) );
}
//如果有活動線程,等待指定的時間
if( !bNoActive )
this.m_aeWriters.WaitOne( millisecondsTimeout, true );
}
它首先進入臨界區判斷當前活動線程的數目,如果當前有活動線程存在,不管是寫線程還是讀線程
(m_nActive),線程將等待指定的時間并且等待的寫入線程數目加1,否則線程擁有寫的權限。
釋放閱讀鎖的函數原型為:public void ReleaseReaderLock()。函數定義如下:
public void ReleaseReaderLock()
{
System.LocalDataStoreSlot slot = Thread.GetNamedDataSlot(m_strThreadSlotName );
LockFlags flag = (LockFlags)Thread.GetData( slot );
if( flag == LockFlags.None )
{
return;
}
bool bReader = true;
switch( flag )
{
case LockFlags.None:
break;
case LockFlags.Writer:
bReader = false;
break;
}
if( !bReader )
return;
Thread.SetData( slot, LockFlags.None );
m_mutex.WaitOne();
AutoResetEvent autoresetevent = null;
this.m_nActive --;
if( this.m_nActive == 0 )
{
if( this.m_nWaitingReaders > 0 )
{
m_nActive ++ ;
m_nWaitingReaders --;
autoresetevent = this.m_aeReaders;
}
else if( this.m_nWaitingWriters > 0)
{
m_nWaitingWriters--;
m_nActive --;
autoresetevent = this.m_aeWriters ;
}
}
m_mutex.ReleaseMutex();
if( autoresetevent != null )
autoresetevent.Set();
}
釋放閱讀鎖時,首先判斷當前線程是否擁有閱讀鎖(通過線程局部存儲的標志),然后判斷是否有
等待的閱讀線程,如果有,先將當前活動線程加1,等待閱讀線程數目減1,然后置事件為有信號。如果沒
有等待的閱讀線程,判斷是否有等待的寫入線程,如果有則活動線程數目減1,等待的寫入線程數目減1。
釋放寫入鎖與釋放閱讀鎖的過程基本一致,可以參看源代碼。
注意在程序中,釋放鎖時,只會喚醒一個閱讀程序,這是因為使用AutoResetEvent 的原歷,讀者可
自行將其改成ManualResetEvent,同時喚醒多個閱讀程序,此時應令m_nActive 等于整個等待的閱讀線程
數目。
測試
測試程序取自.Net FrameSDK 中的一個例子,只是稍做修改。測試程序如下,
using System;
using System.Threading;
using MyThreading;
class Resource {
myReaderWriterLock rwl = new myReaderWriterLock();
public void Read(Int32 threadNum) {
rwl.AcquireReaderLock(Timeout.Infinite);
try {
Console.WriteLine("Start Resource reading (Thread={0})", threadNum);
Thread.Sleep(250);
Console.WriteLine("Stop Resource reading (Thread={0})", threadNum);
}
finally {
rwl.ReleaseReaderLock();
}
}
public void Write(Int32 threadNum) {
rwl.AcquireWriterLock(Timeout.Infinite);
try {
Console.WriteLine("Start Resource writing (Thread={0})", threadNum);
Thread.Sleep(750);
Console.WriteLine("Stop Resource writing (Thread={0})", threadNum);
}
finally {
rwl.ReleaseWriterLock();
}
}
}
class App {
static Int32 numAsyncOps = 20;
static AutoResetEvent asyncOpsAreDone = new AutoResetEvent(false);
static Resource res = new Resource();
public static void Main() {
for (Int32 threadNum = 0; threadNum < 20; threadNum++) {
ThreadPool.QueueUserWorkItem(new WaitCallback(UpdateResource), threadNum);
}
asyncOpsAreDone.WaitOne();
Console.WriteLine("All operations have completed.");
Console.ReadLine();
}
// The callback method's signature MUST match that of a System.Threading.TimerCallback
// delegate (it takes an Object parameter and returns void)
static void UpdateResource(Object state) {
Int32 threadNum = (Int32) state;
if ((threadNum % 2) != 0) res.Read(threadNum);
else res.Write(threadNum);
if (Interlocked.Decrement(ref numAsyncOps) == 0)
asyncOpsAreDone.Set();
}
}
從測試結果中可以看出,可以滿足單個寫入程序/多個閱讀程序的實現要求。
C# 一個多線程操作控件的例子.
//多線程間 控件的操作例子
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;
using System.Data.SqlClient;
using System.Collections;
namespace AutoMessager
{
delegate void myDelegate();
delegate void SetTextCallback(string text);
public partial class frmAutoMsg : Form
{
event myDelegate myEvent;
string connStr = string.Empty;
Thread thd;
//private Icon eyeIcon;
//private NotifyIconEx notifyIconA;
//private NotifyIconEx notifyIconB;
private bool canClosed = false;
public frmAutoMsg()
{
this.ShowInTaskbar = false;
InitializeComponent();
//eyeIcon = new Icon(GetType(), "EYE.ICO");
notifyIcon1.ContextMenu = contextMenuB;
}
private void SetText(string text)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.txtMsgStatus.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else
{
this.txtMsgStatus.Text += text;
}
}
private void frmAutoMsg_Load(object sender, EventArgs e)
{
connStr = System.Configuration.ConfigurationManager.AppSe
ttings["ConnString"];
thd = new Thread(new ThreadStart(doEvent));
thd.IsBackground = true;
thd.Start();
//doEvent();
//notifyIcon1.Visible = true;
}
/// <summary>
/// 員工合同到期提醒
/// </summary>
void UpUserState()
{
SqlConnection conn = null;
DateTime now = DateTime.Now;
SqlTransaction tran = null;
SqlDataReader dr = null;
try
{
//數據庫操作部分省略
SetText(" 系統提示: 職員合同消息更新成功! ");
SetText("執行時間:" + now.ToString("yyyy-MM-dd HH:mm:s
s") + " ");
}
catch (Exception ex)
{
dr.Close();
tran.Rollback();
SetText(" 系統錯誤: 職員合同消息更新錯誤:" + ex.Messa
ge + " ");
SetText("執行時間:" + now.ToString("yyyy-MM-dd HH:mm:
ss") + " ");
}
finally
{
dr.Close();
conn.Close();
conn.Dispose();
}
}
/// <summary>
/// 采購及供貨 到貨提醒
/// </summary>
void UpCaiGou()
{
SqlConnection conn = null;
DateTime now = DateTime.Now;
SqlTransaction tran = null;
SqlDataReader dr = null;
try
{
//數據庫操作部分省略
SetText("系統提示: 合同采購消息更新成功! ");
SetText("執行時間:" + now.ToString("yyyy-MM-dd HH:mm:
ss") + " ");
}
catch (Exception ex)
{
dr.Close();
tran.Rollback();
SetText("系統錯誤: 合同采購消息更新錯誤:" + ex.Messag
e + " ");
SetText("執行時間:" + now.ToString("yyyy-MM-dd HH:mm:
ss") + " ");
}
finally
{
dr.Close();
conn.Close();
conn.Dispose();
}
}
/// <summary>
/// 供貨收款情況提醒
/// </summary>
void GetMoney()
{
SqlConnection conn = null;
DateTime now = DateTime.Now;
try
{
//數據庫操作部分省略
SetText("系統提示: 供貨付款消息更新成功! ");
SetText("執行時間:" + now.ToString("yyyy-MM-dd HH:mm:s
s") + " ");
}
catch (Exception ex)
{
SetText("系統錯誤: 供貨付款消息更新錯誤:" + ex.Messag
e + " ");
SetText("執行時間:" + now.ToString("yyyy-MM-dd HH:mm:
ss") + " ");
}
finally
{
conn.Close();
conn.Dispose();
}
}
void doEvent()
{
//int weather = int.Parse(weatherTime.Text);
//int del = int.Parse(fileTime.Text);
// if(weather < 1 || weather > 24 || del < 1 |
| del > 24)
// {
// MessageBox.Show("時間輸入有錯!");
// button1.Enabled = true;
// return ;
// }
while (true)
{
//DateTime now = DateTime.Now;
int i = DateTime.Now.Hour;
if (i > 2 && i < 4)
{
myEvent = new myDelegate(UpUserState);
myEvent += new myDelegate(UpCaiGou);
// myEvent += new myDelegate(GetMoney);
}
//if (now.Hour == 3)
//{
// myEventB = new myDelegate(deltemp);
//}
//if (myEventA != null) myEventA();
//if (myEventB != null) myEventB();
if (myEvent != null)
{
myEvent();
myEvent = null;
}
Application.DoEvents();
Thread.Sleep(6000000); //每100 分鐘檢查一次時間
}
}
private void frmAutoMsg_FormClosing(object sender, FormClosin
gEventArgs e)
{
if (canClosed == false)
{
e.Cancel = true;
this.Hide();
this.Visible = false;
//this.
}
}
private void menuItem2_Click(object sender, EventArgs e)
{
this.ShowInTaskbar = true;
this.Show();
this.Visible = true; //恢復主窗體
}
private void menuItem1_Click(object sender, EventArgs e)
{
canClosed = true;
Application.Exit();
}
private void notifyIcon1_MouseDoubleClick(object sender, Mous
eEventArgs e)
{
this.ShowInTaskbar = true;
this.Show();
if (this.Visible == false)
{
this.Visible = true;
}
}
private void btnClear_Click(object sender, EventArgs e)
{
this.txtMsgStatus.Text = "";
}
private void btnUpMsg_Click(object sender, EventArgs e)
{
myEvent = new myDelegate(UpUserState);
myEvent += new myDelegate(UpCaiGou);
//myEvent += new myDelegate(GetMoney);
if (myEvent != null)
myEvent();
}
}
}
.NET 事件模型教程(一)
目錄
? 事件、事件處理程序概念
? 問題描述:一個需要較長時間才能完成的任務
? 高耦合的實現
? 事件模型的解決方案,簡單易懂的 VB.NET 版本
? 委托(delegate)簡介
? C# 實現
? 向“.NET Framework 類庫設計指南”靠攏,標準實現
事件、事件處理程序概念
在面向對象理論中,一個對象(類的實例)可以有屬性(property,獲取或設置對象的狀態)、方法(method,
對象可以做的動作)等成員外,還有事件(event)。所謂事件,是對象內部狀態發生了某些變化、或者對象做
某些動作時(或做之前、做之后),向外界發出的通知。打個比方就是,對象“張三”肚子疼了,然后他站在空地
上大叫一聲“我肚子疼了!”事件就是這個通知。
那么,相對于對象內部發出的事件通知,外部環境可能需要應對某些事件的發生,而做出相應的反應。接著上面
的比方,張三大叫一聲之后,救護車來了把它接到醫院(或者瘋人院,呵呵,開個玩笑)。外界因應事件發生而
做出的反應(具體到程序上,就是針對該事件而寫的那些處理代碼),稱為事件處理程序(event handler)。
事件處理程序必須和對象的事件掛鉤后,才可能會被執行。否則,孤立的事件處理程序不會被執行。另一方面,
對象發生事件時,并不一定要有相應的處理程序。就如張三大叫之后,外界環境沒有做出任何反應。也就是說,
對象的事件和外界對該對象的事件處理之間,并沒有必然的聯系,需要你去掛接。
在開始學習之前,我希望大家首先區分“事件”和“事件處理程序”這兩個概念。事件是隸屬于對象(類)本身的,
事件處理程序是外界代碼針對對象的事件做出的反應。事件,是對象(類)的設計者、開發者應該完成的;事件
處理程序是外界調用方需要完成的。簡單的說,事件是“內”;事件處理程序是“外”。
了解以上基本概念之后,我們開始學習具體的代碼實現過程。因為涉及代碼比較多,限于篇幅,我只是將代碼中
比較重要的部分貼在文章里,進行解析,剩余代碼還是請讀者自己查閱,我已經把源代碼打了包提供下載。我也
建議你對照這些源代碼,來學習教程。[下載本教程的源代碼]
[TOP]
問題描述:一個需要較長時間才能完成的任務
Demo 1A,問題描述。這是一個情景演示,也是本教程中其他 Demo 都致力于解決的一個“實際問題”:Worker
類中有一個可能需要較長時間才能完成的方法 DoLongTimeTask:
using System;
using System.Threading;
namespace percyboy.EventModelDemo.Demo1A
{
// 需要做很長時間才能完成任務的 Worker,沒有加入任何匯報途徑。
public class Worker
{
// 請根據你的機器配置情況,設置 MAX 的值。
// 在我這里(CPU: AMD Sempron 2400+, DDRAM 512MB)
// 當 MAX = 10000,任務耗時 20 秒。
private const int MAX = 10000;
public Worker()
{
}
public void DoLongTimeTask()
{
int i;
bool t = false;
for (i = 0; i <= MAX; i++)
{
// 此處 Thread.Sleep 的目的有兩個:
// 一個是不讓 CPU 時間全部耗費在這個任務上:
// 因為本例中的工作是一個純粹消耗 CPU 計算資源的任務。
// 如果一直讓它一直占用 CPU,則 CPU 時間幾乎全部都耗費
于此。
// 如果任務時間較短,可能影響不大;
// 但如果任務耗時也長,就可能會影響系統中其他任務的正
常運行。
// 所以,Sleep 就是要讓 CPU 有機會“分一下心”,
// 處理一下來自其他任務的計算請求。
//
// 當然,這里的主要目的是為了讓這個任務看起來耗時更長
一點。
Thread.Sleep(1);
t = !t;
}
}
}
}
界面很簡單(本教程中其他 Demo 也都沿用這個界面,因為我們主要的研究對象是 Worker.cs):
單擊“Start”按鈕后,開始執行該方法。(具體的機器配置條件,完成此任務需要的時間也不同,你可以根據你
的實際情況調整代碼中的 MAX 值。)
在沒有進度指示的情況下,界面長時間的無響應,往往會被用戶認為是程序故障或者“死機”,而實際上,你的工
作正在進行還沒有結束。此次教程就是以解決此問題為實例,向你介紹 .NET 中事件模型的原理、設計與具體
編碼實現。
[TOP]
高耦合的實現
Demo 1B,高度耦合。有很多辦法可以讓 Worker 在工作的時候向用戶界面報告進度,比如最容易想到的:
public void DoLongTimeTask()
{
int i;
bool t = false;
for (i = 0; i <= MAX; i++)
{
Thread.Sleep(1);
t = !t;
在此處書寫刷新用戶界面狀態欄的代碼
}
}
如果說 DoLongTimeTask 是用戶界面(Windows 窗體)的一個方法,那么上面藍色部分或許很簡單,可能
只不過是如下的兩行代碼:
double rate = (double)i / (double)MAX;
this.statusbar.Text = String.Format(@"已完成 {0:P2} ...",
rate);
不過這樣的話,DoLongTimeTask 就是這個 Windows 窗體的一部分了,顯然它不利于其他窗體調用這段代
碼。那么:Worker 類應該作為一個相對獨立的部分存在。源代碼 Demo1B 中給出了這樣的一個示例(應該
還有很多種、和它類似的方法):
Windows 窗體 Form1 中單擊“Start”按鈕后,初始化 Worker 類的一個新實例,并執行它的
DoLongTimeTask 方法。但你應該同時看到,Form1 也賦值給 Worker 的一個屬性,在 Worker 執行
DoLongTimeTask 方法時,通過這個屬性刷新 Form1 的狀態欄。Form1 和 Worker 之間相互粘在一起:
Form1 依賴于 Worker 類(因為它單擊按鈕后要實例化 Worker),Worker 類也依賴于 Form1(因為它在
工作時,需要訪問 Form1)。這二者之間形成了高度耦合。
高度耦合同樣不利于代碼重用,你仍然無法在另一個窗體里使用 Worker 類,代碼靈活度大為降低。正確的設
計原則應該是努力實現低耦合:如果 Form1 必須依賴于 Worker 類,那么 Worker 類就不應該再反過來依
賴于 Form1。
下面我們考慮使用 .NET 事件模型解決上述的“高度耦合”問題:
讓 Worker 類在工作時,向外界發出“進度報告”的事件通知(RateReport)。同時,為了演示更多的情景,
我們讓 Worker 類在開始 DoLongTimeTask 之前發出一個“我要開始干活了!總任務數有 N 件。”的事件通
知(StartWork),并在完成任務時發出“任務完成”的事件通知(EndWork)。
采用事件模型后,類 Worker 本身并不實際去刷新 Form1 的狀態欄,也就是說 Worker 不依賴于 Form1。
在 Form1 中,單擊“Start”按鈕后,Worker 的一個實例開始工作,并發出一系列的事件通知。我們需要做的
是為 Worker 的事件書寫事件處理程序,并將它們掛接起來。
[TOP]
事件模型的解決方案,簡單易懂的 VB.NET 版本
Demo 1C,VB.NET 代碼。雖然本教程以 C# 為示例語言,我還是給出一段 VB.NET 的代碼輔助大家的理
解。因為我個人認為 VB.NET 的事件語法,能讓你非常直觀的領悟到 .NET 事件模型的“思維方式”:
Public Class Worker
Private Const MAX = 10000
Public Sub New()
End Sub
' 注:此例的寫法不符合 .NET Framework 類庫設計指南中的約定,
' 只是為了讓你快速理解事件模型而簡化的。
' 請繼續閱讀,使用 Demo 1F 的 VB.NET 標準寫法。
'
' 工作開始事件,并同時通知外界需要完成的數量。
Public Event StartWork(ByVal totalUnits As Integer)
' 進度匯報事件,通知外界任務完成的進度情況。
Public Event RateReport(ByVal rate As Double)
' 工作結束事件。
Public Event EndWork()
Public Sub DoLongTimeTask()
Dim i As Integer
Dim t As Boolean = False
Dim rate As Double
' 開始工作前,向外界發出事件通知
RaiseEvent StartWork(MAX)
For i = 0 To MAX
Thread.Sleep(1)
t = Not t
rate = i / MAX
RaiseEvent RateReport(rate)
Next
RaiseEvent EndWork()
End Sub
首先是事件的聲明部分:你只需寫上 Public Event 關鍵字,然后寫事件的名稱,后面的參數部分寫上需要發送
到外界的參數聲明。
然后請注意已標記為藍色的 RaiseEvent 關鍵字,VB.NET 使用此關鍵字在類內部引發事件,也就是向外界發
送事件通知。請注意它的語法,RaiseEvent 后接上你要引發的事件名稱,然后是具體的事件參數值。
從這個例子中,我們可以加深對事件模型的認識:事件是對象(類)的成員,在對象(類)內部狀態發生了一些
變化(比如此例中 rate 在變化),或者對象做一些動作時(比如此例中,方法開始時,向外界 raise event;
方法結束時,向外界 raise event),對象(類)發出的通知。并且,你也了解了事件參數的用法:事件參數是
事件通知的相關內容,比如 RateReport 事件通知需要報告進度值 rate,StartWork 事件通知需要報告總任
務數 MAX。
我想 RaiseEvent 很形象的說明了這些道理。
[TOP]
委托(delegate)簡介。
在學習 C# 實現之前,我們首先應該了解一些關于“委托”的基礎概念。
你可以簡單的把“委托(delegate)”理解為 .NET 對函數的包裝(這是委托的主要用途)。委托代表一“類”函
數,它們都符合一定的規格,如:擁有相同的參數個數、參數類型、返回值類型等。也可以認為委托是對函數的
抽象,是函數的“類”(類是具有某些相同特征的事物的抽象)。這時,委托的實例將代表一個具體的函數。
你可以用如下的方式聲明委托:
public delegate void MyDelegate(int integerParameter);
如上的委托將可以用于代表:有且只有一個整數型參數、且不帶返回值的一組函數。它的寫法和一個函數的寫法
類似,只是多了 delegate 關鍵字、而沒有函數體。(注:本文中的函數(function),取了面向過程理論中慣
用的術語。在完全面向對象的 .NET/C# 中,我用以指代類的實例方法或靜態方法(method),希望不會因此
引起誤解。順帶地,既然完全面向對象,其實委托本身也是一種對象。)
委托的實例化:既然委托是函數的“類”,那么使用委托之前也需要實例化。我們先看如下的代碼:
public class Sample
{
public void DoSomething(int mode)
{
Console.WriteLine("test function.");
}
public static void Hello(int world)
{
Console.WriteLine("hello, world!");
}
}
我們看到 Sample 的實例方法 DoSomething 和靜態方法 Hello 都符合上面已經定義了的 MyDelegate
委托的“規格”。那么我們可以使用 MyDelegate 委托來包裝它們,以用于特殊的用途(比如下面要講的事件模
型,或者將來教程中要講的多線程模型)。當然,包裝的過程其實也是委托的實例化過程:
Sample sp = new Sample();
MyDelegate del = new MyDelegate(sp.DoSomething);
這是對上面的實例方法的包裝。但如果這段代碼寫在 Sample 類內部,則應使用 this.DoSomething 而不用
新建一個 Sample 實例。對 Sample 的 Hello 靜態方法可以包裝如下:
MyDelegate del = new MyDelegate(Sample.Hello);
調用委托:對于某個委托的實例(其實是一個具體的函數),如果想執行它:
del(12345);
直接寫上委托實例的名字,并在括號中給相應的參數賦值即可。(如果函數有返回值,也可以像普通函數那樣接
收返回值)。
[TOP]
C# 實現
Demo 1D,C# 實現。這里給出 Demo 1C 中 VB.NET 代碼的 C# 實現:是不是比 VB.NET 的代碼復雜
了一些呢?
using System;
using System.Threading;
namespace percyboy.EventModelDemo.Demo1D
{
// 需要做很長時間才能完成任務的 Worker,這次我們使用事件向外界通知
進度。
public class Worker
{
private const int MAX = 10000;
// 注:此例的寫法不符合 .NET Framework 類庫設計指南中的約定,
// 只是為了讓你快速理解事件模型而簡化的。
// 請繼續閱讀,使用 Demo 1E / Demo 1H 的 C# 標準寫法。
//
public delegate void StartWorkEventHandler(int totalUnits);
public delegate void EndWorkEventHandler();
public delegate void RateReportEventHandler(double rate);
public event StartWorkEventHandler StartWork;
public event EndWorkEventHandler EndWork;
public event RateReportEventHandler RateReport;
public Worker()
{
}
public void DoLongTimeTask()
{
int i;
bool t = false;
double rate;
if (StartWork != null)
{
StartWork(MAX);
}
for (i = 0; i <= MAX; i++)
{
Thread.Sleep(1);
t = !t;
rate = (double)i / (double)MAX;
if (RateReport != null)
{
RateReport(rate);
}
}
if (EndWork != null)
{
EndWork();
}
}
}
}
這份代碼和上面 VB.NET 代碼實現一致的功能。通過 C# 代碼,我們可以看到被 VB.NET 隱藏了的一些實現
細節:
首先,這里一開始聲明了幾個委托(delegate)。然后聲明了三個事件,這里請注意 C# 事件聲明的方法:
public event [委托類型] [事件名稱];
這里你可以看到 VB.NET 隱藏了聲明委托的步驟。
另外提醒你注意代碼中具體引發事件的部分:
if (RateReport != null)
{
RateReport(rate);
}
在調用委托之前,必須檢查委托是否為 null,否則將有可能引發 NullReferenceException 意外;比較 VB.NET
的代碼,VB.NET 的 RaiseEvent 語句實際上也隱藏了這一細節。
好了,到此為止,Worker 類部分通過事件模型向外界發送事件通知的功能已經有了第一個版本,修改你的
Windows 窗體,給它添加 RateReport 事件處理程序(請參看你已下載的源代碼),并掛接到一起,看看現
在的效果:
添加了進度指示之后的界面,極大的改善了用戶體驗,對用戶更為友好。
[TOP]
向“.NET Framework 類庫設計指南”靠攏,標準實現
Demo 1E,C# 的標準實現。上文已經反復強調了 Demo 1C, Demo 1D 代碼不符合 CLS 約定。微軟
為 .NET 類庫的設計與命名提出了一些指南,作為一種約定,.NET 開發者應當遵守這些約定。涉及事件的部分,
請參看事件命名指南(對應的在線網頁),事件使用指南(對應的在線網頁)。
using System;
using System.Threading;
namespace percyboy.EventModelDemo.Demo1E
{
public class Worker
{
private const int MAX = 10000;
public class StartWorkEventArgs : EventArgs
{
private int totalUnits;
public int TotalUnits
{
get { return totalUnits; }
}
public StartWorkEventArgs(int totalUnits)
{
this.totalUnits = totalUnits;
}
}
public class RateReportEventArgs : EventArgs
{
private double rate;
public double Rate
{
get { return rate; }
}
public RateReportEventArgs(double rate)
{
this.rate = rate;
}
}
public delegate void StartWorkEventHandler(object sender,
StartWorkEventArgs e);
public delegate void RateReportEventHandler(object sender,
RateReportEventArgs e);
public event StartWorkEventHandler StartWork;
public event EventHandler EndWork;
public event RateReportEventHandler RateReport;
protected virtual void OnStartWork( StartWorkEventArgs e )
{
if (StartWork != null)
{
StartWork(this, e);
}
}
protected virtual void OnEndWork( EventArgs e )
{
if (EndWork != null)
{
EndWork(this, e);
}
}
protected virtual void OnRateReport( RateReportEventArgs e )
{
if (RateReport != null)
{
RateReport(this, e);
}
}
public Worker()
{
}
public void DoLongTimeTask()
{
int i;
bool t = false;
double rate;
OnStartWork(new StartWorkEventArgs(MAX) );
for (i = 0; i <= MAX; i++)
{
Thread.Sleep(1);
t = !t;
rate = (double)i / (double)MAX;
OnRateReport( new RateReportEventArgs(rate) );
}
OnEndWork( EventArgs.Empty );
}
}
}
按照 .NET Framework 類庫設計指南中的約定:
(1)事件委托名稱應以 EventHandler 為結尾;
(2)事件委托的“規格”應該是兩個參數:第一個參數是 object 類型的 sender,代表發出事件通知的對象(代
碼中一般是 this 關鍵字(VB.NET 中是 Me))。第二個參數 e,應該是 EventArgs 類型或者從 EventArgs 繼
承而來的類型;
事件參數類型,應從 EventArgs 繼承,名稱應以 EventArgs 結尾。應該將所有想通過事件、傳達到外界的信
息,放在事件參數 e 中。
(3)一般的,只要類不是密封(C# 中的 sealed,VB.NET 中的 NotInheritable)的,或者說此類可被繼承,
應該為每個事件提供一個 protected 并且是可重寫(C# 用 virtual,VB.NET 用 Overridable)的 OnXxxx
方法:該方法名稱,應該是 On 加上事件的名稱;只有一個事件參數 e;一般在該方法中進行 null 判斷,并且
把 this/Me 作為 sender 執行事件委托;在需要發出事件通知的地方,應調用此 OnXxxx 方法。
對于此類的子類,如果要改變發生此事件時的行為,應重寫 OnXxxx 方法;并且在重寫時,一般情況下應調用
基類的此方法(C# 里的 base.OnXxxx,VB.NET 用 MyBase.OnXxxx)。
我建議你能繼續花些時間研究一下這份代碼的寫法,它是 C# 的標準事件實現代碼,相信你會用得著它!
在 Demo 1D 中我沒有講解如何將事件處理程序掛接到 Worker 實例的事件的代碼,在這個 Demo 中,我將
主要的部分列在這里:
private void button1_Click(object sender, System.EventArgs e)
{
statusBar1.Text = "開始工作 ....";
this.Cursor = Cursors.WaitCursor;
long tick = DateTime.Now.Ticks;
Worker worker = new Worker();
// 將事件處理程序與 Worker 的相應事件掛鉤
// 這里我只掛鉤了 RateReport 事件做示意
worker.RateReport += new
Worker.RateReportEventHandler(this.worker_RateReport);
worker.DoLongTimeTask();
tick = DateTime.Now.Ticks - tick;
TimeSpan ts = new TimeSpan(tick);
this.Cursor = Cursors.Default;
statusBar1.Text = String.Format("任務完成,耗時 {0} 秒。",
ts.TotalSeconds);
}
private void worker_RateReport(object sender,
Worker.RateReportEventArgs e)
{
this.statusBar1.Text = String.Format("已完成 {0:P0} ....",
e.Rate);
}
請注意 C# 的掛接方式(“+=”運算符)。
到這里為此,你已經看到了事件機制的好處:Worker 類的代碼和這個 Windows Form 沒有依賴關系。Worker
類可以單獨存在,可以被重復應用到不同的地方。
VB.NET 的讀者,請查看 Demo 1F 中的 VB.NET 標準事件寫法,并參考這里的說明,我就不再贅述了。
[TOP]
2005 年1 月22 日 18:27 - (閱讀:11953;評論:44)
評論
# RE: .NET 事件模型教程(一)
2005-1-22 22:10 | 開心就好
有沒有想法到MSDN Webcast 來講一次網絡講座,把你的這些心得傳播給更多的朋友呢?如果有想法的
話,請將你的文章,整理成一個PPT,并且做一個十分鐘左右的錄音文件,發送到
msdnprc^_^microsoft.com(^_^變為@)。或者發給立楠也可以。
# RE: .NET 事件模型教程(一)
2005-1-24 11:43 | BESTSKY
受益非淺,我以前從沒有這樣寫過謝謝.
# RE: .NET 事件模型教程(一)
2005-1-25 14:59 | TRULY
Good
# RE: .NET 事件模型教程(一)
2005-1-27 17:10 | MOUSEINDARK
小弟愚笨,折騰了一個多小時才算是理解了
現在用.net 就感覺越學越是不懂,今天算是又張了見識了
# RE: .NET 事件模型教程(一)
2005-2-2 17:32 | 生如夏花
前兩天面試時碰到過一道關于事件代理機制的題,今天認識的更清楚了。
# .NET 事件模型教程
2005-2-4 11:34 | MOREPOWER
Ping Back 來自:blog.csdn.net
# RE: .NET 事件模型教程(一)
2005-3-7 15:27 | JUNO MAY
真的很感謝你寫的這幾篇文章,讓我對event driven 這些很模糊的概念變得清晰起來。
我是個網站設計師,喜歡用php 和function-oriented 的方法寫應用程序, 對M$的東西都很感冒不太喜
歡,但是.NET 的一些概念和方法確實對開發帶來便利,盡管龐大的framework 和 class 使得程序變得
臃腫緩慢,但是大大加速開發進程和便于維護。
現在我們在用Prado (一個借鑒了asp.net 大部分思想的php5 framework)開發應用程序,它山之石可
以攻玉 :)
# RE: .NET 事件模型教程(一)
2005-3-17 16:44 | 沖浪
文章很不錯,有沒有想過出書哦....
# RE: .NET 事件模型教程(一)
2005-3-21 1:25 | LIANGYJ
在《.net 框架程序設計》中,說到的“回調方法的原形應該有一個void 返回值,并且接受兩個參數,第一
個參數為object 類型,其指向發送通知的對象,第二個參數為一個繼承自EventArgs 的類型,其中包含
所有通知接受者需要的附加信息”
而你在這里定義的event 并沒有符合這兩個規則,那么究竟是你錯?還是那本書的錯呢?還是有另外的解
釋呢?請指點一下。
# RE: .NET 事件模型教程(一)
2005-3-21 9:18 | 破寶
to Liangyj:
我相信如果你讀完這第一篇教程全文的話,就不會認為我寫的和你那本書有矛盾。你再看看最后一個小節
“向“.NET Framework 類庫設計指南”靠攏,標準實現”里的內容?
# RE: .NET 事件模型教程(一)
2005-4-1 4:01 | JAYE
xiexie 破寶,收藏一下不介意吧
# RE: .NET 事件模型教程(一)
2005-4-9 21:29 | CQHYDZ
我是看msdn 中自定義控件哪個錄像知道事件模型的,你寫的不錯
# RE: .NET 事件模型教程(一)
2005-4-12 17:55 | JERRY
好文啊!
我事件這一張翻來復去看了幾遍都沒看明白。聽你這么一講解,思路清晰了很多,多謝啊!
# RE: .NET 事件模型教程(一)
2005-4-21 12:33 | 涼
寫的真好,之前看的msdn,一點也沒看懂,現在總算有點明白了。
# RE: .NET 事件模型教程(一)
2005-4-28 20:03 | DUR
寫得很棒。:)
偶有一問。在你的Worker.DoLongTimeTask 中寫了OnStartWork 來讓Worker 對象發出一個事件。
那么Form 對象是怎么捕捉鼠標點了一下的呢?
如果想讓某接口在收到一脈沖時產生一個事件,該怎么辦呢?
# RE: .NET 事件模型教程(一)
2005-5-9 3:08 | MYASPX
看了此文,對事件有了更深的了解!
# RE: .NET 事件模型教程(一)
2005-5-12 19:49 | ERICZQWANG
對那個掛鉤RateReport 事件沒有完全理解。EndReport 和 StartReport 沒有掛鉤,是不是就不執行了
呢?
// 這里我只掛鉤了 RateReport 事件做示意
worker.RateReport += new Worker.RateReportEventHandler(this.worker_RateReport);
# RE: .NET 事件模型教程(一)
2005-5-12 20:02 | 破寶
to EricZQWang:
不知道你所謂“不執行”是指“誰”不執行?
正如文中一開始就說,事件是一個對象發出的消息,到了那個時候它就要發消息,無論是否有人注意這個
消息。
如果一開始我們對某個事件掛接了一個handler,則在這個事件發生時,handler 被執行。如果不掛鉤,
handler 不會執行。
# RE: .NET 事件模型教程(一)
2005-5-13 8:53 | ERICZQWANG
謝謝破寶。 我Share 一下自己新的理解:“
// 這里掛鉤了 RateReport 事件做示意
worker.RateReport += new Worker.RateReportEventHandler(this.worker_RateReport); ”
他的作用就是使RateReport != null.在method:
worker.DoLongTimeTask()中,Worker 總共發出了三個消息
OnStartWork,OnRateReport,OnEndReport
在method: button1_Click()中掛鉤了OnRateReport 消息,我覺得作用就是實例化了
Worker.RateReport,使RateReport != null,OnStartReport 和OnEndReport 沒有掛鉤,StartWork
and EndStart ==null.
當Worker 發出OnRateReport 的消息時,會執行
this.statusBar1.Text = String.Format("已完成 {0:P0} ....", e.Rate);
但是:當Worker 發出OnStartWork 和OnEndReport 消息時,
因為StartWork 和EndReport==null,所以在Worker 的Method:OnStartReport 和OnEndReport
中什么都沒有作
滿分100 的話,這個理解可以打多少分?:)
# RE: .NET 事件模型教程(一)
2005-5-13 10:55 | 破寶
應該說從一個面向過程的觀點來看,你的理解基本上沒什么問題(當然指出一點:“消息”是指
StartWork,RateRepport,EndWork,而不是OnXxxx,后者只是worker 的protected 方法)。
但希望你能夠上升到面向對象的角度再來領悟這個問題。祝你好運!
# RE: .NET 事件模型教程(一)
2005-5-13 22:57 | GAOFAN
代碼下不了啊,能不能給我發一份
[email protected]
謝謝先
# RE: .NET 事件模型教程(一)
2005-5-28 20:26 | LHQ
雖然我還是個新手.
不過還是覺得應該更正一下sender 和e 不是類型而是對象
否則你怎么進行轉換呢?
類本事是沒有轉換這個概念的
有了繼承才有了轉換這個概念
而轉換本身又不是針對類的
因為C#是一門面向對象的設計語言
# RE: .NET 事件模型教程(一)
2005-5-29 2:13 | 破寶
to lhq:
不知道是哪句話中的措辭不嚴密,敬請指出。
按照.net 編碼指南,事件處理程序的兩個參數:sender 參數的類型應為object 類型,e 參數的類型應為
EventArgs 類型或者EventArgs 類型的子類。
# RE: .NET 事件模型教程(一)
2005-5-31 11:32 | LHQ
(2)事件委托的“規格”應該是兩個參數:第一個參數是 object 類型的 sender,代表發出事件通知的對象
(代碼中一般是 this 關鍵字(VB.NET 中是 Me))。第二個參數 e,應該是 EventArgs 類型或者從
EventArgs 繼承而來的類型;
在最后幾句里,第一句我沒仔細看現在再看這句沒有問題
不過第二個的e 還有點問題 我認為e 應該是EventArgs 類型或者從 EventArgs 繼承而來的類型的對象
好比
class A{}
A a;
那A 是類型,a 就是對象
e 就是System.EventArgs 以及它所派生的類的對象
以上屬個人意見,如有不當請指出
# RE: .NET 事件模型教程(一)
2005-5-31 17:24 | 破寶
to lhq:
多謝指正,純屬文法上的考慮不周。
# RE: .NET 事件模型教程(一)
2005-6-2 11:52 | JERRY
弱弱的問題:
在接收到事件后,我用Label 來顯示進度總是沒辦法顯示,只有在事件完成后顯示出一個100%,而沒辦
法在運行事件過程中顯示現在運行到百分之多少了,用了StatusBar 沒這個問題。
StatusBar 的Text 和Label 的Text 屬性不一樣嗎?
# RE: .NET 事件模型教程(一)
2005-6-2 13:53 | JERRY
另外,我用progressbar 也不能正確顯示當前進度。
好象正在運行的任務也會使窗體陷入無法響應的狀態。
有沒有辦法使進程在運行,而窗體是有響應的:可以拖拽,可以最小話、最大化?
# RE: .NET 事件模型教程(一)
2005-7-21 18:32 | LOVALING
看完了,總的來說感覺C#的看起來最順眼,個人觀點,呵呵,
向“.NET Framework 類庫設計指南”靠攏,標準實現 這一節我不是十分認同這樣的做法(雖然是設計指
南),本來一個簡單的事情被搞得復雜了。第一個sender 參數是必要的,使用過委托的都會有這樣的需要,
完全解除了設計上的藕合,結果丟失了一些參數,不得不以這樣的方式來補償。
沒有必要把一個事件的參數包裝到一個類里面,也許這樣看起來所有的事件都有一樣的外觀,但我認為不
如直接傳遞參數來得方便。如下:
namespace percyboy.EventModelDemo.Demo1E
{
public class Worker
{
private const int MAX = 10000;
public delegate void StartWorkEventHandler(object sender, int totalUnits);
public delegate void RateReportEventHandler(object sender, double rate);
public delegate void EndWorkEventHandler(object sender);
public event StartWorkEventHandler StartWork;
public event EndWorkEventHandler EndWork;
public event RateReportEventHandler RateReport;
public Worker()
{
}
public void DoLongTimeTask()
{
int i;
bool t = false;
double rate;
StartWork(this, MAX);
for (i = 0; i <= MAX; i++)
{
Thread.Sleep(1);
t = !t;
rate = (double)i / (double)MAX;
RateReport(this, rate);
}
EndWork(this);
}
}
}
sender 可以保留。不過派生一個類我覺得是一種過時的老的作法,既然有簡單的,為何還要用那么復雜
的呢,.net 里面這樣也保證不會有問題。
可能是想通過一個類型來增強靜態編譯時的檢錯吧,防止有相同參數類型的事件接口綁定錯了,猜想。
# RE: .NET 事件模型教程(一)
2005-7-21 20:58 | 鐮村疂
to lovaling:
我的觀點是“入鄉隨俗”。
很多從 Java 轉 C# 的人在書寫代碼時,把類、方法、屬性等等的命名,按照 Java 的命名規則去做,
這樣做雖然沒什么壞處,但看起來總是感覺不倫不類的。所以我的觀點是“入鄉隨俗”,做 .NET 就按微軟
給大家的約定,沒什么不好;做 Java 就看 Sun 的編碼規范。這樣做出來的東西,感覺才是那個“味道”。
另外,關于事件參數都從 EventArgs 繼承這一點,我認為是有好處的,不過好處不是定義事件的一方,
而是調用事件的一方。
比如,我們在 VS.NET 中寫代碼時,如果是某事件handler 的代碼,你可以直接寫 e ,然后一“點”就把
所有跟此事件相關的參數成員點出來了,使用起來還是很方便。
我們在使用微軟提供的標準類庫時,形成了這樣的習慣,那么在定義自己的事件時,沒必要一定自創一套。
標準類庫的事件遵循一套標準,你自定義的遵循另一套標準,這樣感覺還是會帶來一定程度的混亂和迷惑。
# RE: .NET 事件模型教程(一)
2005-9-22 15:32 | PUBLIC
在 委托(delegate)簡介 一節中 “順帶地,既然完全面向對象,其實委托本身也是一種對象。” 一句話
題出疑義,我好像記得: 委托(delegate)是一種類(class)。
# RE: .NET 事件模型教程(一)
2005-11-5 17:07 | SUNW
感謝樓上的,終于弄懂了C#里面的事件代理.
# RE: .NET 事件模型教程(一)
2006-2-13 14:26 | YAO
好文章.以前不清楚的概念現在清楚了.
# RE: .NET 事件模型教程(一)
2006-3-7 19:22 | WQXH
確實不錯,以前比較模糊的概念現在越來越清楚了.
希望破寶寫出更加好的文章.
# RE: .NET 事件模型教程(一)
2006-7-2 12:59 | 野風
不錯,很喜歡你的文章,有獨道的見解...
# .NET 事件模型教程(一)
2006-8-29 11:52 | AFTER_
.NET 事件模型教程(一)
目錄
事件、事件處理程序概念
問題描述:一個需要較長時間才能完成的任務
高耦合的實現
事件模型的解決方案,簡單易懂的 VB.NET 版本
委托(delegate)簡介
C# 實現
向“.NET Framework 類庫設計指南”靠攏,標準實現
事件、事件處理程序概念
# RE: .NET 事件模型教程(一)
2006-9-8 8:30 | 蛋蛋
太好了!佩服
# RE: .NET 事件模型教程(一)
2006-11-26 8:45 | ZHANG
public class Worker
{
private const int MAX = 10000;
public class StartWorkEventArgs : EventArgs
{
private int totalUnits;
public int TotalUnits
{
get { return totalUnits; }
}
public StartWorkEventArgs(int totalUnits)
{
this.totalUnits = totalUnits;
}
}
public class RateReportEventArgs : EventArgs
{
private double rate;
public double Rate
{
get { return rate; }
}
public RateReportEventArgs(double rate)
{
this.rate = rate;
}
}
感覺public class StartWorkEventArgs : EventArgs 和public class RateReportEventArgs :
EventArgs
被嵌入public class Worker 類內部了,是不是應該移出來,結構更清楚些。
# 回復: .NET 事件模型教程(一)
2006-12-27 17:51 | 我考百試通
.NET 事件模型和 Java 事件模型的對比
# 轉載:.NET 事件模型教程(一)
2007-2-2 11:18 | 狂風
源文來源:http://blog.joycode.com/percyboy/archive/2005/01/22/43433.aspx.NET 事件模型
教程(一) 目錄 事件、事件...
# FJTREQJJ
2007-2-9 21:39 | FJTREQJJ
<a href="http://bvxaupgt.com">jylxsuia</a> btxcyibl http://alnqrxwg.com/ jsyeamnk
hlqpsxzx [URL=http://tbrbhpom.com/]jewjulvr[/URL]
# 回復: .NET 事件模型教程(一)
2007-2-13 20:39 | LIUYUANBO
非常佩服!寫的太好了
# 回復: .NET 事件模型教程(一)
2007-4-22 2:42 | 無情人
DD
# 回復: .NET 事件模型教程(一)
2007-4-25 15:09 | X2
受益匪淺!感動~~
# 回復: .NET 事件模型教程(一)
2007-4-25 15:10 | X2
受益匪淺!
強烈的感謝作者!
感動~~
.NET 事件模型教程(二)
目錄
? 屬性樣式的事件聲明
? 單播事件和多播事件
? 支持多播事件的改進
屬性樣式的事件聲明
在第一節中,我們討論了 .NET 事件模型的基本實現方式。這一部分我們將學習 C# 語言提供的高級實現方式:
使用 add/remove 訪問器聲明事件。(注:本節內容不適用于 VB.NET。)
我們再來看看上一節中我們聲明事件的格式:
public event [委托類型] [事件名稱];
這種聲明方法,類似于類中的字段(field)。無論是否有事件處理程序掛接,它都會占用一定的內存空間。一般
情況中,這樣的內存消耗或許是微不足道的;然而,還是有些時候,內存開銷會變得不可接受。比如,類似
System.Windows.Forms.Control 類型具有五六十個事件,這些事件并非每次都會掛接事件處理程序,如果
每次都無端的多處這么多的內存開銷,可能就無法容忍了。
好在 C# 語言提供了“屬性”樣式的事件聲明方式:
public event [委托類型] [事件名稱]
{
add { .... }
remove { .... }
}
如上的格式聲明事件,具有 add 和 remove 訪問器,看起來就像屬性聲明中的 get 和 set 訪問器。使用特
定的存儲方式(比如使用 Hashtable 等集合結構),通過 add 和 remove 訪問器,自定義你自己的事件處
理程序添加和移除的實現方法。
Demo 1G:“屬性”樣式的事件聲明。我首先給出一種實現方案如下(此實現參考了 .NET Framework SDK 文
檔中的一些提示)(限于篇幅,我只將主要的部分貼在這里):
public delegate void StartWorkEventHandler(object sender,
StartWorkEventArgs e);
public delegate void RateReportEventHandler(object sender,
RateReportEventArgs e);
// 注意:本例中的實現,僅支持“單播事件”。
// 如需要“多播事件”支持,請參考 Demo 1H 的實現。
// 為每種事件生成一個唯一的 object 作為鍵
static readonly object StartWorkEventKey = new object();
static readonly object EndWorkEventKey = new object();
static readonly object RateReportEventKey = new object();
// 使用 Hashtable 存儲事件處理程序
private Hashtable handlers = new Hashtable();
// 使用 protected 方法而沒有直接將 handlers.Add /
handlers.Remove
// 寫入事件 add / remove 訪問器,是因為:
// 如果 Worker 具有子類的話,
// 我們不希望子類可以直接訪問、修改 handlers 這個 Hashtable。
// 并且,子類如果有其他的事件定義,
// 也可以使用基類的這幾個方法方便的增減事件處理程序。
protected void AddEventHandler(object eventKey, Delegate
handler)
{
lock(this)
{
if (handlers[ eventKey ] == null)
{
handlers.Add( eventKey, handler );
}
else
{
handlers[ eventKey ] = handler;
}
}
}
protected void RemoveEventHandler(object eventKey)
{
lock(this)
{
handlers.Remove( eventKey );
}
}
protected Delegate GetEventHandler(object eventKey)
{
return (Delegate) handlers[ eventKey ];
}
// 使用了 add 和 remove 訪問器的事件聲明
public event StartWorkEventHandler StartWork
{
add { AddEventHandler(StartWorkEventKey, value); }
remove { RemoveEventHandler(StartWorkEventKey); }
}
public event EventHandler EndWork
{
add { AddEventHandler(EndWorkEventKey, value); }
remove { RemoveEventHandler(EndWorkEventKey); }
}
public event RateReportEventHandler RateReport
{
add { AddEventHandler(RateReportEventKey, value); }
remove { RemoveEventHandler(RateReportEventKey); }
}
// 此處需要做些相應調整
protected virtual void OnStartWork( StartWorkEventArgs e )
{
StartWorkEventHandler handler =
(StartWorkEventHandler)
GetEventHandler( StartWorkEventKey );
if (handler != null)
{
handler(this, e);
}
}
protected virtual void OnEndWork( EventArgs e )
{
EventHandler handler =
(EventHandler) GetEventHandler( EndWorkEventKey );
if (handler != null)
{
handler(this, e);
}
}
protected virtual void OnRateReport( RateReportEventArgs e )
{
RateReportEventHandler handler =
(RateReportEventHandler)
GetEventHandler( RateReportEventKey );
if (handler != null)
{
handler(this, e);
}
}
public Worker()
{
}
public void DoLongTimeTask()
{
int i;
bool t = false;
double rate;
OnStartWork(new StartWorkEventArgs(MAX) );
for (i = 0; i <= MAX; i++)
{
Thread.Sleep(1);
t = !t;
rate = (double)i / (double)MAX;
OnRateReport( new RateReportEventArgs(rate) );
}
OnEndWork( EventArgs.Empty );
}
細細研讀這段代碼,不難理解它的算法。這里,使用了名為 handlers 的 Hashtable 存儲外部掛接上的事件處
理程序。每當事件處理程序被“add”,就把它加入到 handlers 里存儲;相反 remove 時,就將它從 handlers
里移除。這里取 event 的 key (開始部分為每一種 event 都生成了一個 object 作為代表這種 event 的
key)作為 Hashtable 的鍵。
[TOP]
單播事件和多播事件
在 Demo 1G 給出的解決方案中,你或許已經注意到:如果某一事件被掛接多次,則后掛接的事件處理程序,
將改寫先掛接的事件處理程序。這里就涉及到一個概念,叫“單播事件”。
所謂單播事件,就是對象(類)發出的事件通知,只能被外界的某一個事件處理程序處理,而不能被多個事件處
理程序處理。也就是說,此事件只能被掛接一次,它只能“傳播”到一個地方。相對的,就有“多播事件”,對象(類)
發出的事件通知,可以同時被外界不同的事件處理程序處理。
打個比方,上一節開頭時張三大叫一聲之后,既招來了救護車,也招來了警察叔叔(問他是不是回不了家了),
或許還有電視轉播車(現場直播、采訪張三為什么大叫,呵呵)。
多播事件會有很多特殊的用法。如果以后有機會向大家介紹 Observer 模式,可以看看 Observer 模式中是怎
么運用多播事件的。(注:經我初步測試,字段形式的事件聲明,默認是支持“多播事件”的。所以如果在事件種
類不多時,我建議你采用上一節中所講的字段形式的聲明方式。)
[TOP]
支持多播事件的改進
Demo1H,支持多播事件。為了支持多播事件,我們需要改進存儲結構,請參考下面的算法:
public delegate void StartWorkEventHandler(object sender,
StartWorkEventArgs e);
public delegate void RateReportEventHandler(object sender,
RateReportEventArgs e);
// 為每種事件生成一個唯一的鍵
static readonly object StartWorkEventKey = new object();
static readonly object EndWorkEventKey = new object();
static readonly object RateReportEventKey = new object();
// 為外部掛接的每一個事件處理程序,生成一個唯一的鍵
private object EventHandlerKey
{
get { return new object(); }
}
// 對比 Demo 1G,
// 為了支持“多播”,
// 這里使用兩個 Hashtable:一個記錄 handlers,
// 另一個記錄這些 handler 分別對應的 event 類型(event 的類型
用各自不同的 eventKey 來表示)。
// 兩個 Hashtable 都使用 handlerKey 作為鍵。
// 使用 Hashtable 存儲事件處理程序
private Hashtable handlers = new Hashtable();
// 另一個 Hashtable 存儲這些 handler 對應的事件類型
private Hashtable events = new Hashtable();
protected void AddEventHandler(object eventKey, Delegate
handler)
{
// 注意添加時,首先取了一個 object 作為 handler 的 key,
// 并分別作為兩個 Hashtable 的鍵。
lock(this)
{
object handlerKey = EventHandlerKey;
handlers.Add( handlerKey, handler );
events.Add( handlerKey, eventKey);
}
}
protected void RemoveEventHandler(object eventKey, Delegate
handler)
{
// 移除時,遍歷 events,對每一個符合 eventKey 的項,
// 分別檢查其在 handlers 中的對應項,
// 如果兩者都吻合,同時移除 events 和 handlers 中的對應項。
//
// 或許還有更簡單的算法,不過我一時想不出來了 :(
lock(this)
{
foreach ( object handlerKey in events.Keys)
{
if (events[ handlerKey ] == eventKey)
{
if ( (Delegate)handlers[ handlerKey ] ==
handler )
{
handlers.Remove( handlers[ handlerKey ] );
events.Remove( events[ handlerKey ] );
break;
}
}
}
}
}
protected ArrayList GetEventHandlers(object eventKey)
{
ArrayList t = new ArrayList();
lock(this)
{
foreach ( object handlerKey in events.Keys )
{
if ( events[ handlerKey ] == eventKey)
{
t.Add( handlers[ handlerKey ] );
}
}
}
return t;
}
// 使用了 add 和 remove 訪問器的事件聲明
public event StartWorkEventHandler StartWork
{
add { AddEventHandler(StartWorkEventKey, value); }
remove { RemoveEventHandler(StartWorkEventKey, value); }
}
public event EventHandler EndWork
{
add { AddEventHandler(EndWorkEventKey, value); }
remove { RemoveEventHandler(EndWorkEventKey, value); }
}
public event RateReportEventHandler RateReport
{
add { AddEventHandler(RateReportEventKey, value); }
remove { RemoveEventHandler(RateReportEventKey, value); }
}
// 此處需要做些相應調整
protected virtual void OnStartWork( StartWorkEventArgs e )
{
ArrayList handlers = GetEventHandlers( StartWorkEventKey );
foreach(StartWorkEventHandler handler in handlers)
{
handler(this, e);
}
}
protected virtual void OnEndWork( EventArgs e )
{
ArrayList handlers = GetEventHandlers( EndWorkEventKey );
foreach(EventHandler handler in handlers)
{
handler(this, e);
}
}
protected virtual void OnRateReport( RateReportEventArgs e )
{
ArrayList handlers =
GetEventHandlers( RateReportEventKey );
foreach(RateReportEventHandler handler in handlers)
{
handler(this, e);
}
}
上面給出的算法,只是給你做參考,應該還有比這個實現更簡單、更高效的方式。
為了實現“多播事件”,這次使用了兩個 Hashtable:一個存儲“handlerKey - handler”對,一個存儲
“handlerKey - eventKey”對。相信通過仔細研讀,你可以讀懂這段代碼。我就不再贅述了。
[TOP]
2005 年1 月22 日 18:35 - (閱讀:6119;評論:13)
評論
# RE: .NET 事件模型教程(二)
2005-1-25 21:51 | HOO
good
# 對多播事件的一點意見。
2005-1-31 11:25 | WANG_SOLARIS
看了一下對多播事件的處理方式,總體思路值得肯定,但在此處對用Hashtable 來存儲鍵值對覺得有些不
妥。
一般按照傳統采用非靜態成員來標識事件類型的方式,當在客戶端為一個事件預定多個事件處理函數的時
候,是按照隊列的方式來處理的(即先進先出原則)。
而在你的代碼中采用了Hashtable 就破壞了這個原則,因為對Hashtable 的遍歷并不是按照插入時的順
序進行的(見上面對events 的遍歷)。所以我建議換成其它支持按插入時順序進行遍歷的集合類型,比如
ListDictionary 是個選擇,不過當事件很多而對性能要求又很高時,需考慮其它實現。(當然上面程序中的
handlers 仍然可以使用Hashtable)
# RE: .NET 事件模型教程(二)
2005-1-31 11:51 | 破寶
謝謝 wang_solaris 的建議!
我正在準備重寫這一部分,因為已經有人給我指出了不確切的地方:
其實委托可以是多路的
這樣的話,就沒有必要分別存儲多個委托,
而可以直接掛接在同一個委托實例上。
就是說,委托的一個實例可以同時掛接多個函數,
委托是具有 +=,-= 運算符的。
這一點,我寫文章時不了解,給大家介紹的方法其實走了彎路。
所以我正在準備重寫這一部分,暫時因為年關太忙無法馬上動筆,請諸位見諒!
# .NET 事件模型教程
2005-2-4 11:34 | MOREPOWER
Ping Back 來自:blog.csdn.net
# RE: .NET 事件模型教程(二)
2005-5-11 14:37 | GAOFAN
代碼下不了啊,誰有能否給我一份,感激不盡。。
[email protected]
# RE: .NET 事件模型教程(二)
2005-8-12 15:35 | AYONGWUST
A 對象能不能偽裝B 對象發出B 對象的事件通知。
# .NET 事件模型教程(二)
2006-8-30 15:53 | JELINK
framework
# 回復: .NET 事件模型教程(二)
2006-12-27 17:13 | 我考百試通
.NET 事件模型和 Java 事件模型的對比
# 回復: .NET 事件模型教程(二)
2006-12-27 17:50 | 我考百試通
.NET 事件模型和 Java 事件模型的對比
# 回復: .NET 事件模型教程(二)
2007-5-10 10:55 | 座看云起
我的理解,未經證實:
掛接事件時將運算符由"+="改為"=",事件應該就由多播變成單播了。
也就是說.NET 代理機制本身是以某種方式實現了一個隊列。
# 回復: .NET 事件模型教程(二)
2007-5-10 12:11 | 坐看云起
剛做了實驗,掛接事件時使用"="是不允許的。呵呵。
# .NET技術-.NET理論資料-.NET理論資料
2007-7-4 14:00 | JASONLI
綜合:http://210.27.12.83/jingpin_mms.asp
# 回復: .NET 事件模型教程(二)
2007-7-11 13:33 | ILEX
謝謝,怎么一直未見你的改進版呢
.NET 事件模型教程(三)
通過前兩節的學習,你已經掌握了 .NET 事件模型的原理和實現方式。這一節我將介紹兩個替代方案,這些方
案并不是推薦采用的,請盡量采用事件模型去實現。另外,在本節末尾,有一段適合熟悉 Java 語言的讀者閱讀,
討論了 .NET 和 Java 在“事件模型”方面的差異。
目錄
? 使用接口實現回調
? .NET 事件模型和 Java 事件模型的對比
使用接口實現回調
事件模型其實是回調函數的一種特例。像前面的例子,Form1 調用了 Worker,Worker 反過來(通過事件模
型)讓 Form1 改變了狀態欄的信息。這個操作就屬于回調的一種。
在“.NET Framework 類庫設計指南”中提到了:“委托、接口和事件允許提供回調功能。每個類型都有自己特定
的使用特性,使其更適合特定的情況。”(參見本地 SDK 版本,在線 MSDN 版本)
事件模型中,事實上也應用了委托來實現回調,可以說,事件模型是委托回調的一個特例。如果有機會,我會在
關于多線程的教程中介紹委托回調在多線程中的應用。
這里我先來看看,如何使用接口實現回調功能,以達到前面事件模型實現的效果。
Demo 1I:使用接口實現回調。
using System;
using System.Threading;
using System.Collections;
namespace percyboy.EventModelDemo.Demo1I
{
// 注意這個接口
public interface IWorkerReport
{
void OnStartWork(int totalUnits);
void OnEndWork();
void OnRateReport(double rate);
}
public class Worker
{
private const int MAX = Consts.MAX;
private IWorkerReport report = null;
public Worker()
{
}
// 初始化時同時指定 IWorkerReport
public Worker(IWorkerReport report)
{
this.report = report;
}
// 或者初始化后,通過設置此屬性指定
public IWorkerReport Report
{
set { report = value; }
}
public void DoLongTimeTask()
{
int i;
bool t = false;
double rate;
if (report != null)
{
report.OnStartWork( MAX );
}
for (i = 0; i <= MAX; i++)
{
Thread.Sleep(1);
t = !t;
rate = (double)i / (double)MAX;
if (report != null)
{
report.OnRateReport( rate );
}
}
if ( report != null)
{
report.OnEndWork();
}
}
}
}
你可以運行編譯好的示例,它可以完成和前面介紹的事件模型一樣的工作,并保證了耦合度沒有增加。調用
Worker 的 Form1 需要做一個 IWorkerReport 的實現:
private void button1_Click(object sender, System.EventArgs e)
{
statusBar1.Text = "開始工作 ....";
this.Cursor = Cursors.WaitCursor;
long tick = DateTime.Now.Ticks;
Worker worker = new Worker();
// 指定 IWorkerReport
worker.Report = new MyWorkerReport(this);
worker.DoLongTimeTask();
tick = DateTime.Now.Ticks - tick;
TimeSpan ts = new TimeSpan(tick);
this.Cursor = Cursors.Default;
statusBar1.Text = String.Format("任務完成,耗時 {0} 秒。",
ts.TotalSeconds);
}
// 這里實現 IWorkerReport
private class MyWorkerReport : IWorkerReport
{
public void OnStartWork(int totalUnits)
{
}
public void OnEndWork()
{
}
public void OnRateReport(double rate)
{
parent.statusBar1.Text = String.Format("已完成
{0:P0} ....", rate);
}
private Form1 parent;
public MyWorkerReport(Form1 form)
{
this.parent = form;
}
}
你或許已經覺得這種實現方式,雖然 Worker 類“里面”可能少了一些代碼,卻在調用時增加了很多代碼量。從
重復使用的角度來看,事件模型顯然要更方便調用。另外,從面向對象的角度,我覺得理解了事件模型的原理之
后,你會覺得“事件”會更親切一些。
另外,IWorkerReport 中包含多個方法,而大多時候我們并不是每個方法都需要,就像上面的例子中那樣,
OnStartWork 和 OnEndWork 這兩個都是空白。如果接口中的方法很多,也會給調用方增加更多的代碼量。
下載的源代碼中還包括一個 Demo 1J,它和 Worker 類一起,提供了一個 IWorkerReport 的默認實現
WorkerReportAdapter(每個方法都是空白)。這樣,調用方只需要從 WorkerReportAdapter 繼承,重寫
其中需要重寫的方法,這樣會減少一部分代碼量。但我覺得仍然是很多。
注意,上述的代碼,套用(僅僅是套用,因為它不是事件模型)“單播事件”和“多播事件”的概念來說,它只能支
持“單播事件”。如果你想支持“多播事件”,我想你可以考慮加入 AddWorkerReport 和
RemoveWorkerReport 方法,并使用 Hashtable 等數據結構,存儲每一個加入的 IWorkerReport。
[TOP]
.NET 事件模型和 Java 事件模型的對比
(我對 Java 語言的了解不是很多,如果有誤,歡迎指正!)
.NET 的事件模型,對于 C#/VB.NET 兩種主流語言來說,是在語言層次上實現的。C# 提供了 event 關鍵
字,VB.NET 提供了 Event,RaiseEvent 關鍵字。像前面兩節所講的那樣,它們都有各自的聲明事件成員的
語法。而 Java 語言本身是沒有“事件”這一概念的。
從面向對象理論來看,.NET 的一個類(或類的實例:對象),可以擁有:字段、屬性、方法、事件、構造函數、
析構函數、運算符等成員類型。在 Java 中,類只有:字段、方法、構造函數、析構函數、運算符。Java 的類
中沒有屬性和事件的概念。(雖然 Java Bean 中將 getWidth、setWidth 的兩個方法,間接的轉換為一個
Width 屬性,但 Java 依然沒有把“屬性”作為一個語言層次的概念提出。)總之,在語言層次上,Java 不支持
事件。
Java Swing 是 Java 世界中常用的制作 Windows 窗體程序的一套 API。在 Java Swing 中有一套事件模
型,來讓它的控件(比如 Button 等)擁有事件機制。
Swing 事件模型,有些類似于本節中介紹的接口機制。它使用的接口,諸如 ActionListener、KeyListener、
MouseListener(注意:按照 Java 的命名習慣,接口命名不用前綴 I)等;它同時也提供一些接口的默認實
現,如 KeyAdapter,MouseAdapter 等,使用方法大概和本節介紹的類似,它使用的是
addActionListener/removeActionListener,addKeyListener/removeKeyListener,
addMouseListener/removeMouseListener 等方法,來增減這些接口的。
正像本節的例子那樣,使用接口機制的 Swing 事件模型,需要書寫很多的代碼去實現接口或者重寫 Adapter。
而相比之下,.NET 事件模型則顯得更為輕量級,所需的掛接代碼僅一行足矣。
另一方面,我們看到 Swing 的命名方式,將這些接口都命名為 Listener,監聽器;而相比之下,.NET 事件
模型中,對事件的處理被稱為 handler,事件處理程序。一個采用“監聽”,一個是“處理”,我認為這體現了一種
思維上的差異。
還拿張三大叫的例子來講,“處理”模型是說:當張三大叫事件發生時,外界對它做出處理動作(handle this
event);監聽,則是外界一直“監聽”著張三的一舉一動(listening),一旦張三大叫,監聽器就被觸發。處理
模型是以張三為中心的思維,監聽模型則是以外部環境為中心的思維。
[TOP]
2005 年1 月22 日 18:37 - (閱讀:4524;評論:12)
評論
# RE: .NET 事件模型教程(三)
2005-1-25 21:52 | HOO
不錯,收藏先
# .NET 事件模型教程
2005-2-4 11:34 | MOREPOWER
Ping Back 來自:blog.csdn.net
# RE: .NET 事件模型教程(三)
2005-4-22 16:15 | AA
寫得不錯
# RE: .NET 事件模型教程(三)
2005-7-20 13:26 | COOLLOVE
如果我在PicBox 上繪幾個矩形如何用自定義事件(例如在mousedown 事件中)讓它們自己區分,并且
回應??
估計沒有人試過。
# RE: .NET 事件模型教程(三)
2006-6-19 10:23 | AA
very good!thanks a lot!
# RE: .NET 事件模型教程(三)
2006-6-30 10:30 | CREEKSUN
寫的很好阿,就是看不懂.我想問您:我想在asp.net 網頁中調用一個別人編寫的成熟的水質模型,該如何實
現?求救!
# .NET 事件模型教程(三)
2006-8-30 15:54 | JELINK
framework
# 回復: .NET 事件模型教程(三)
2006-12-27 17:13 | 我考百試通
.NET 事件模型和 Java 事件模型的對比
# 回復: .NET 事件模型教程(三)
2007-1-12 13:33 | MIKE_D
最近正好在做一個項目
用C#,但是對C#不是很熟悉,尤其是事件模型,怎么也不懂
今天看了覺得相當不錯
現在就在我的項目中實驗呢
# 回復: .NET 事件模型教程(三)
2007-3-15 17:27 | 飛揚跋扈
好文!!!
# 回復: .NET 事件模型教程(三)
2007-3-15 17:27 | 飛揚跋扈
好文!!!好文!!!好文!!!好文!!!好文!!!
.NET 2.0 中真正的多線程實例
Real Multi-threading in .NET 2.0
remex1980 翻譯于 2007-5-16 20:43:21
原作者: Jose Luis Latorre
原文地址: http://www.codeproject.com/useritems/RealMultiThreading.asp
多線程實例源代碼(保留原文處鏈接)
http://www.codeproject.com/useritems/RealMultiThreading/RealMultiThreading_src.zip
簡介
多線程總是那么讓人振奮。大家都希望能夠同時處理很多事情,不過如果我們沒有正確的硬
件的話,我們很難達到這點。到目前為止,我們所做的只是分開CPU 使用較多的工作,使
其為后臺進程,這樣可以使得界面上不被阻塞。
不過我希望能夠得到更好的效果,并充分利用當前最新的多CPU 效能。因此,我將寫一個
真正的多線程實例,將會有多個線程作為后臺線程在運行。
這就是這篇文章將要寫的,不得不說的是,最終的結果實在是讓我很激動。希望你也能夠發
覺它的用處。
在有4 個CPU 的多CPU 服務器上,我得到了280%的效果(測試的是CPU 型的任務),
在一些非CPU 占用較多的任務中,它可以提高到500% 到1000%的性能。
背景
網上也有不少介紹.Net 2.0 下的多線程的文章,應該說,我從它們中受益頗多。我正在使用
的是BackgroundWorker .Net 2.0 組件(不過也有實現在.net 1.1 下的代碼)。
這里,我列出一些有用的文章鏈接:
來自Paul Kimmel的很好的介紹性文章
http://www.informit.com/articles/article.asp?p=459619&seqNum=5&rl=1
來自Juval L?wy的介紹性文章 http://www.devx.com/codemag/Article/20639/1954?pf=true
(必看)Joseph Albahari的C#中使用線程 http://www.albahari.com/threading/part3.html
Michael Weinhardt寫的在Windows Forms 2.0 中一個簡單安全的多線程,我使用了這個網
頁中的CPU密集型任務,這是他從Chris Sell的文章中引用的。
http://www.mikedub.net/mikeDubSamples/SafeReallySimpleMultithreadingInWindowsFor
ms20/SafeReallySimpleMultithreadingInWindowsForms20.htm
如果你對多線程世界仍然不是特別熟悉或者希望了解最新的.Net 2.0 的
BackgroundWorker 組件,那么應該好好讀讀上面的文章。
提出的問題
任何一個任務……無論是CPU 密集型還是普通的任務:
CPU 密集型:它可以分成一個、兩個或多個線程,每個線程會占用一個CPU(這樣就使得
程序的性能翻番)
普通任務:每一個順序執行的普通任務,在進行數據存儲或使用一個web service 的時候都
會有一些延遲。所有的這些,都意味著這些沒有使用的時間對于用戶或任務本身來說有了浪
費。這些時間將被重新安排,并將被并行的任務使用,不會再丟失。也就是說,如果有100
個100ms 延遲的任務,它們在單線程模型和20 個線程模型的性能差距會達到1000%。
我們說,如果要處理一個創建一個網站多個塊的任務,不是順序的執行,而是花1-4 秒鐘
把所有的section 創建好;商標,在線用戶,最新文章,投票工具等等…… 如果我們能夠
異步地創建它們,然后在發送給用戶,會怎么樣?我們就會節省很多webservice 的調用,
數據庫的調用,許多寶貴的時間……這些調用會更快地執行。看上去是不是很誘人?
解決方案如下:
調用BackgroundWorker,正如我們想要的那樣,我們會繼承它。后臺worker 會幫助我們
建立一個“Worker”,用于異步地做一個工作。
我們想做的是建立一個工廠Factory(只是為了面向對象的設計,于設計模式無關),任務
會放在在這個Factory 中執行。這意味著,我們將有一類的任務,一些進程,一些知道如何
執行任務的worker。
當然我們需要一個負責分配任務給這些worker 的manager,告訴這些worker 當它們做完
一步或全部時,做什么事情。當然,我們也需要manager 能夠告訴worker 停止當前的任務。
它們也需要休息啊:)當manager 說停止的時候,它們就應該停止。
我們將會從底至上地解釋這些,首先從Worker 說起,然后再繼續Manager。
Worker
它是Background worker 的繼承類,我們構建一個構造函數,并分配兩個BackgroundWorker
的屬性,分別是WorkerReportsProgress 和WorkerSupportsCancellation,它們的功能就
向其名字的意義一樣:報告進度,停止任務。每個Worker 還有一個id,Manager 將會通過
這個id 控制它們。
public class MTWorker : BackgroundWorker
{
#region Private members
private int _idxLWorker = 0;
#endregion
#region Properties
public int IdxLWorker
{
get { return _idxLWorker; }
set { _idxLWorker = value; }
}
#endregion
#region Constructor
public MTWorker()
{
WorkerReportsProgress = true;
WorkerSupportsCancellation = true;
}
public MTWorker(int idxWorker)
: this()
{
_idxLWorker = idxWorker;
}
#endregion
另外,我們將重載BackgroundWorker 的一些函數。事實上,最有意思的是,究竟誰在做
真正的工作?它就是OnDoWork,當我們invoke 或者啟動多線程的時候,它就會被調用。
在這里,我們啟動任務、執行任務、取消和完成這個任務。
我加了兩個可能的任務,一個是普通型的,它會申請并等待文件系統、網絡、數據庫或
Webservices 的調用。另一個是CPU 密集型的任務:計算PI 值。你可以試試增加或減少線
程數量后,增加或是減少的延遲(我的意思是增減Worker 的數量)
OnDoWork 方法的代碼
protected override void OnDoWork(DoWorkEventArgs e)
{
//Here we receive the necessary data for doing the work...
//we get an int but it could be a struct, class, whatever..
int digits = (int)e.Argument;
double tmpProgress = 0;
int Progress = 0;
String pi = "3";
// This method will run on a thread other than the UI thread.
// Be sure not to manipulate any Windows Forms controls created
// on the UI thread from this method.
this.ReportProgress(0, pi);
//Here we tell the manager that we start the job..
Boolean bJobFinished = false;
int percentCompleteCalc = 0;
String TypeOfProcess = "NORMAL"; //Change to "PI" for a cpu intensive task
//Initialize calculations
while (!bJobFinished)
{
if (TypeOfProcess == "NORMAL")
{
#region Normal Process simulation, putting a time
delay to emulate a wait-for-something
while (!bJobFinished)
{
if (CancellationPending)
{
e.Cancel = true;
return; //break
}
//Perform another calculation step
Thread.Sleep(250);
percentCompleteCalc = percentCompleteCalc + 10;
if (percentCompleteCalc >= 100)
bJobFinished = true;
else
ReportProgress(percentCompleteCalc, pi);
}
#endregion
}
else
{
#region Pi Calculation - CPU intensive job,
beware of it if not using threading ;) !!
//PI Calculation
if (digits > 0)
{
pi += ".";
for (int i = 0; i < digits; i += 9)
{
// Work out pi. Scientific bit :-)
int nineDigits = NineDigitsOfPi.StartingAt(i + 1);
int digitCount = System.Math.Min(digits - i, 9);
string ds = System.String.Format("{0:D9}", nineDigits);
pi += ds.Substring(0, digitCount);
// Show progress
tmpProgress = (i + digitCount);
tmpProgress = (tmpProgress / digits);
tmpProgress = tmpProgress * 100;
Progress = Convert.ToInt32(tmpProgress);
ReportProgress(Progress, pi);
// Deal with possible cancellation
if (CancellationPending) //If the manager says to stop, do so..
{
bJobFinished = true;
e.Cancel = true;
return;
}
}
}
bJobFinished = true;
#endregion
}
}
ReportProgress(100, pi); //Last job report to the manager ;)
e.Result = pi; //Here we pass the final result of the Job
}
Manager
這是一個很有趣的地方,我確信它有很大的改進空間-歡迎任何的評論和改進!它所做的是
給每個線程生成和配置一個Worker,然后給這些Worker 安排任務。目前,傳給Worker
的參數是數字,但是它能夠傳送一個包含任務定義的類或結構。一個可能的改進是選擇如何
做這些內部工作的策略模式。
調用InitManager 方法配置任務,和它的數量等屬性。然后,創建一個多線程Worker 的數
組,配置它們。
配置的代碼如下:
private void ConfigureWorker(MTWorker MTW)
{
//We associate the events of the worker
MTW.ProgressChanged += MTWorker_ProgressChanged;
MTW.RunWorkerCompleted += MTWorker_RunWorkerCompleted;
}
Like this, the Worker’s subclassed thread management Methods are linked to the
Methods held by the Manager. Note that with a Strategy pattern implemented we could
assign these to the proper manager for these methods.
最主要的方法是AssignWorkers,它會檢查所有的Worker,如果發現沒有任務的Worker,
就分配一個任務給它。直到掃描一遍之后,沒有發現任何有任務的Worker,這樣就意味這
任務結束了。不需要再做別的了!
代碼如下:
public void AssignWorkers()
{
Boolean ThereAreWorkersWorking = false;
//We check all workers that are not doing a job... and assign a new one
foreach (MTWorker W in _arrLWorker)
{
if (W.IsBusy == false)
{
//If there are still jobs to be done...
//we assign the job to the free worker
if (_iNumJobs > _LastSentThread)
{
//We control the threads associated to a worker
//(not meaning the jobs done) just 4 control.
_LastSentThread = _LastSentThread + 1;
W.JobId = _LastSentThread; //We assign the job number..
W.RunWorkerAsync(_iPiNumbers); //We pass the parameters for the job.
ThereAreWorkersWorking = true;
//We have at least this worker we just assigned the job working..
}
}
else
{
ThereAreWorkersWorking = true;
}
}
if (ThereAreWorkersWorking == false)
{
//This means that no worker is working and no job has been assigned.
//this means that the full package of jobs has finished
//We could do something here...
Button BtnStart = (Button)FormManager.Controls["btnStart"];
Button BtnCancel = (Button)FormManager.Controls["btnCancel"];
BtnStart.Enabled = true;
BtnCancel.Enabled = false;
MessageBox.Show("Hi, I'm the manager to the boss (user): " +
"All Jobs have finished, boss!!");
}
}
只要有任務完成,這個方法就會被調用。從而,保證所有的任務能夠完成。
我們還通過一個屬性鏈接到Form 上,這樣我們就能向UI 上輸出我們想要的任何消息了。
當然,你可能想鏈接到其它的一些類,不過這是最基本最通用的。
Well… improving it we could get a BackgroundManager for all our application needs..
界面
連接到界面上,并不是最主要的功能。這一部分的代碼量非常少,也很簡單:在Manager
中添加一個引用,并在form 的構造函數中配置它。
在一個按鈕中,執行Manager 類的LaunchManagedProcess 方法。
private MTManager LM;
public Form1()
{
InitializeComponent();
LM = new MTManager(this, 25);
LM.InitManager();
}
private void btnStart_Click(object sender, EventArgs e)
{
btnStart.Enabled = false;
btnCancel.Enabled = true;
LM.LaunchManagedProcess();
}
private void btnCancel_Click(object sender, EventArgs e)
{
LM.StopManagedProcess();
btnCancel.Enabled = false;
btnStart.Enabled = true;
}
(下面的自己看嘍:)
Trying it!
This is the funniest part, changing the properties of how many threads to run
simultaneously and how many Jobs to be processed and then try it on different CPU’s…
ah, and of course, change the calculation method from a CPU-intensive task to a normal
task with a operation delay...
I would love to know your results and what have you done with this, any feedback would
be great!!
Exercises For You…
This is not done! It could be a MultiThreadJob Framework if there is being done the
following:
Implement a Strategy pattern that determines the kind of Worker to produce (with a
factory pattern) so we will be able to do different kind of jobs inside the same factory..
what about migrating a database and processing each table in a different way… or
integrating systems with this engine…
Implement -or extend- the strategy pattern for determining the treatment for the Input data
and the result data of the jobs. We could too set-up a factory for getting the classes into a
operating environment.
Optimize the AssignWorkers engine – I am pretty sure it can be improved.
Improve the WorkerManager class in order to be able to attach it to another class instead
to only a form.
Send me the code! I Would love to hear from you and what have you done.
About Jose Luis Latorre
Professional developer since 1991, having developed on multiple systems and
languages since then, from Unix, as400, lotus notes, flash, javascript, asp, prolog, vb, c++,
vb.Net, C#...
Now I'm focused on .Net development, both windows and web with two-three year
experience on both and fully up-to date with 2.0 .Net in both Vb and C#
Also have experience with SQL server 2005 and Business Intelligence.
Jose Luis Lives in Barcelona, Spain, with his cat Pancho. To contact Jose Luis, email him
at
[email protected]
Click here to view Jose Luis Latorre's online profile.
Copyright MSProject 2006-2007. 轉載本站文章必須經過作者本人或管理員的同意
C#中在線程中訪問主Form控件的問題
C#不允許直接從線程中訪問Form 里的控件,比如希望在線程里修改Form 里的
一個TextBox 的內容等等,唯一的做法是使用Invoke 方法,下面是一個MSDN
里的Example,很說明問題:
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
public class MyFormControl : Form
{
public delegate void AddListItem(String myString);
public AddListItem myDelegate;
private Button myButton;
private Thread myThread;
private ListBox myListBox;
public MyFormControl()
{
myButton = new Button();
myListBox = new ListBox();
myButton.Location = new Point(72, 160);
myButton.Size = new Size(152, 32);
myButton.TabIndex = 1;
myButton.Text = "Add items in list box";
myButton.Click += new EventHandler(Button_Click);
myListBox.Location = new Point(48, 32);
myListBox.Name = "myListBox";
myListBox.Size = new Size(200, 95);
myListBox.TabIndex = 2;
ClientSize = new Size(292, 273);
Controls.AddRange(new Control[] {myListBox,myButton});
Text = " 'Control_Invoke' example ";
myDelegate = new AddListItem(AddListItemMethod);
}
static void Main()
{
MyFormControl myForm = new MyFormControl();
myForm.ShowDialog();
}
public void AddListItemMethod(String myString)
{
myListBox.Items.Add(myString);
}
private void Button_Click(object sender, EventArgs e)
{
myThread = new Thread(new ThreadStart(ThreadFunction));
myThread.Start();
}
private void ThreadFunction()
{
MyThreadClass myThreadClassObject = new MyThreadClass(this);
myThreadClassObject.Run();
}
}
public class MyThreadClass
{
MyFormControl myFormControl1;
public MyThreadClass(MyFormControl myForm)
{
myFormControl1 = myForm;
}
String myString;
public void Run()
{
for (int i = 1; i <= 5; i++)
{
myString = "Step number " + i.ToString() + " executed";
Thread.Sleep(400);
// Execute the specified delegate on the thread that owns
// 'myFormControl1' control's underlying window handle with
// the specified list of arguments.
myFormControl1.Invoke(myFormControl1.myDelegate, new Object[] {myStrin
g}); }
}
}
BackgroudWorker 范例
在很多場合下, 你需要在主(UI)線程中運行一些比較耗時間的任務,比如以下的任務
1 Image downloads
2 Web service invocations
3 File downloads and uploads (including for peer-to-peer applications)
4 Complex local computations
5 Database transactions
6 Local disk access, given its slow speed relative to memory access
這個時候UI 就會陷入一種假死的狀態,會給用戶帶來一種很不好的體驗. 如何在這里發揮多線程的優勢以改善用
戶體驗? .Net2.0 的System.ComponentModel.BackgroundWorker 為我們提供了一個很方便的解決方法.
在vs.net2005 101 sample 中提供了一個計算素數的例子, 不過那個例子并沒有全面演示
BackgroundWorker 的能力, 尤其是沒有對線程工作過程(ReportProgress)中的能力做比較好的演示.因此我
重新做了一個Demo.
這個例子很簡單, 就是將左邊列表中的內容移至的右邊, 用一個進度條來顯示移動的進度, 當然既然是
BackgroundWorker 這個時候主界面可以進行其他操作.
本文的源代碼提供下載, 其中有詳細注釋, 所以我在此簡要介紹一下需要注意的地方.
BackgroundWorker 主要通過對DoWork ProgressChanged RunWorkerCompleted 三個事件
的處理來完成任務. 需要注意在DoWork 中不能直接操作主界面的元素.比如你在MainForm 類中啟動了一個
BackgroundWorker, 在DoWork 的處理方法中不能直接調用任何MainForm 中的成員變量. 但是在
ProgressChanged 和 RunWorkerCompleted 的事件處理中則無此限制, 可以在后臺線程中直接調用主線程
中的元素, 這是BackgroundWorker 中最有亮點的地方. 雖然在DoWork 的處理方法中不能調用但是它也提供
了參數傳遞的方法,可以間接調用.示例如下:
27 //If your background operation requires a parameter,
28 //call System.ComponentModel.BackgroundWorker.RunWorkerAsync
29 //with your parameter. Inside the System.ComponentModel.BackgroundWorker.DoWork
30 //event handler, you can extract the parameter from the
31 //System.ComponentModel.DoWorkEventArgs.Argument property.
32 worker.RunWorkerAsync(leftList);
27 private void worker_DoWork(object sender, DoWorkEventArgs e)
28 {
29 MoveList((BackgroundWorker)sender,e);
30 }
31
32 private void MoveList(BackgroundWorker worker,DoWorkEventArgs e)
33 { //get leftList in Main UI Thread from arguments
34 IList<string> list = e.Argument as IList<string>;
35 //...
36 }
而在ProgressChanged 和RunWorkerCompleted 事件的處理方法中則更加簡單.
27 private void worker_ProgressChanged(object sender, ProgressChangedEventArgs
e)
28 {
29 //Add string to the right listBox, we use rightList in Main UI Thread directly
30 rightList.Add(e.UserState as string);
31 }
上述原則可以說是BackgroundWorker 最需要注意的地方.
另外一個容易被人粗心漏過的地方是有關屬性的設置.
如果你要使BackgroundWorker 支持進度匯報和取消功能別忘了在初始化的時候為下面兩個屬性賦值.
// Specify that the background worker provides progress notifications
worker.WorkerReportsProgress = true;
// Specify that the background worker supports cancellation
worker.WorkerSupportsCancellation = true;
其它部分就讓大家自己看代碼吧.
BackgroundWorker 內部實現是基于delegate 的異步調用.
dotnet 中一個重要組件-BackgroundWorker - Strive for perfection,Settle for excellence! - 博客園
dotnet 中一個重要組件-BackgroundWorker
最近一直在看wse3.0,從一個例子中偶然的收獲。雖然通過后臺操作,從而減少用戶交互時的“僵硬”體驗一
直是每個程序員的追求,在今天這樣ajax 的時代里面更加顯的重要。一切為了用戶,一切為了更豐富愉快的體
驗。本文并不是ajax 相關的東東。偉大的BackgroundWorker!
BackgroundWorker 類允許您在單獨的專用線程上運行操作。耗時的操作(如下載和數據庫事務)在長時間運
行時可能會導致用戶界面 (UI)
似乎處于停止響應狀態。如果您需要能進行響應的用戶界面,而且面臨與這類操作相關的長時間延遲,則可以使
用 BackgroundWorker 類方便地解決問題。
您必須非常小心,確保在 DoWork 事件處理程序中不操作任何用戶界面對象。而應該通過
ProgressChanged 和
RunWorkerCompleted 事件與用戶界面進行通信
使用方式:
1。給組件注冊事件處理方法:
//正式做事情的地方
backgroundWorker1.DoWork +=
new DoWorkEventHandler(backgroundWorker1_DoWork);
//任務完稱時要做的,比如提示等等
backgroundWorker1.RunWorkerCompleted +=
new
RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted);
//任務進行時,報告進度
backgroundWorker1.ProgressChanged +=
new
ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
2。添加具體事件處理方法
DoWork 調用 RunWorkerAsync 時發生。
ProgressChanged 調用 ReportProgress 時發生。
RunWorkerCompleted 當后臺操作已完成、被取消或引發異常時發生。
1//這個例子沒有做什么事情,完全是看看效果而已,但同時有個大問題,我也不知道為什么,沒有去除僵硬情
況。
2namespace BackgroundWorkerTest
3{
4 public partial class Form1 : Form
5 {
6 public Form1()
7 {
8 InitializeComponent();
9 InitialzeBackgroundWorker();
10 }
11
12 private void InitialzeBackgroundWorker()
13 {
14 this.backgroundWorker1.DoWork+=new
DoWorkEventHandler(backgroundWorker1_DoWork);
15 this.backgroundWorker1.ProgressChanged+=new
ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
16 this.backgroundWorker1.RunWorkerCompleted+=new
RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted);
17 }
18
19
20 private void backgroundWorker1_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
21 {
22 MessageBox.Show("Completly");
23 }
24
25 private void backgroundWorker1_ProgressChanged(object sender,
ProgressChangedEventArgs e)
26 {
27 this.progressBar1.Value = e.ProgressPercentage;
28 }
29 private void backgroundWorker1_DoWork(object sender,DoWorkEventArgs e)
30 {
31 e.Result = ComputeFibonacci(this.backgroundWorker1, e);
32 }
33
34 private int ComputeFibonacci(object sender, DoWorkEventArgs e)
35 {
36
37 for (int i = 0; i < 100000; i++)
38 {
39 if (this.backgroundWorker1.CancellationPending)
40 {
41 e.Cancel = true;
42
43 }
44 else
45 {
46 this.backgroundWorker1.ReportProgress(i);
47 }
48
49 }
50 return 0;
51
52 }
53
54
55 private void button1_Click(object sender, EventArgs e)
56 {
57 this.backgroundWorker1.RunWorkerAsync();
58 }
59
60 private void button2_Click(object sender, EventArgs e)
61 {
62 this.backgroundWorker1.CancelAsync();
63 }
64 }
65}
給出另一種使用:繼承BackgroundWorker:
namespace UploadWinClient
{
/**//// <summary>
/// Contains common functionality for the upload and download classes
/// This class should really be marked abstract but VS doesn't like that
because it can't draw it as a component then :(
/// </summary>
public class FileTransferBase : BackgroundWorker
{
public FileTransferBase()
{
base.WorkerReportsProgress = true;
base.WorkerSupportsCancellation = true;
}
protected override void Dispose(bool disposing)
{
if(this.HashThread != null && this.HashThread.IsAlive)
this.HashThread.Abort();
base.Dispose(disposing);
}
protected override void OnRunWorkerCompleted(RunWorkerCompletedEventArgs
e)
{
if(this.HashThread != null && this.HashThread.IsAlive)
this.HashThread.Abort();
base.OnRunWorkerCompleted(e);
}
protected override void OnDoWork(DoWorkEventArgs e)
{
// make sure we can connect to the web service. if this step is not
done here, it will retry 50 times because of the retry code
this.WebService.Ping();
base.OnDoWork(e);
}
}//截取的部分代碼。
//從中給出我們要override 的一些方法
BackgroundWorker 在長時間的webservices 中特別有用。
posted on 2006-07-05 10:25 flyingchen 閱讀(708) 評論(1) 編輯 收藏 引用 網摘 所屬分類: DotNet
技術
評論
# 關于net2.0 里面新出現的類backgroundworker 的應用[TrackBack] 2007-01-11 16:16 天龍
這是一個在.net2.0 里面新出現的類,用于執行后臺比較長的任務而又想能和UI 有點操作的應用里面。普通情
況下,你點擊一個按鈕,去后臺執行一個process,如果你想得到結果,就得等這個proces...
這是一個在.net2.0 里面新出現的類,用于執行后臺比較長的任務而又想能和UI 有點操作的應用里面。
普通情況下,你點擊一個按鈕,去后臺執行一個process,如果你想得到結果,就得等這個process 結束。通
常,可以使用異步執行回調來解決這個問題。現在,backgroundworker 給我們實現了這樣一種簡單的封裝,
可以把我們的復雜任務交給新的線程去處理,然后繼續UI 線程。等到我們的任務需要通知UI 做什么事情的時候,
可以report 一下,在其事件里就可以直接使用UI 控件,而不需要Control.Invoke 去掉用之。
有這樣一個應用:客戶需要把大量數據(需要執行3 天)copy 到另外一個機器,中間想能看到有多少數據被復
制/失敗等(實時報道)。
在這個例子里面,我們的界面可能非常簡單:一個開始按鈕,一個結束按鈕,一個richtextBox 來顯示運行記錄。
但是后臺執行可能就會比較棘手。如果簡單的執行,并且報告,那么整個界面將失去響應(都在同一個線程里面,
造成忙碌)。這時候,可以使用這個backgroundworker 了。它可以在后臺執行,并且報告給界面實時信息,界
面不會失去響應。
先介紹一下backgroundworker 的幾個屬性/方法
.WorkerReportsProgress:是否可以向外報告進度。
.WorkerSupportsCancellation :是否可以暫停任務
. CancellationPending: 是否正在暫停中
. RunWorkerAsync() : 開始執行任務。觸發DoWork 事件
. ReportProgress(int percentPrgress,object userState) : 向外報告進度。觸發ProgressChanged
事件.其中,參數可以在ProgressChangedEventArgs(worker_ProgressChanged(object sender,
ProgressChangedEventArgs e))中得到
. CancelAsync() :取消(暫停)執行。
事件
worker.DoWork += new DoWorkEventHandler(worker_DoWork);//執行任務
worker.RunWorkerCompleted += new
RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);//任務結束時
worker.ProgressChanged += new
ProgressChangedEventHandler(worker_ProgressChanged)//報告狀態
按照上邊的資料,我們這個應用就可以這樣處理之
formDisplay 是用于顯示實時狀態的窗口。有DisplyMessage 方法來顯示信息到界面
在Hanlder 類(處理文件copy 的)里面:
static void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
//show the message on windows
formDisplay.DisplyMessage(“copy”, e.UserState.ToString());//show message.
}
static void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs
e)
{
string msg = "JOB copy : have been completed";
formDisplay.DisplyMessage(msg);//show message
}
static void worker_DoWork(object sender, DoWorkEventArgs e)
{
for(…)
{
//copying
(sender as BackgroundWorker). ReportProgress(0,”xxx complete”);//report
}
}
這樣構造的程序里面,才不會出現UI 失去響應。
當然,通過寫自己的異步處理也可以實現,功能更強大。只不過這個用起來更簡單。
至于backgroundworker 是怎么實現的呢?這里有人已經做出了一些解答:
http://blog.joycode.com/sunmast/archive/2006/03/02/about_system_componentmodel_asynco
peration.aspx
在UI 上使用BackgroundWorker
凡是WinForm 的應用程序,如果他執行了一個的非常冗長的處理操作(比如文件查詢),它在執行時會鎖定
用戶界面,雖然主活動窗口 一直在運行,但用戶無法與程序交互,無法移動窗體或改變窗體大小,所以用戶感
覺很不爽。如何做才能使得這個程序有響應。答案就是在后臺線程中執行這個操作。
在這里已經有了多種方法來做這個事情:
(一)委托異步調用
將具體耗時的操作作為一個委托,并用BeginInvoke 來異步執行這個委托(Invoke 是同步調用),并且可以
為這個操作傳入參數并且通過EndInvoke 方法獲得返回返回值。
(二)使用ThreadPool
新建.net FrameWork 中自帶的WaitCallback 委托,然后放到線程池中運行
ThreadPool.QueueUserWorkItem( callback ); 根據WaitCallback 委托的定義,可以傳入一個object 類
型的參數。
但是不能精確的控制線程池中的線程。
(三)使用Thread
和ThreadPool 相比,使用Thread 的開銷會比較大。但是它有它的優勢,使用 Thread 類可以顯式管
理線程。只要有可能,就應該使用 ThreadPool 類來創建線程。然而,在一些情況下,您還是需要創建并管理
您自己的線程,而不是使用 ThreadPool 類。在.net 2.0 中,提供了一個新的委托
ParameterizedThreadStart 支持啟動一個線程并傳入參數,這是對原來的ThreadStart 委托的改進。
說了這么多還沒有說到今天的主角BackgroundWorker,他也是一個在2.0 中新增的類,可以用于啟動后臺
線程,并在后臺計算結束后調用主線程的方法.可以看出同樣的功能使用委托的異步調用也可以實現,只是使用
BackgroundWorker 的話會更加的簡便快捷,可以節省開發時間,并把你從創建自己的委托以及對它們的調用
中解救出來。真是這樣的嗎看看下面這個例子。其實我也是從101Samples 中看到的例子。
先看看BackgroundWorker 中的主要概念。
第一:主要的事件及參數。
DoWork——當執行BackgroundWorker.RunWorkerAsync 方法時會觸發該事件,并且傳遞
DoWorkEventArgs 參數;
ProgressChanged——操作處理中獲得的處理狀態變化,通過
BackgroundWorker.ReportProgress(int)方法觸發該事件,并且傳遞ProgressChangedEventArgs,其中包
含了處理的百分比;
RunWorkerCompleted——異步操作完成后會觸發該事件,當然如果需要在操作過程中結束可以執行
BackgroundWorker.CancelAsync 方法要求異步調用中止,并且在異步委托操作中檢測
BackgroundWorker.CancellationPending 屬性如果為true 的話,跳出異步調用,同時將
DoWorkEventArgs.Cancel 屬性設為true,這樣當退出異步調用的時候,可以讓處理RunWorkerCompleted
事件的函數知道是正常退出還是中途退出。
第二:主要的方法。
BackgroundWorker.RunWorkerAsync——
“起動”異步調用的方法有兩次重載RunWorkerAsync(),RunWorkerAsync(object argument),
第二個重載提供了一個參數,可以供異步調用使用。(如果有多個參數要傳遞怎么辦,使用一個類來傳遞他們吧)。
調用該方法后會觸發DoWork 事件,并且為處理DoWork 事件的函數DoWorkEventArg 事件參數,其中包含
了RunWorkerAsync 傳遞的參數。在相應DoWork 的處理函數中就可以做具體的復雜操作。
BackgroundWorker.ReportProgress——
有時候需要在一個冗長的操作中向用戶不斷反饋進度,這樣的話就可以調用的ReportProgress(int
percent),在調用 ReportProgress 方法時,觸發ProgressChanged 事件。提供一個在 0 到 100 之間的整
數,它表示后臺活動已完成的百分比。你也可能提供任何對象作為第二個參數,允許你 給事件處理程序傳遞狀
態信息。作為傳遞到此過程的 ProgressChangedEventArgs 參數屬性,百分比和你自己的對象(如果提供的
話)均要被傳遞到 ProgressChanged 事件處理程序。這些屬性被分別命名為 ProgressPercentage 和
UserState,并且你的事件處理程序可以以任何需要的方式使用它們。(注意:只有在
BackgroundWorker.WorkerReportsProgress 屬性被設置為true 該方法才可用)。
BackgroundWorker.CancelAsync——
但需要退出異步調用的時候,就調用的這個方法。但是樣還不夠,因為它僅僅是將
BackgroudWorker.CancellationPending 屬性設置為true。你需要在具體的異步調用處理的時候,不斷檢查
BackgroudWorker.CancellationPending 是否為true,如果是真的話就退出。(注意:只有在
BackgroundWorker.WorkerSupportsCancellation 屬性被設置為true 該方法才可用)。
貼出一段101Samples 里面的代碼,看一下就明白了:
public partial class MainForm : Form
{
private System.ComponentModel.BackgroundWorker backgroundCalculator;
public MainForm()
{
InitializeComponent();
backgroundCalculator = new BackgroundWorker();
backgroundCalculator.WorkerReportsProgress = true;
backgroundCalculator.WorkerSupportsCancellation = true;
backgroundCalculator.DoWork += new
DoWorkEventHandler(backgroundCalculator_DoWork);
backgroundCalculator.ProgressChanged += new
ProgressChangedEventHandler(backgroundCalculator_ProgressChanged);
backgroundCalculator.RunWorkerCompleted += new
RunWorkerCompletedEventHandler(backgroundCalculator_RunWorkerCompleted);
updateStatus(String.Empty);
}
private int getNextPrimeAsync(int start, BackgroundWorker worker, DoWorkEventArgs e)
{
int percentComplete = 0;
start++;
while (!isPrime(start))
{
// Check for cancellation
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
else
{
start++;
percentComplete++;
worker.ReportProgress(percentComplete % 100);
}
}
return start;
}
void backgroundCalculator_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs
e)
{
if (e.Cancelled)
{
updateStatus("Cancelled.");
}
else if (e.Error != null)
{
reportError(e.Error);
}
else
{
reportPrime((int)e.Result);
}
calcProgress.Value = 0;
}
void backgroundCalculator_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
updateProgress(e.ProgressPercentage);
}
void backgroundCalculator_DoWork(object sender, DoWorkEventArgs e)
{
int start = (int) e.Argument;
e.Result = getNextPrimeAsync(start, (BackgroundWorker)sender, e);
}
private void nextPrimeAsyncButton_Click(object sender, EventArgs e)
{
updateStatus("Calculating...");
int start;
Int32.TryParse(textBoxPrime.Text, out start);
if (start == 0)
{
reportError("The number must be a valid integer");
}
else
{
// Kick off the background worker process
backgroundCalculator.RunWorkerAsync(int.Parse(textBoxPrime.Text));
}
}
private void cancelButton_Click(object sender, EventArgs e)
{
if (backgroundCalculator.IsBusy)
{
updateStatus("Cancelling...");
backgroundCalculator.CancelAsync();
}
}
// Update the Status label
private void updateStatus(string status)
{
calcStatus.Text = status;
}
// Indicate progress using progress bar
private void updateProgress(int percentComplete)
{
calcProgress.Value = percentComplete;
}
}
BackgroundWorker 創建自己的委托并調用這個窗體的 Invoke 方法來運行它,BackgroundWorker
組件以一種優雅的方式來處理這個線程轉換。BackgroundWorker 組件允許你從后臺線程中調用它的
ReportProgress 方法,該方法觸發其 ProgressChanged 事件處理例程返回到窗體的線程中。你不必使用
delegate/Invoke 方法自己處理這個線程轉換,而是調用 ReportProgress,其余的事情交給組件來做。
DotNet 中異步編程的簡單應用 - shenba - 博客園
DotNet 中異步編程的簡單應用
這里說的異步編程并不是AJAX 等的Web 異步編程,而僅僅是DotNet 中多線程的異步編程.這種多線程的異步
編程主要用來解決某些受計算操作影響而引起主線程阻塞的問題.讓程序(主要是窗體應用程序)看跑得更流暢.在
dotnet 的CLR 以及API 方法中有簡單易用的方法供我們實現異步編程,并且都有相似的調用方法,諸如
BeginXXX,EndXXX,IAsyncResult 對象,同時也都涉及到回調,委托等操作.下面是一些簡單的應用
1.異步的IO 操作,基本上就是按參數傳遞
異步IO
1// 值得注意的是最好給定FileOptions.Asynchronous,相對效率會高些
2 FileStream fs = new FileStream("E://test.txt", FileMode.Open,
FileAccess.Read, FileShare.Read, 1024, FileOptions.Asynchronous);
3 byte[] data = new byte[(int)fs.Length];
4
5 // 開始讀取
6 fs.BeginRead(data, 0, data.Length, delegate (IAsyncResult ar)
7 {
8 // 這里是讀取結束之后回調的委托方法內容
9 fs.EndRead(ar);
10 fs.Close();
11
12 string content = Encoding.Default.GetString(data);
13 Console.WriteLine(content);
14 }, fs);
2.讀取數據庫內容的異步操作,這里的代碼是用在窗體程序中,涉及到一個跨線程改變窗體控件的問題,但是在窗
體里,只有通過窗體的主線程來改變控件的行為.如下異步讀取數據,讀取數據的操作是在另外一個線程中,因此試
圖在這個線程中操作窗體控件是徒勞的,還會出異常.所以只能通過窗體的invoke 方法,調用一個委托,這樣才能
實現.當然這里只是為了提出這個問題,其他的解決的方法還是有的,比如效率更低的輪詢方法.
數據訪問
1// 這個委托 用于在窗體的主線程中調用
2 private delegate void FillGridHandler(SqlDataReader reader);
3 private void AsnDataAccess()
4 {
5 // 對于異步操作的數據訪問,連接字符串里必須設置Async=True
6 string conStr =
ConfigurationManager.ConnectionStrings["NorthWind"].ConnectionString;
7 SqlConnection con = new SqlConnection(conStr);
8 SqlCommand command = new SqlCommand("select [CompanyName] from
[Suppliers]", con);
9
10 con.Open();
11 command.BeginExecuteReader(delegate(IAsyncResult ar)
12 {
13 SqlDataReader reader = command.EndExecuteReader(ar);
14
15 // 對于窗體應用程序的 GridView 的綁定
16 FillGridHandler fillGridHandler = delegate(SqlDataReader
reader1)
17 {
18 DataTable dt = new DataTable();
19 dt.Load(reader1);
20 reader.Close();
21 dataGridView1.DataSource = dt;
22 };
23
24 // 用窗體自身的線程去觸發委托方法 才能改變窗體里控件屬性
25 this.Invoke(fillGridHandler, reader);
26 }, command, System.Data.CommandBehavior.CloseConnection);
27 }
3.異步觸發委托的方法,異步編程離不開委托,其本身也就是調用了委托的異步方法,其內部就必定有一個委托對

委托異步
1 delegate int CalSumHandler(int number);
2 private static void DelegateAsyn()
3 {
4 CalSumHandler handler = delegate(int number)
5 {
6 int sum = 0;
7 for (int i = 0; i < number; i++)
8 {
9 sum += i;
10 }
11 return sum;
12 };
13
14 int n = 10;
15 handler.BeginInvoke(n, delegate(IAsyncResult ar)
16 {
17 int res = handler.EndInvoke(ar);
18 Console.WriteLine("result from asyndelegate,sum = {0}", res);
19 }, n);
20 }
posted on 2007-10-01 11:12 神八 閱讀(102) 評論(0) 編輯 收藏
Delegate 比較全面的例子(原創) - 享受代碼,享受人生 - 博客園
享受代碼,享受人生
SOA is an integration solution. SOA is message oriented first.
The Key character of SOA is loosely coupled. SOA is enriched by
creating composite apps.
將Delegate 理解為接口,只有一個方法的接口,這樣最容易理解。這個方法只有聲明,沒有實現,實現
在別的類。(實際上應該把它看作函數指針,不過接口更容易理解些。)
在你的類中有一個Delegate 就相當于有一個接口。通過這個接口你可以調用一個方法,而這個方法在別
的類定義,由別的類來干。
為了說的形象一點,舉個例子:
學生考試完后成績出來了,考的好了老師要表揚,考的不好了老師要批評。
使用接口的方法:
using System;
public class Student
{
private IAdviser adviser;
public void SetAdviser(IAdviser iadviser)
{
adviser = iadviser;
}
private int score;
public void SetScore(int value)
{
if (value > 100 || value < 0)
{
Console.Out.WriteLine("分數不對");
}
else
{
score = value;
if (adviser != null)
{
string result = adviser.Advise(score);
Console.Out.WriteLine("學生收到老師返回的結果/t"+result);
}
}
}
}
public interface IAdviser
{
string Advise(int score);
}
public class Teacher : IAdviser
{
public string Advise(int score)
{
if (score < 60)
{
Console.Out.WriteLine(score+"老師說加油");
return "不及格";
}
else
{
Console.Out.WriteLine(score+"老師說不錯");
return "及格";
}
}
}
class MainClass
{
[STAThread]
private static void Main(string[] args)
{
IAdviser teacher = new Teacher();
Student s = new Student();
s.SetAdviser(teacher);
Console.Out.WriteLine("學生得到50 分");
s.SetScore(50);
Console.Out.WriteLine("/n 學生得到75 分");
s.SetScore(75);
Console.ReadLine();
}
}
使用Delegate 的方法:
using System;
using System.Threading;
public class Student
{
private int score;
public void SetScore(int value)
{
if (value > 100 || value < 0)
{
Console.Out.WriteLine("分數不對");
}
else
{
score = value;
if (AdviseDelegateInstance!= null)
{
string result=AdviseDelegateInstance(score);
Console.Out.WriteLine("學生收到老師返回的結果/t"+result);
}
}
}
public delegate string AdviseDelegate(int score);
public AdviseDelegate AdviseDelegateInstance;
}
public class Teacher
{
public string Advise(int score)
{
if(score<60)
{
Console.Out.WriteLine(score+"老師說加油");
return "不及格";
}
else
{
Console.Out.WriteLine(score+"老師說不錯");
return "及格";
}
}
}
class MainClass
{
[STAThread]
static void Main(string[] args)
{
Teacher teacher=new Teacher();
Student s=new Student();
s.AdviseDelegateInstance=new
Student.AdviseDelegate(teacher.Advise);
Console.Out.WriteLine("學生得到50 分");
s.SetScore(50);
Console.Out.WriteLine("/n 學生得到75 分");
s.SetScore(75);
Console.ReadLine();
}
}
如果老師很忙不能及時回復怎么辦?比如這樣:
public class Teacher
{
public string Advise(int score)
{
Thread.Sleep(3000);
if(score<60)
{
Console.Out.WriteLine(score+"老師說加油");
return "不及格";
}
else
{
Console.Out.WriteLine(score+"老師說不錯");
return "及格";
}
}
}
總不能讓學生一直等下去吧,采用多線程并發的辦法。
Interface 的解決辦法:
public void SetScore(int value)
{
if (value > 100 || value < 0)
{
Console.Out.WriteLine("分數不對");
}
else
{
score = value;
if (adviser != null)
{
Thread.adviserThread=new Thread(new
ThreadStart(adviser.Advise()));
adviserThread.Start();
}
}
}
但是它不能使用帶參數的函數,怎么辦?(誰知道方法請指教)
.Net2.0 提供了新的方法ParameterizedThreadStart
用Delegate 解決(異步調用):
public void SetScore(int value)
{
if (value > 100 || value < 0)
{
Console.Out.WriteLine("分數不對");
}
else
{
score = value;
if (AdviseDelegateInstance!= null)
{
AdviseDelegateInstance.BeginInvoke(score,null,null);
}
}
}
不過這樣我們失去了老師的返回結果,不知道有沒有及格了。
采用輪訊的方法去獲得結果:
public void SetScore(int value)
{
if (value > 100 || value < 0)
{
Console.Out.WriteLine("分數不對");
}
else
{
score = value;
if (AdviseDelegateInstance!= null)
{
IAsyncResult res =
AdviseDelegateInstance.BeginInvoke(score,null, null);
while( !res.IsCompleted )
System.Threading.Thread.Sleep(1);
string result =
AdviseDelegateInstance.EndInvoke(res);
Console.Out.WriteLine("學生收到老師返回的結果/t"+result);
}
}
}
不過這樣主線程又被阻塞了,采用回調的方式: (注:接口也可以采用回調的方式獲得返回值)
public void SetScore(int value)
{
if (value > 100 || value < 0)
{
Console.Out.WriteLine("分數不對");
}
else
{
score = value;
if (AdviseDelegateInstance!= null)
{
IAsyncResult res =
AdviseDelegateInstance.BeginInvoke(score, new
System.AsyncCallback(CallBackMethod), null);
}
}
}
private void CallBackMethod(IAsyncResult asyncResult)
{
string result = AdviseDelegateInstance.EndInvoke(asyncResult);
Console.Out.WriteLine("學生收到老師返回的結果/t" + result);
}
這樣就比較得到了一個比較好的解決方案了。我們再來看看BeginInvoke 的第四個參數是干嗎的呢?
public void SetScore(int value)
{
if (value > 100 || value < 0)
{
Console.Out.WriteLine("分數不對");
}
else
{
score = value;
if (AdviseDelegateInstance!= null)
{
AdviseDelegateInstance.BeginInvoke(score, new
System.AsyncCallback(CallBackMethod), "idior");
}
}
}
private void CallBackMethod(IAsyncResult asyncResult)
{
string result = AdviseDelegateInstance.EndInvoke(asyncResult);
string stateObj=(string)asyncResult.AsyncState;
Console.Out.WriteLine("學生{0}收到老師返回的結果/t" +
result,stateObj.ToString());
}
哦,原來它可以用來標記調用者的一些信息。(這里采取的是硬編碼的方式,你可以把它改為學生的id 之
類的信息)。
總結:Delegate 類似與Interface 但是功能更加強大和靈活,它甚至還可以綁定到Static 方法只要函數
簽名一致,而且由于+=操作符的功能,實現多播也是極為方便(即Observer 模式),在此不再舉例。
(補充:多播的時候改一下SetScore 函數)
public void SetScore(int value)
{
if (value > 100 || value < 0)
{
Console.Out.WriteLine("分數不對");
}
else
{
score = value;
if (AdviseDelegateInstance!= null)
{
foreach( AdviseDelegate ad in
AdviseDelegateInstance.GetInvocationList())
{
ad.BeginInvoke(score, new
System.AsyncCallback(CallBackMethod), "idior");
}
}
}
}
本文沒什么新的內容,就是自己練一下手,寫個總結材料,希望對大家有幫助。.net2.0 提供了更好的線
程模型。
完整源代碼如下:
using System;
using System.Threading;
public class Student
{
private int score;
public void SetScore(int value)
{
if (value > 100 || value < 0)
{
Console.Out.WriteLine("分數不對");
}
else
{
score = value;
if (AdviseDelegateInstance!= null)
{
AdviseDelegateInstance.BeginInvoke(score, new
System.AsyncCallback(CallBackMethod), "idior");
}
}
}
private void CallBackMethod(IAsyncResult asyncResult)
{
string result = AdviseDelegateInstance.EndInvoke(asyncResult);
string stateObj=(string)asyncResult.AsyncState;
Console.Out.WriteLine("學生{0}收到老師返回的結果/t" + result,stateObj);
}
public delegate string AdviseDelegate(int score);
public AdviseDelegate AdviseDelegateInstance;
}
public class Teacher
{
public string Advise(int score)
{
Thread.Sleep(3000);
if (score < 60)
{
Console.Out.WriteLine(score + "老師說加油");
return "不及格";
}
else
{
Console.Out.WriteLine(score + "老師說不錯");
return "及格";
}
}
}
class MainClass
{
[STAThread]
private static void Main(string[] args)
{
Teacher teacher = new Teacher();
Student s = new Student();
s.AdviseDelegateInstance= new
Student.AdviseDelegate(teacher.Advise);
Console.Out.WriteLine("學生得到50 分");
s.SetScore(50);
Console.Out.WriteLine("/n 學生得到75 分");
s.SetScore(75);
Console.ReadLine();
}
}
參考資料: .NET Delegates: A C# Bedtime Story
Feedback
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-02-02 10:57 by KingofSC
不錯啊,居然還說到多線程去了
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-02-02 11:27 by idior
@KingofSC
發現你總是潛水哦 :P
哪天看看你的大作啊?
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-02-02 14:05 by 呂震宇
不錯!很全面。就是“Advise”出現得太多了,有時候分不清是Advise 方法還是Advise 委派了:)
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-02-02 15:22 by idior
@呂震宇
不好意思,是有點讓人看不懂,已修改.謝謝指正.
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-03-11 00:56 by douhao_lale
很好啊,謝謝
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-07-25 15:46 by Boler
開始看的挺明白,后來太復雜了,看不懂了,放棄了~
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-07-25 20:46 by idior
需要用到多線程的時候再來看看吧
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-07-26 08:17 by yinh
不錯,idior 你寫的文章我都非常感興趣。
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-07-30 17:06 by HNHLS99
上面的多線程的例子比較透徹,但是對多線程的機制涉及的很少啊,希望能補充以下。比如線程的輪詢,
線程的開始與結束,異步的調用的同步等等。
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-07-30 17:49 by idior
呵呵 別忘了本文的題目 “Delegate 比較全面的例子”
或許可以由你來介紹多線程啊。
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-08-18 11:18 by 阿新
牛,嘔像
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-11-22 10:45 by luyu
前面看的不錯,越看越混亂了。
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-12-14 17:05 by giogio
呵呵,其實寫到“如果老師很忙不能及時回復怎么辦?比如這樣:”之前比較好。
后面就和多線程結合的更緊密了。
我覺得線程本身比delegate 要更大更基本,delegate 可以算一節,線程就應該算一章。
類似于講蒸氣機的時候講到一半開始用量子力學解釋……這個確實容易讓人暈……
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-12-19 09:41 by giogio
“將Delegate 理解為接口,只有一個方法的接口,這樣最容易理解。”
這么說不好吧……
我覺得這倆玩意不是一回事啊,只是看起來比較想像而已。
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-12-19 22:16 by idior
@ giogio
記住這句話,或許有一天你會突然覺得有道理的。
這是delegate 的本質,不過需要你對面向對象有一定的理解。
你可以參考一下這篇文章。
http://idior.cnblogs.com/archive/2005/02/03/101510.aspx
http://linkcd.cnblogs.com/archive/2005/07/19/196087.html
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2005-12-19 22:46 by giogio
其實是這樣的。
老師講完delegate 后,我就說:這玩意和interface 起的作用一樣。
老師就說:從表面上看是這樣的,但是兩者從根本上不同。
他似乎很像讓我牢牢記住這是兩種東西,強調這兩個不能混淆。
老師還讓我準備一下,用大約15 分鐘給大家講delegate 和event 呢……
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2006-03-18 17:03 by qq:86562467
拜托各位高手,誰哪兒有關于Delegate Event WebService 方面的例子
我想實現如下的功能
假設我有三個類 分別是Class S , Class A , Class B
現在想用 代理Delegate 和 事件Event 實現類A 和類B 的通信功能
但是 類A 和類B 不能直接通信 必須通過 類S 實現
類S 就是 起一個 中轉或服務器的作用
誰哪兒有這樣的例子 拜托 給一份 學習一下
謝謝了
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2006-04-26 10:48 by anchky
收藏了!
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2006-08-10 09:28 by 匿名
貌似老大很習慣用JAVA,相信public void SetScore(int value)
不能通過編譯,似乎s.AdviseDelegateInstance=new
Student.AdviseDelegate(teacher.Advise)也是不能通過編譯的,因為你沒有實現Advise 的get 方法。
但老大確實很有想法,
把一些問題說得比較清楚
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2006-09-13 16:20 by lxinxuan
Console.Out.WriteLine("學生{0}收到老師返回的結果/t" + result,stateObj); 改為
Console.Out.WriteLine(string.Format("學生{0}收到老師返回的結果/t" + result,
stateObj));
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2006-09-13 17:11 by lxinxuan
@qq:86562467:
我知道以下是沒有滿足你的要求,所以,我請求idior 幫忙解答:
public class A
{
public delegate void SetValueDelegate(string v);
public SetValueDelegate SetValueInstance;
public void SetValue(string v)
{
SetValueInstance(v);
}
}
public class S
{
B b = new B();
public void SetValue(string v)
{
b.SetValue(v);
}
}
public class B
{
public string bValue;
public B()
{
bValue = "b";
}
public void SetValue(string v)
{
this.bValue = v;
}
public string GetValue()
{
return this.bValue;
}
}
class MainClass
{
[STAThread]
private static void Main(string[] args)
{
S s = new S();
A a = new A();
B b = new B();
a.SetValueInstance = new A.SetValueDelegate(s.SetValue);
a.SetValue("a");
MessageBox.Show(b.GetValue());//
}
}
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2006-09-19 00:53 by huangyi_
<pre>
class Delegate(object):
'模擬.net 的delegate'
def __init__(self):
self.handlers = []
def __call__(self,*args,**kw):
for h in self.handlers:
h(*args,**kw)
def __iadd__(self,handler):
self.handlers.append(handler)
return self
d = Delegate()
def handler1(a,b):print a,b
def handler2(a,b):print a+b
d(1,2)
</pre>
也許這個可以幫助理解? 呵呵
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2006-10-12 16:59 by wthorse
但是它不能使用帶參數的函數,怎么辦?
可以在調用的target 所在的類中定義屬性,調用多線程之前先賦值。
# re: Delegate 比較全面的例子(原創) 回復 更多評論
2007-02-11 16:03 by 臭石頭
多線程傳遞參數,我經常用,呵呵。
寫一個類來包裝,寫一個沒有參數的方法,方法內部調用帶參數的方法,這些參數,作為這個類的公共成
員。
聲明這個類的一個實例,然后……剩下的不說了
# 對Delegate 的理解[TrackBack] 回復 更多評論
2007-03-20 18:06 by 9q
下面這篇文章論述的比較有意思:Delegate 比較全面的例子(原創) 查看原文
System.ComponentModel.AsyncOperation 類 - 這個類很特別昨天在嘗試使用
System.ComponentModel.BackgroundWorker 時,發現這個類的行為和我預料的大不一樣,可以說是驚喜。
原來以為這個類只是一個線程的簡單包裝,用多線程模擬了異步調用而已;但是看下面的這段代碼:
Thread.CurrentThread.Name = "Main Thread";
backgroundWorker1.RunWorkerAsync();
...
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
int i = 0;
while (i++ < 100)
{
backgroundWorker1.ReportProgress(i);
Thread.Sleep(50);
}
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.Text = e.ProgressPercentage + "% - " + Thread.CurrentThread.Name;
}
private void backgroundWorker1_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
this.Text = "DONE - " + Thread.CurrentThread.Name;
}
毫無疑問,_DoWork 方法是運行在另一個不同線程之上的(很容易驗證這一點,這也符合BackgroundWorker
的設計),而這個方法又調用了backgroundWorker1.ReportProgress 方法,觸發了ProgressChanged 事件。
在通常的異步實現,_ProgressChanged 方法應該運行于事件觸發者相同的線程中;但在這里,它運行于主線
程(名為Main Thread 的線程)。_RunWorkerCompleted 方法也是一樣。
在我看來,這個行為非常特別,實際上它也非常有用。這樣_ProgressChanged 方法體中操作UI 控件的代碼都
無需使用Control.Invoke 包裝了,讓程序的編寫大為簡化。而我真正感興趣的是這個類究竟是怎么實現的,我
用Reflector 打開它的源碼之后,原來關鍵在于它用到了一個名為AsyncOperation 的類
(System.ComponentModel.AsyncOperation)。
AsyncOperation 類有個Post 方法,可以用來把一個委托(作為方法指針/列表)提交給另一個線程執行。繼續
反編譯下去,又查到了System.Threading.SynchronizationContext 類。不過具體怎么實現是無從得知了,
因為追蹤到最后,停在了一個[MethodImpl(MethodImplOptions.InternalCall)]的方法,它由CLR 本身實現。
(我個人猜測,其中很可能利用了Windows API:Get/SetThreadContext,和結構體CONTEXT,改掉了線
程上下文。)
退一步說,它怎么實現的并不是那么重要,重要的是我們可以用這個AsyncOperation 類實現自己的
BackgroundWorker。這里是我寫的和上面代碼基本等價的實現:
AsyncOperation asyncOperation;
SendOrPostCallback progressReporter;
Thread workerThread;
public MainForm()
{
InitializeComponent();
asyncOperation = AsyncOperationManager.CreateOperation(null);
progressReporter = new SendOrPostCallback(ReportProgress);
workerThread = new Thread(new ThreadStart(WorkOnWorkerThread));
}
private void MainForm_Load(object sender, EventArgs e)
{
Thread.CurrentThread.Name = "Main Thread";
workerThread.Name = "Worker Thread";
workerThread.IsBackground = true;
workerThread.Start();
}
void ReportProgress(object obj)
{
this.Text = obj.ToString() + "% - " + Thread.CurrentThread.Name;
}
void WorkOnWorkerThread()
{
int i = 0;
while (i++ < 100)
{
asyncOperation.Post(progressReporter, i);
Thread.Sleep(50);
}
}
C#事件編程 - 五年 - 博客園 wxy
1.定義一個 "代表"
如:
public delegate void DisconnectedEventHandler(object
sender,ClientEventArgs e);
2.定義事件參數
如:
public class ClientEventArgs : EventArgs
{
public IPAddress IP
{
get { return ( (IPEndPoint)this.socket.RemoteEndPoint
).Address; }
}
public int Port
{
get{return ((IPEndPoint)this.socket.RemoteEndPoint).Port;}
}
public ClientEventArgs(Socket clientManagerSocket)
{
this.socket = clientManagerSocket;
}
}
3.使用"代表"定義一個事件
public event DisconnectedEventHandler Disconnected;
4.觸發事件
protected virtual void OnDisconnected(ClientEventArgs e)
{
if ( Disconnected != null )
Disconnected(this , e);
}
this.OnDisconnected(new ClientEventArgs(this.socket));
5.使用事件
ClientManager newClientManager = new ClientManager(socket);
newClientManager.Disconnected += new
DisconnectedEventHandler(ClientDisconnected);
6.定義事件處理方法
void ClientDisconnected(object sender , ClientEventArgs e)
{
if ( this.RemoveClientManager(e.IP) )
this.UpdateConsole("Disconnected." , e.IP , e.Port);
}
關于.NET 異步調用的初步總結
最近看了看.NET 異步調用方面的資料,現擇重點總結,若有紕漏敬請指正。
異步調用的實質:
異步調用通過委托將所需調用的方法置于一個新線程上運行,從而能夠使一個可能需要較長時間的任務在后臺執
行而不影響調用方的其他行為。
異步調用的實現:
前面已經講道,異步調用通過委托實現。委托支持同步和異步調用。在同步調用中,一個委托的實例可記錄多個
目標方法;在異步調用中,一個委托實例中有且只能包含一個目標方法。異步調用使用委托實例的BeginInvoke
方法和EndInvoke 方法分別開始調用和檢索返回值,這兩個方法在編譯期生成。調用BeginInvoke 后委托立
即返回;調用EndInvoke 時倘若委托方法未執行完畢,則阻塞當前線程至調用完畢。
假設有一個委托
public delegate int ASyncHandler(int a,string b,ref string c);
那么,其BeginInvoke 與EndInvoke 的形式如下:
public IAsyncResult BeginInvoke(int a,string b,ref string c,AsyncCallback callback,object
asyncState);
public int EndInvoke(ref string c,IAsyncResult asyncResult);
也就是說,BeginInvoke 與EndInvoke 的參數列表與當前委托簽名有關,可以總結為:
public IAsyncResult BeginInvoke(委托所具有的全部參數,AsyncCallback callback,object asyncState);
public 委托返回值 EndInvoke(委托參數中ref/out 部分,IAsyncResult asyncResult);
BeginInvoke 返回一個IAsyncResult,其實質是實現IAsyncResult 的
System.Runtime.Remoting.Messaging.AsyncResult 類。該對象相當于一個“憑證”,在調用EndInvoke
時用于確認應等待返回的方法(猜測如此)。就像去銀行,存錢時拿到一份存折(憑證),取款時依據存折(憑證)
取款。
EndInvoke 檢索委托返回值,并返回標有ref/out 的參數值。
IAsyncResult 接口聲明:
public interface IAsyncResult
{
object AsyncState{get;}
WaitHandle AsyncWaitHandle{get;}
bool CompletedSynchronously{get;}
bool IsCompleted{get;}
}
等待調用結束的三種方法:
1、使用EndInvoke 主動等待異步調用結束。這是最簡單的一種方法,適用于非用戶界面程序及一些IO 操作,
因為在調用EndInvoke 之后當前線程被阻塞,除了等待什么都不能做。
2、使用WaitHandle 等待異步調用結束。IAsyncResult 中有WaitHandle 成員,獲取用于等待異步操作完成
的WaitHandle,即調用結束信號。使用WaitHandle.WaitOne()可以阻塞當前線程至異步調用完成。這樣做
的好處是:在調用WaitOne 之后、EndInvoke 之前,可以執行其他處理。
3、主動輪詢。使用IAsyncResult 中有IsCompleted 成員檢索當前異步調用情況。該方法適用于用戶界面程
序,想象可在一個循環內做到既等待委托完成,又可以更新用戶界面。
4、使用回調,在異步調用結束時執行一個操作。前面的BeginInvoke 方法簽名的最后兩個參數用于回調。需
要用到AsyncCallback 委托:
public delegate void AsyncCallback(IAsyncResult asyncResult);
回調方法在系統線程池中執行。BeginInvoke 的最后一個參數(object asyncState)可以傳遞包含回調方法
將要使用的信息的對象。在回調方法中調用EndInvoke 可以通過取得
System.Runtime.Remoting.Messaging.AsyncResult.AsyncDelegate 實現。
個人認為方法1、2 相差不算太大。
先寫這么些,以后再補上其他的一些東西。
Mcad 學習筆記之異步編程(AsyncCallback 委托,IAsyncResult 接口,BeginInvoke 方法,EndInvoke 方法的
使用小總結) - aierong 原創技術隨筆(.Net 方向應用) - 博客園
Mcad 學習筆記之異步編程(AsyncCallback 委托,IAsyncResult 接口,BeginInvoke 方法,EndInvoke
方法的使用小總結)
Posted on 2005-05-25 17:47 aierong 閱讀(4162) 評論(3) 編輯 收藏 引用 網摘 所屬分類:
MCAD 學習
讓我們來看看同步異步的區別:
同步方法調用在程序繼續執行之前需要等待同步方法執行完畢返回結果
異步方法則在被調用之后立即返回以便程序在被調用方法完成其任務的同時執行其它操作
.NET 框架基類庫中有好幾種類都可以提供同步和異步的方法調用。
因為同步方法調用會導致程序流程中途等待,所以采用同步方法的情況下往往會導致程序執行的延遲
相比來說,在某些條件下選擇異步方法調用就可能更好一些
例如,有的時候程序需要給多個Web 服務發出請求,還有遠程處理信道(HTTP、TCP)和代理,這時就
最好采用異步方法
.NET Framework 允許異步調用任何方法,定義與需要調用的方法具有相同簽名的委托
CLR 將自動為該委托定義添加適當簽名的BeginInvoke 虛方法和EndInvoke 虛方法和Invoke 方法。
關于委托的這3 個方法的詳細說明可以參考這文章
http://www.cnblogs.com/aierong/archive/2005/05/25/162181.html
我們先來了解這2 個方法和一個委托和一個接口:
(1)
BeginInvoke 方法用于啟動異步調用
它與您需要異步執行的方法具有相同的參數,只不過還有兩個額外的參數,將 AsyncCallback 和
AsyncState(可通過
IAsyncResult 接口的
AsyncState 屬性獲得)作為最后兩個參數,如沒有可以為空.
BeginInvoke 立即返回,不等待異步調用完成。
BeginInvoke 返回IasyncResult,可用于監視調用進度。
結果對象IAsyncResult 是從開始操作返回的,并且可用于獲取有關異步開始操作是否已完成的狀態。
結果對象被傳遞到結束操作,該操作返回調用的最終返回值。
在開始操作中可以提供可選的回調。如果提供回調,在調用結束后,將調用該回調;并且回調中的代碼可
以調用結束操作。
(2)
EndInvoke 方法用于檢索異步調用結果。
在調用BeginInvoke 后可隨時調用EndInvoke 方法,注意:始終在異步調用完成后調用EndInvoke.
如果異步調用未完成,EndInvoke 將一直阻塞到異步調用完成。
EndInvoke 的參數包括需要異步執行的方法的out 和ref 參數以及由BeginInvoke 返回的
IAsyncResult。
要注意的是,始終在異步調用完成后調用EndInvoke
(3)
AsyncCallback 委托用于指定在開始操作完成后應被調用的方法
AsyncCallback 委托被作為開始操作上的第二個到最后一個參數傳遞
代碼原型如下:
[Serializable]
public delegate void AsyncCallback(IAsyncResult ar);
(4)
IAsyncResult 接口
它表示異步操作的狀態.
該接口定義了4 個公用屬性
實際上,發起和完成.NET 異步調用有4 種方案可供你選擇
1.方案1-自己調用EndInvoke 方法
異步執行方法的最簡單方式是以BeginInvoke 開始,對主線程執行一些操作,然后調用
EndInvoke,EndInvoke 直到異步調用完成后才返回
還是先來段自己喜歡的控制臺代碼:
1using System;
2
3namespace ConsoleApplication1
4{
5 class Class1
6 {
7 public delegate void AsyncEventHandler();
8
9 void Event1()
10 {
11 Console.WriteLine("Event1 Start");
12 System.Threading.Thread.Sleep(2000);
13 Console.WriteLine("Event1 End");
14 }
15
16 void Event2()
17 {
18 Console.WriteLine("Event2 Start");
19 int i=1;
20 while(i<1000)
21 {
22 i=i+1;
23 Console.WriteLine("Event2 "+i.ToString());
24 }
25 Console.WriteLine("Event2 End");
26 }
27
28 void CallbackMethod(IAsyncResult ar)
29 {
30 ((AsyncEventHandler) ar.AsyncState).EndInvoke(ar);
31 }
34 [STAThread]
35 static void Main(string[] args)
36 {
37 long start=0;
38 long end=0;
39 Class1 c = new Class1();
40 Console.WriteLine("ready");
41 start=DateTime.Now.Ticks;
42
43 AsyncEventHandler asy = new AsyncEventHandler(c.Event1);
44 IAsyncResult ia=asy.BeginInvoke(null,null);
45 c.Event2();
46 asy.EndInvoke(ia);
47
48 end =DateTime.Now.Ticks;
49 Console.WriteLine("時間刻度差="+ Convert.ToString(end-start) );
50 Console.ReadLine();
51 }
52 }
53}
54
此程序簡單,異步的處理過程在代碼43-46 這幾行
結果如下:
現在讓我們來看看同步處理
修改代碼43-46 這幾行代碼:
c.Event1();
c.Event2();
結果如下:
前者的時間刻度大大小于后者
我們可以明顯地看到異步運行的速度優越性
2.方案2-采用查詢(IsCompleted 屬性)
IAsyncResult.IsCompleted 屬性獲取異步操作是否已完成的指示,發現異步調用何時完成.
再次修改代碼43-46 這幾行代碼:
AsyncEventHandler asy = new AsyncEventHandler(c.Event1);
IAsyncResult ia=asy.BeginInvoke(null,null);
c.Event2();
while(!ia.IsCompleted)
{
}
asy.EndInvoke(ia);
3.方案3-采用AsyncWaitHandle 來等待方法調用的完成
IAsyncResult.AsyncWaitHandle 屬性獲取用于等待異步操作完成的WaitHandle
WaitHandle.WaitOne 方法阻塞當前線程,直到當前的WaitHandle 收到信號
使用WaitHandle,則在異步調用完成之后,但在通過調用EndInvoke 結果之前,可以執行其他處理
再次修改代碼43-46 這幾行代碼:
AsyncEventHandler asy = new AsyncEventHandler(c.Event1);
IAsyncResult ia=asy.BeginInvoke(null,null);
c.Event2();
ia.AsyncWaitHandle.WaitOne();
4.方案4-利用回調函數
如果啟動異步調用的線程不需要處理調用結果,則可以在調用完成時執行回調方法
要使用回調方法,必須將代表該方法的AsyncCallback 委托傳遞給BeginInvoke
再次修改代碼43-46 這幾行代碼:
AsyncEventHandler asy = new AsyncEventHandler(c.Event1);
asy.BeginInvoke(new AsyncCallback(c.CallbackMethod),asy);
c.Event2();
希望上面提到的知識對你有所提示
當然歡迎交流和指正
blog:
http://www.cnblogs.com/aierong
author:aierong
email:[email protected]
Feedback
# re:
Mcad 學習筆記之異步編程(AsyncCallback 委托,IAsyncResult 接口,BeginInvoke 方法,EndInvoke
方法的使用小總結)
回復 更多評論
2005-05-26 09:14 by eric
問個題外話,考Mcsd for .net 怎么報名?準備呢? 微軟中文認證主頁上,找不到相關的信息啊;能給些
相關的資源嗎?謝謝。
# re:
Mcad 學習筆記之異步編程(AsyncCallback 委托,IAsyncResult 接口,BeginInvoke 方法,EndInvoke
方法的使用小總結)
回復 更多評論
2005-05-26 09:59 by aierong
to:eric
我也沒考過
具體的我也不知道
http://www.examlink.com/
你去看看
# 使用委托進行異步編程[TrackBack] 回復 更多評論
2007-04-19 10:50 by 落葉
1.什么是異步編程使用.NET 異步編程,在程序繼續執行的同時對.NET 類方法進行調用,直到進行指定的
回調為止;如果沒有提供回調,則直到對調用的阻塞、輪詢或等待完成為止。例如,一個程序可以調用一...
查看原文
UI 設計注意點
軟件的智能和記憶功能
1.用戶登錄界面最好有用戶名和ID 的記憶,焦點直接定位到密碼輸入框
2.單據錄入界面最好有保存和載入默認值的功能
3.單據搜索界面可以保存用戶自定義的各種搜索條件組合
4.用戶調整過的GRID 的列寬,窗口的位置可以自動記憶
5.系統可以根據用戶的使用頻度對相關功能進行自動的優先級排序
6.系統能夠記憶不同用戶的使用偏好,使用系統的固有模式和常用的自定義設置
減少不必要的重復交互
1.減少不必要的各種操作,能夠點一次鼠標或敲一次鍵盤完成的絕不作出兩次或多次。
2.提示信息要適度,太多不好,太少也不好。
3.數據項完整性校驗問題要注意光標焦點自動定位到錯誤處
4.完整業務功能不要讓用戶在多個窗口切換多次才能夠完成。盡量減少這種切換。
5.為了方便用戶切換窗口,相關的表單最好都作為非模式的形式。
6.相同的信息不要讓用戶在系統中多處或多次錄入,保證入口的唯一性
7.系統要盡可能根據用戶已經錄入信息自動獲取其它附屬信息,而不需要用戶重復的選擇或錄
入。
導航和界面跳轉
1.表單新彈出對話框,對話框再彈出對話框的這種層次要控制在3 層以內。
2.所有的非模式活動窗口最好有類似桌面任務欄一樣的停靠方式,方便切換窗口
3.系統可以支持用戶自己定義常用功能和菜單
4.對于常用功能應該提供便捷的快捷鍵和工具欄按鈕
5.對于系統中提供的各種業務和表單功能能夠讓用戶便捷挑轉到幫助信息上
6.對表單和界面聯動和交互的時候要注意相關界面數據的自動刷新
7.一個窗口中最多不要出現超過三個的GRID 控件
8.BS 方式不要左右滾屏。CS 模式既要避免左右滾屏也要避免上下滾屏
9.需要根據業務查看需求和數據的展現需求來選擇合適的界面控件
系統性能和健壯性方面的
1.系統中相關的耗時操作都必須必須轉變鼠標為等待狀態
2.系統耗時操作超過30 秒的最好能夠提供給用戶相關的進度條功能
3.系統耗時功能超過2 分鐘的最好能夠設計為異步多線程的方式進行處理
4.系統應用有友好的完整性和約束校驗的提示信息,方便用戶修改錄入數據
5.在系統出現異常情況下應該有友好的統一的提示信息,同時后臺應該記錄詳細的異常日志
界面友好性和易用性方面的
1.表單應該能夠根據屏幕分辯率自動適應。在界面上讓用戶一次能夠看到足夠多的信息
2.表單應該支持Tab 鍵功能,順序為從左到右,從上到下。
3.常用的表單應該同時支持鍵盤操作和鼠標操作。
4.界面上控件的布局應該間距適當,標簽和控件對齊,有適當的錄入提示信息。
5.界面的配色應該盡量簡單,盡量少使用各種刺眼的顏色
6.用戶看到表單后應該就基本清楚相關功能,表單要盡量自我解釋,不要設計過多的隱含在界面
里面功能
數據的錄入和檢索
1.根據業務需要選擇適合的數據錄入控件
2.數據錄入控件應該有完備的數據完整性和一致性校驗功能
3.系統應該提供用戶暫時保存錄入數據的功能
4.能夠自動獲取數據不要讓用戶再去錄入,能夠選擇錄入數據不要讓用戶手工錄入
5.數據檢索條件應該適中,不應太多也不應太少。檢索支持組合條件檢索。
6.為了滿足不同需求檢索可以提供簡單檢索和高級檢索多種方式。
7.應該在第一時間提供給用戶檢索數據,因此檢索功能存在性能問題時候要考慮分頁。
8.在檢索功能較耗時的時候應該提供給用戶相關的進度條顯示進度
9.表格最好能夠提供行顯示和列顯示等多種顯示模式,方面用戶查看數據
C#是一門支持多線程的語言,因此線程的使用也是比較常見的。由于線程的知識在Win32 編程的時候已經說得
過多,所以在.Net 中很少介紹這部分(可能.Net 不覺得這部分是它所特有的)。
那么線程相關的問題大致有如下四類(這篇文章只討論單線程、單線程與UI 線程這兩方面的問題)。
問題一,線程的基本操作,例如:暫停、繼續、停止等;
問題二,如何向線程傳遞參數或者從中得到其返回值;
問題三,如何使線程所占用的CPU 不要老是百分之百;
最后一個,也是問題最多的,就是如何在子線程來控制UI 中的控件,換句話說,就是在線程中控制窗體某些控
件的顯示。
對于問題一,我不建議使用Thread 類提供的Suspend、Resume 以及Abort 這三個方法,前兩個有問題,好
像在VS05 已經屏蔽這兩個方法;對于Abort 來說,除了資源沒有得到及時釋放外,有時候會出現異常。如何
做呢,通過設置開關變量來完成。
對于問題二,我不建議使用靜態成員來完成,僅僅為了線程而破壞類的封裝有些得不償失。那如何做呢,通過創
建單獨的線程類來完成。
對于問題三來說,造成這個原因是由于線程中進行不間斷的循環操作,從而使CPU 完全被子線程占有。那么處
理此類問題,其實很簡單,在適當的位置調用Thread.Sleep(20)來釋放所占有CPU 資源,不要小看這20 毫
秒的睡眠,它的作用可是巨大的,可以使其他線程得到CPU 資源,從而使你的CPU 使用效率降下來。
看完前面的三個問題的解釋,對于如何做似乎沒有給出一個明確的答案,為了更好地說明如何解決這三個問題,
我用一個比較完整的例子展現給大家,代碼如下。
//--------------------------- Sub-thread class ---------------------------------------
//------------------------------------------------------------------------------------
//---File: clsSubThread
//---Description: The sub-thread template class file
//---Author: Knight
//---Date: Aug.21, 2006
//------------------------------------------------------------------------------------
//---------------------------{Sub-thread class}---------------------------------------
namespace ThreadTemplate
{
using System;
using System.Threading;
using System.IO;
/// <summary>
/// Summary description for clsSubThread.
/// </summary>
public class clsSubThread:IDisposable
{
private Thread thdSubThread = null;
private Mutex mUnique = new Mutex();
private bool blnIsStopped;
private bool blnSuspended;
private bool blnStarted;
private int nStartNum;
public bool IsStopped
{
get{ return blnIsStopped; }
}
public bool IsSuspended
{
get{ return blnSuspended; }
}
public int ReturnValue
{
get{ return nStartNum;}
}
public clsSubThread( int StartNum )
{
//
// TODO: Add constructor logic here
//
blnIsStopped = true;
blnSuspended = false;
blnStarted = false;
nStartNum = StartNum;
}
/// <summary>
/// Start sub-thread
/// </summary>
public void Start()
{
if( !blnStarted )
{
thdSubThread = new Thread( new ThreadStart( SubThread ) );
blnIsStopped = false;
blnStarted = true;
thdSubThread.Start();
}
}
/// <summary>
/// Thread entry function
/// </summary>
private void SubThread()
{
do
{
// Wait for resume-command if got suspend-command here
mUnique.WaitOne();
mUnique.ReleaseMutex();
nStartNum++;
Thread.Sleep(1000); // Release CPU here
}while( blnIsStopped == false );
}
/// <summary>
/// Suspend sub-thread
/// </summary>
public void Suspend()
{
if( blnStarted && !blnSuspended )
{
blnSuspended = true;
mUnique.WaitOne();
}
}
/// <summary>
/// Resume sub-thread
/// </summary>
public void Resume()
{
if( blnStarted && blnSuspended )
{
blnSuspended = false;
mUnique.ReleaseMutex();
}
}
/// <summary>
/// Stop sub-thread
/// </summary>
public void Stop()
{
if( blnStarted )
{
if( blnSuspended )
Resume();
blnStarted = false;
blnIsStopped = true;
thdSubThread.Join();
}
}
#region IDisposable Members
/// <summary>
/// Class resources dispose here
/// </summary>
public void Dispose()
{
// TODO: Add clsSubThread.Dispose implementation
Stop();//Stop thread first
GC.SuppressFinalize( this );
}
#endregion
}
}
那么對于調用呢,就非常簡單了,如下:
// Create new sub-thread object with parameters
clsSubThread mySubThread = new clsSubThread( 5 );
mySubThread.Start();//Start thread
Thread.Sleep( 2000 );
mySubThread.Suspend();//Suspend thread
Thread.Sleep( 2000 );
mySubThread.Resume();//Resume thread
Thread.Sleep( 2000 );
mySubThread.Stop();//Stop thread
//Get thread's return value
Debug.WriteLine( mySubThread.ReturnValue );
//Release sub-thread object
mySubThread.Dispose();
在回過頭來看看前面所說的三個問題。
對于問題一來說,首先需要局部成員的支持,那么
private Mutex mUnique = new Mutex();
private bool blnIsStopped;
private bool blnSuspended;
private bool blnStarted;
光看成員名稱,估計大家都已經猜出其代表的意思。接下來需要修改線程入口函數,要是這些開關變量能發揮作
用,那么看看SubThread 這個函數。
/// <summary>
/// Thread entry function
/// </summary>
private void SubThread()
{
do
{
// Wait for resume-command if got suspend-command here
mUnique.WaitOne();
mUnique.ReleaseMutex();
nStartNum++;
Thread.Sleep(1000);
}while( blnIsStopped == false );
}
函數比較簡單,不到十句,可能對于“blnIsStopped == false”這個判斷來說,大家還比較好理解,這是一個
普通的判斷,如果當前Stop 開關打開了,就停止循環;否則一直循環。
大家比較迷惑的可能是如下這兩句:
mUnique.WaitOne();
mUnique.ReleaseMutex();
這兩句的目的是為了使線程在Suspend 操作的時候能發揮效果,為了解釋這兩句,需要結合Suspend 和
Resume 這兩個方法,它倆的代碼如下。
/// <summary>
/// Suspend sub-thread
/// </summary>
public void Suspend()
{
if( blnStarted && !blnSuspended )
{
blnSuspended = true;
mUnique.WaitOne();
}
}
/// <summary>
/// Resume sub-thread
/// </summary>
public void Resume()
{
if( blnStarted && blnSuspended )
{
blnSuspended = false;
mUnique.ReleaseMutex();
}
}
為了更好地說明,還需要先簡單說說Mutex 類型。對于此類型對象,當調用對象的WaitOne 之后,如果此時沒
有其他線程對它使用的時候,就立刻獲得信號量,繼續執行代碼;當再調用ReleaseMutex 之前,如果再調用
對象的WaitOne 方法,就會一直等待,直到獲得信號量的調用ReleaseMutex 來進行釋放。這就好比衛生間的
使用,如果沒有人使用則可以直接使用,否則只有等待。
明白了這一點后,再來解釋這兩句所能出現的現象。
mUnique.WaitOne();
mUnique.ReleaseMutex();
當在線程函數中,執行到“mUnique.WaitOne();”這一句的時候,如果此時外界沒有發送Suspend 消息,也
就是信號量沒有被占用,那么這一句可以立刻返回。那么為什么要緊接著釋放呢,因為不能總占著信號量,立即
釋放信號量是避免在發送Suspend 命令的時候出現等待;如果此時外界已經發送了Suspend 消息,也就是說
信號量已經被占用,此時“mUnique.WaitOne();”不能立刻返回,需要等到信號量被釋放才能繼續進行,也
就是需要調用Resume 的時候,“mUnique.WaitOne();”才能獲得信號量進行繼續執行。這樣才能達到真正
意義上的Suspend 和Resume。
至于線程的Start 和Stop 來說,相對比較簡單,這里我就不多說了。
現在再來分析一下問題二,其實例子比較明顯,是通過構造函數和屬性來完成參數和返回值,這一點我也不多說
了。如果線程參數比較多的話,可以考慮屬性來完成,類似于返回值。
問題三,我就更不用多說了。有人說了,如果子線程中的循環不能睡眠怎么辦,因為睡眠的話,有時會造成數據
丟失,這方面的可以借鑒前面Suspend 的做法,如果更復雜,則牽扯到多線程的同步問題,這部分我會稍后單
獨寫一篇文章。
前三個問題解決了,該說說最常見的問題,如何在子線程中控制窗體控件。這也是寫線程方面程序經常遇到的,
其實我以前寫過兩篇文章,都對這方面做了部分介紹。那么大家如果有時間的話,不妨去看看。
http://blog.csdn.net/knight94/archive/2006/03/16/626584.aspx
http://blog.csdn.net/knight94/archive/2006/05/27/757351.aspx
首先說說,為什么不能直接在子線程中操縱UI 呢。原因在于子線程和UI 線程屬于不同的上下文,換句比較通俗
的話說,就好比兩個人在不同的房間里一樣,那么要你直接操作另一個房間里的東西,恐怕不行罷,那么對于子
線程來說也一樣,不能直接操作UI 線程中的對象。
那么如何在子線程中操縱UI 線程中的對象呢,.Net 提供了Invoke 和BeginInvoke 這兩種方法。簡單地說,
就是子線程發消息讓UI 線程來完成相應的操作。
這兩個方法有什么區別,這在我以前的文章已經說過了,Invoke 需要等到所調函數的返回,而BeginInvoke
則不需要。
用這兩個方法需要注意的,有如下三點:
第一個是由于Invoke 和BeginInvoke 屬于Control 類型的成員方法,因此調用的時候,需要得到Control 類
型的對象才能觸發,也就是說你要觸發窗體做什么操作或者窗體上某個控件做什么操作,需要把窗體對象或者控
件對象傳遞到線程中。
第二個,對于Invoke 和BeginInvoke 接受的參數屬于一個delegate 類型,我在以前的文章中使用的是
MethodInvoker,這是.Net 自帶的一個delegate 類型,而并不意味著在使用Invoke 或者BeginInvoke 的時
候只能用它。參看我給的第二篇文章(《如何彈出一個模式窗口來顯示進度條》),會有很多不同的delegate 定義。
最后一個,使用Invoke 和BeginInvoke 有個需要注意的,就是當子線程在Form_Load 開啟的時候,會遇到
異常,這是因為觸發Invoke 的對象還沒有完全初始化完畢。處理此類問題,在開啟線程之前顯式的調用
“this.Show();”,來使窗體顯示在線程開啟之前。如果此時只是開啟線程來初始化顯示數據,那我建議你不要
使用子線程,用Splash 窗體的效果可能更好。這方面可以參看如下的例子。
http://www.syncfusion.com/FAQ/WindowsForms/FAQ_c95c.aspx#q621q
線程的四個相關問題已經說完了,這篇文章只說了單線程,以及單線程與UI 線程交互的問題。其中涉及到的方
法不一定是唯一的,因為.Net 還提供了其他類來扶助線程操作,這里就不一一羅列。至于多線程之間的同步,我
會稍后專門寫篇文章進行描述。
VS2005 中更新其他線程訪問界面線程控件的方法
作者:Depraved_Survival | 錄入時間:2007-9-4 | 點擊:17 次 打印此文章 | 字體:大 中 小
VS2005 中,界面線程中的控件如果由其他線程進行更新時,編譯器會自動拋出異常來避免這種不安全的
跨線程訪問方式。解決這個問題的一種方法是在界面線程中編寫控件內容更新程序,并聲明委托,利用 Invoke
方法進行實現。具體實現方法如下。
假設我們的 From1 中有一個 TextBox 空間,名為 txtBox,From1 在 Load 的時候啟動一個線程
thread,線程處理函數為 threadProc,在該線程中將 Hello the world 寫入 txtBox 控件中。
對于上面的問題,在以往 VS2003 中我們只需在 thread 線程的處理函數中加入
this.txtBox.Text="Hello the world!"即可,而在 VS2005 中同樣的寫法編譯器會拋出異常。在 VS2005 中
正確的調用方法是先聲明用于更新空間的委托,在定義一個更新控件用的方法,然后在線程函數中通過 From1
的 InvokeRequired 方法判斷是否需要采用 Invoke 方式,如果需要使用 Invoke 方式訪問,否則采用
VS2003 中的方式更新控件內容,代碼如下:
//聲明更新控件用的代理委托
protected delegate void UpdateControlText(string strText);
//定義更新控件的方法
protected void updateControlText(string strText)
{
txtBox.Text = strText;
return;
}
需要說明的是,上述委托和方法為 From1 對應類對象的成員,委托的參數必須與方法的參數完全相同。
在線程中需要更新控件的位置中添加如下代碼:
if (this.InvokeRequired)
{
UpdateControlText update = new UpdateControlText(updateControlText);
this.Invoke(update, "Hello the world!");
}
else
{
this.txtBox.Text="Hello the world!";
}
Invoke 方法中第一個參數為委托類型的對象,構造該對象的時候采用 updateControlText 作為構造參數。從
第二個參數開始為一個 params object [] objParams 的隊列,可以容納任意多任意類型的參數,但是針對每
次調用來說必須要指定類型個數與 UpdateControlText 完全相同的參數表。雖然參數不同時編譯器不會報錯,
但是會在運行中彈出異常。
借助WebService 實現多線程上傳文件 - 愚翁專欄 - CSDNBlog 正在處理您的請求...
借助WebService 實現多線程上傳文件
在WebService 的幫助下,進行多線程上傳文件是非常簡單。因此我只做個簡單的例子,那么如果想要實現此功
能的朋友,可以在我的基礎上進行擴展。
首先說說服務器端,只需要提供一個能允許多線程寫文件的函數即可,具體代碼如下。
[WebMethod]
public bool UploadFileData( string FileName, int StartPosition, byte[] bData )
{
string strFullName = Server.MapPath( "Uploads" ) + @"/" + FileName;
FileStream fs = null;
try
{
fs = new FileStream( strFullName, FileMode.OpenOrCreate,
FileAccess.Write, FileShare.Write );
}
catch( IOException err )
{
Session["ErrorMessage"] = err.Message;
return false;
}
using( fs )
{
fs.Position = StartPosition;
fs.Write( bData, 0, bData.Length );
}
return true;
}
其中“Uploads”是在服務程序所在目錄下的一個子目錄,需要設置ASPNET 用戶對此目錄具有可寫權限。
相對于服務器端來說,客戶端要稍微復雜一些,因為要牽扯到多線程的問題。為了更好的傳遞參數,我用一個線
程類來完成。具體如下。
public delegate void UploadFileData( string FileName, int StartPos, byte[]
bData );
/// <summary>
/// FileThread: a class for sub-thread
/// </summary>
sealed class FileThread
{
private int nStartPos;
private int nTotalBytes;
private string strFileName;
public static UploadFileData UploadHandle;
/// <summary>
/// Constructor
/// </summary>
/// <param name="StartPos"></param>
/// <param name="TotalBytes"></param>
/// <param name="FileName"></param>
public FileThread( int StartPos, int TotalBytes, string FileName )
{
//Init thread variant
nStartPos = StartPos;
nTotalBytes = TotalBytes;
strFileName = FileName;
//Only for debug
Debug.WriteLine( string.Format( "File name:{0} position: {1} total
byte:{2}",
strFileName, nStartPos, nTotalBytes ) );
}
/// <summary>
/// Sub-thread entry function
/// </summary>
/// <param name="stateinfo"></param>
public void UploadFile( object stateinfo )
{
int nRealRead, nBufferSize;
const int BUFFER_SIZE = 10240;
using( FileStream fs = new FileStream( strFileName,
FileMode.Open, FileAccess.Read,
FileShare.Read ) )
{
string sName = strFileName.Substring( strFileName.LastIndexOf(
"http://" ) + 1 );
byte[] bBuffer = new byte[BUFFER_SIZE];//Init 10k buffer
fs.Position = nStartPos;
nRealRead = 0;
do
{
nBufferSize = BUFFER_SIZE;
if( nRealRead + BUFFER_SIZE > nTotalBytes )
nBufferSize = nTotalBytes - nRealRead;
nBufferSize = fs.Read( bBuffer, 0, nBufferSize );
if( nBufferSize == BUFFER_SIZE )
UploadHandle( sName,
nRealRead + nStartPos,
bBuffer );
else if( nBufferSize > 0 )
{
//Copy data
byte[] bytData = new byte[nBufferSize];
Array.Copy( bBuffer,0, bytData, 0, nBufferSize );
UploadHandle( sName,
nRealRead + nStartPos,
bytData );
}
nRealRead += nBufferSize;
}
while( nRealRead < nTotalBytes );
}
//Release signal
ManualResetEvent mr = stateinfo as ManualResetEvent;
if( mr != null )
mr.Set();
}
}
那么在執行的時候,要創建線程類對象,并為每一個每個線程設置一個信號量,從而能在所有線程都結束的時候
得到通知,大致的代碼如下。
FileInfo fi = new FileInfo( txtFileName.Text );
if( fi.Exists )
{
btnUpload.Enabled = false;//Avoid upload twice
//Init signals
ManualResetEvent[] events = new ManualResetEvent[5];
//Devide blocks
int nTotalBytes = (int)( fi.Length / 5 );
for( int i = 0; i < 5; i++ )
{
events[i] = new ManualResetEvent( false );
FileThread thdSub = new FileThread(
i * nTotalBytes,
( fi.Length - i * nTotalBytes ) > nTotalBytes ?
nTotalBytes:(int)( fi.Length - i * nTotalBytes ),
fi.FullName );
ThreadPool.QueueUserWorkItem( new WaitCallback( thdSub.UploadFile ),
events[i] );
}
//Wait for threads finished
WaitHandle.WaitAll( events );
//Reset button status
btnUpload.Enabled = true;
}
總體來說,程序還是相對比較簡單,而我也只是做了個簡單例子而已,一些細節都沒有進行處理。
本來想打包提供給大家下載,沒想到CSDN 的Blog 對于這點做的太差,老是異常。
如下是客戶端的完整代碼。
//--------------------------- Multi-thread Upload Demo
---------------------------------------
//--------------------------------------------------------------------------------------------
//---File: frmUpload
//---Description: The multi-thread upload form file to demenstrate howto use
multi-thread to
// upload files
//---Author: Knight
//---Date: Oct.12, 2006
//--------------------------------------------------------------------------------------------
//---------------------------{Multi-thread Upload
Demo}---------------------------------------
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
namespace CSUpload
{
using System.IO;
using System.Diagnostics;
using System.Threading;
using WSUploadFile;//Web-service reference namespace
/// <summary>
/// Summary description for Form1.
/// </summary>
public class frmUpload : System.Windows.Forms.Form
{
private System.Windows.Forms.TextBox txtFileName;
private System.Windows.Forms.Button btnBrowse;
private System.Windows.Forms.Button btnUpload;
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;
public frmUpload()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
//
// TODO: Add any constructor code after InitializeComponent call
//
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.txtFileName = new System.Windows.Forms.TextBox();
this.btnBrowse = new System.Windows.Forms.Button();
this.btnUpload = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// txtFileName
//
this.txtFileName.Location = new System.Drawing.Point(16, 24);
this.txtFileName.Name = "txtFileName";
this.txtFileName.Size = new System.Drawing.Size(248, 20);
this.txtFileName.TabIndex = 0;
this.txtFileName.Text = "";
//
// btnBrowse
//
this.btnBrowse.Location = new System.Drawing.Point(272, 24);
this.btnBrowse.Name = "btnBrowse";
this.btnBrowse.TabIndex = 1;
this.btnBrowse.Text = "&Browse...";
this.btnBrowse.Click += new
System.EventHandler(this.btnBrowse_Click);
//
// btnUpload
//
this.btnUpload.Location = new System.Drawing.Point(272, 56);
this.btnUpload.Name = "btnUpload";
this.btnUpload.TabIndex = 2;
this.btnUpload.Text = "&Upload";
this.btnUpload.Click += new
System.EventHandler(this.btnUpload_Click);
//
// frmUpload
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(370, 111);
this.Controls.Add(this.btnUpload);
this.Controls.Add(this.btnBrowse);
this.Controls.Add(this.txtFileName);
this.FormBorderStyle =
System.Windows.Forms.FormBorderStyle.FixedSingle;
this.MaximizeBox = false;
this.Name = "frmUpload";
this.Text = "Upload";
this.Load += new System.EventHandler(this.frmUpload_Load);
this.ResumeLayout(false);
}
#endregion
/// <summary>
/// The main entry point for the application.
/// </summary>
static void Main()
{
Application.Run(new frmUpload());
}
private FileUpload myUpload = new FileUpload();
private void UploadData( string FileName, int StartPos, byte[] bData )
{
//Call web service upload
myUpload.UploadFileData( FileName, StartPos, bData );
}
private void btnUpload_Click(object sender, System.EventArgs e)
{
FileInfo fi = new FileInfo( txtFileName.Text );
if( fi.Exists )
{
btnUpload.Enabled = false;//Avoid upload twice
//Init signals
ManualResetEvent[] events = new ManualResetEvent[5];
//Devide blocks
int nTotalBytes = (int)( fi.Length / 5 );
for( int i = 0; i < 5; i++ )
{
events[i] = new ManualResetEvent( false );
FileThread thdSub = new FileThread(
i * nTotalBytes,
( fi.Length - i * nTotalBytes ) > nTotalBytes ?
nTotalBytes:(int)( fi.Length - i * nTotalBytes ),
fi.FullName );
ThreadPool.QueueUserWorkItem( new WaitCallback(
thdSub.UploadFile ), events[i] );
}
//Wait for threads finished
WaitHandle.WaitAll( events );
//Reset button status
btnUpload.Enabled = true;
}
}
private void frmUpload_Load(object sender, System.EventArgs e)
{
FileThread.UploadHandle = new UploadFileData( this.UploadData );
}
private void btnBrowse_Click(object sender, System.EventArgs e)
{
if( fileOpen.ShowDialog() == DialogResult.OK )
txtFileName.Text = fileOpen.FileName;
}
private OpenFileDialog fileOpen = new OpenFileDialog();
}
public delegate void UploadFileData( string FileName, int StartPos, byte[]
bData );
/// <summary>
/// FileThread: a class for sub-thread
/// </summary>
sealed class FileThread
{
private int nStartPos;
private int nTotalBytes;
private string strFileName;
public static UploadFileData UploadHandle;
/// <summary>
/// Constructor
/// </summary>
/// <param name="StartPos"></param>
/// <param name="TotalBytes"></param>
/// <param name="FileName"></param>
public FileThread( int StartPos, int TotalBytes, string FileName )
{
//Init thread variant
nStartPos = StartPos;
nTotalBytes = TotalBytes;
strFileName = FileName;
//Only for debug
Debug.WriteLine( string.Format( "File name:{0} position: {1} total
byte:{2}",
strFileName, nStartPos, nTotalBytes ) );
}
/// <summary>
/// Sub-thread entry function
/// </summary>
/// <param name="stateinfo"></param>
public void UploadFile( object stateinfo )
{
int nRealRead, nBufferSize;
const int BUFFER_SIZE = 10240;
using( FileStream fs = new FileStream( strFileName,
FileMode.Open, FileAccess.Read,
FileShare.Read ) )
{
string sName = strFileName.Substring( strFileName.LastIndexOf(
"http://" ) + 1 );
byte[] bBuffer = new byte[BUFFER_SIZE];//Init 10k buffer
fs.Position = nStartPos;
nRealRead = 0;
do
{
nBufferSize = BUFFER_SIZE;
if( nRealRead + BUFFER_SIZE > nTotalBytes )
nBufferSize = nTotalBytes - nRealRead;
nBufferSize = fs.Read( bBuffer, 0, nBufferSize );
if( nBufferSize == BUFFER_SIZE )
UploadHandle( sName,
nRealRead + nStartPos,
bBuffer );
else if( nBufferSize > 0 )
{
//Copy data
byte[] bytData = new byte[nBufferSize];
Array.Copy( bBuffer,0, bytData, 0, nBufferSize );
UploadHandle( sName,
nRealRead + nStartPos,
bytData );
}
nRealRead += nBufferSize;
}
while( nRealRead < nTotalBytes );
}
//Release signal
ManualResetEvent mr = stateinfo as ManualResetEvent;
if( mr != null )
mr.Set();
}
}
}
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1332160
[收藏到我的網摘] Knight94 發表于 2006 年10 月12 日 19:47:00
相關文章:
用 WebClient.UploadData 方法 上載文件數據 2005-04-17 sunsnow8
<<《Effective C#》Item 19:推薦在繼承中使用接口 | 《Effective C#》Item 2:定義常量的兩種方法 >>
# 小玉 發表于2006-10-26 09:36:00 IP: 218.24.228.*
問個問題:
水晶報表可以動態創建嗎?或者
.NET 里什么報表可以實現動態創建報表?
我現在要做在程序動行時設計報表,可以給我些提示嗎
謝謝了
# Night 發表于2006-11-25 20:08:00 IP: 219.131.239.*
請問
我在運行時候
這句//Wait for threads finished
WaitHandle.WaitAll( events );
出現報錯
未處理NotSupportedException
不支持一個STA 線程上針對多個句柄的WaitAll
這個是什么引起的
但是注釋了這句也可以運行
去掉 受不受 影響?
# Night 發表于2006-11-26 10:11:00 IP: 219.131.240.*
你好!
我按照你的方法 去做了
去掉了Program.cs 內的[STAThread]
但是if (fileOpen.ShowDialog() == DialogResult.OK)
這里顯示錯誤
在可以調用 OLE 之前,必須將當前線程設置為單線程單元(STA)模式。請確保您的 Main 函數帶有
STAThreadAttribute 標記。
只有將調試器附加到該進程才會引發此異常。
這種該怎么改?
謝謝
# knight94 發表于2006-11-26 10:55:00 IP: 218.71.239.*
你的窗體上引用了什么特殊的com。
# knight94 發表于2006-11-26 09:15:00 IP: 218.71.239.*
to Night
你這個錯誤,只要把main 入口函數上的[STAThread]屬性標示去掉就行了。
# bj20082005 發表于2007-01-23 17:11:56 IP:
你這個FileUpload myUpload = new FileUpload();是引用哪個組件呀
# FlyBird2004 發表于2007-04-04 11:30:05 IP: 222.82.221.*
可以將這個程序的打包幫忙發一份嘛?因為調試了半天也沒有成功。不勝感激![email protected]
新手老問題---------跨線程的控件訪問
電子科技大學03 級02 班 周銀輝
新手經常會遇到這樣的問題: a 線程去訪問b 線程的控件,編譯器報錯(.net1.0 編譯時好像不會報,.net2.0 是肯
定會的).
解決方法有3 種:
1, 不安全的方法: 將 Control.CheckForIllegalCrossThreadCalls 設置為false (.net1.0 中沒有)
2,安全的方法: 異步委托
3, 安全的方法: 就是使用BackgroundWorker 來替代你自己創建的線程(.net1.0 中沒有)
以下是示例代碼
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
namespace CrossThreadDemo
{
public class Form1 : Form
{
// This delegate enables asynchronous calls for setting
// the text property on a TextBox control.
delegate void SetTextCallback(string text);
// This thread is used to demonstrate both thread-safe and
// unsafe ways to call a Windows Forms control.
private Thread demoThread = null;
// This BackgroundWorker is used to demonstrate the
// preferred way of performing asynchronous operations.
private BackgroundWorker backgroundWorker1;
private TextBox textBox1;
private Button setTextUnsafeBtn;
private Button setTextSafeBtn;
private Button setTextBackgroundWorkerBtn;
private System.ComponentModel.IContainer components = null;
public Form1()
{
InitializeComponent();
}
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
// This event handler creates a thread that calls a
// Windows Forms control in an unsafe way.
private void setTextUnsafeBtn_Click(
object sender,
EventArgs e)
{
this.demoThread =
new Thread(new ThreadStart(this.ThreadProcUnsafe));
this.demoThread.Start();
}
// This method is executed on the worker thread and makes
// an unsafe call on the TextBox control.
private void ThreadProcUnsafe()
{
this.textBox1.Text = "This text was set unsafely.";
}
// This event handler creates a thread that calls a
// Windows Forms control in a thread-safe way.
private void setTextSafeBtn_Click(
object sender,
EventArgs e)
{
this.demoThread =
new Thread(new ThreadStart(this.ThreadProcSafe));
this.demoThread.Start();
}
// This method is executed on the worker thread and makes
// a thread-safe call on the TextBox control.
private void ThreadProcSafe()
{
this.SetText("This text was set safely.");
}
// This method demonstrates a pattern for making thread-safe
// calls on a Windows Forms control.
//
// If the calling thread is different from the thread that
// created the TextBox control, this method creates a
// SetTextCallback and calls itself asynchronously using the
// Invoke method.
//
// If the calling thread is the same as the thread that created
// the TextBox control, the Text property is set directly.
private void SetText(string text)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.textBox1.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else
{
this.textBox1.Text = text;
}
}
// This event handler starts the form's
// BackgroundWorker by calling RunWorkerAsync.
//
// The Text property of the TextBox control is set
// when the BackgroundWorker raises the RunWorkerCompleted
// event.
private void setTextBackgroundWorkerBtn_Click(
object sender,
EventArgs e)
{
this.backgroundWorker1.RunWorkerAsync();
}
// This event handler sets the Text property of the TextBox
// control. It is called on the thread that created the
// TextBox control, so the call is thread-safe.
//
// BackgroundWorker is the preferred way to perform asynchronous
// operations.
private void backgroundWorker1_RunWorkerCompleted(
object sender,
RunWorkerCompletedEventArgs e)
{
this.textBox1.Text =
"This text was set safely by BackgroundWorker.";
}
Windows Form Designer generated code#region Windows Form Designer generated code
private void InitializeComponent()
{
this.textBox1 = new System.Windows.Forms.TextBox();
this.setTextUnsafeBtn = new System.Windows.Forms.Button();
this.setTextSafeBtn = new System.Windows.Forms.Button();
this.setTextBackgroundWorkerBtn = new System.Windows.Forms.Button();
this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker();
this.SuspendLayout();
//
// textBox1
//
this.textBox1.Location = new System.Drawing.Point(12, 12);
this.textBox1.Name = "textBox1";
this.textBox1.Size = new System.Drawing.Size(240, 20);
this.textBox1.TabIndex = 0;
//
// setTextUnsafeBtn
//
this.setTextUnsafeBtn.Location = new System.Drawing.Point(15, 55);
this.setTextUnsafeBtn.Name = "setTextUnsafeBtn";
this.setTextUnsafeBtn.TabIndex = 1;
this.setTextUnsafeBtn.Text = "Unsafe Call";
this.setTextUnsafeBtn.Click += new
System.EventHandler(this.setTextUnsafeBtn_Click);
//
// setTextSafeBtn
//
this.setTextSafeBtn.Location = new System.Drawing.Point(96, 55);
this.setTextSafeBtn.Name = "setTextSafeBtn";
this.setTextSafeBtn.TabIndex = 2;
this.setTextSafeBtn.Text = "Safe Call";
this.setTextSafeBtn.Click += new System.EventHandler(this.setTextSafeBtn_Click);
//
// setTextBackgroundWorkerBtn
//
this.setTextBackgroundWorkerBtn.Location = new System.Drawing.Point(177, 55);
this.setTextBackgroundWorkerBtn.Name = "setTextBackgroundWorkerBtn";
this.setTextBackgroundWorkerBtn.TabIndex = 3;
this.setTextBackgroundWorkerBtn.Text = "Safe BW Call";
this.setTextBackgroundWorkerBtn.Click += new
System.EventHandler(this.setTextBackgroundWorkerBtn_Click);
//
// backgroundWorker1
//
this.backgroundWorker1.RunWorkerCompleted += new
System.ComponentModel.RunWorkerCompletedEventHandler(this.backgroundWorker1_RunWork
erCompleted);
//
// Form1
//
this.ClientSize = new System.Drawing.Size(268, 96);
this.Controls.Add(this.setTextBackgroundWorkerBtn);
this.Controls.Add(this.setTextSafeBtn);
this.Controls.Add(this.setTextUnsafeBtn);
this.Controls.Add(this.textBox1);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.Run(new Form1());
}
}
}
在使用方法2 時的注意事項: 不要將除了控件訪問外其他邏輯代碼放到委托的回調方法中.
如何彈出一個模式窗口來顯示進度條
最近看了好多人問這方面的問題,以前我也寫過一篇blog,里面說了如何在子線程中控制進度條。但目前大多
數環境,需要彈出模式窗口,來顯示進度條,那么只需要在原先的基礎上稍作修改即可。
首先是進度條窗體,需要在上面添加進度條,然后去掉ControlBox。除此外,還要增加一個方法,用來控制進
度條的增加幅度,具體如下:
/// <summary>
/// Increase process bar
/// </summary>
/// <param name="nValue">the value increased</param>
/// <returns></returns>
public bool Increase( int nValue )
{
if( nValue > 0 )
{
if( prcBar.Value + nValue < prcBar.Maximum )
{
prcBar.Value += nValue;
return true;
}
else
{
prcBar.Value = prcBar.Maximum;
this.Close();
return false;
}
}
return false;
}
接著就是主窗體了,如何進行操作了,首先需要定義兩個私有成員,一個委托。其中一個私有成員是保存當前進
度條窗體對象,另一個是保存委托方法(即增加進度條尺度),具體如下:
private frmProcessBar myProcessBar = null;
private delegate bool IncreaseHandle( int nValue );
private IncreaseHandle myIncrease = null;
接著要在主窗體中提供函數來打開進度條窗體,如下:
/// <summary>
/// Open process bar window
/// </summary>
private void ShowProcessBar()
{
myProcessBar = new frmProcessBar();
// Init increase event
myIncrease = new IncreaseHandle( myProcessBar.Increase );
myProcessBar.ShowDialog();
myProcessBar = null;
}
那么現在就可以開始創建線程來運行,具體如下:
/// <summary>
/// Sub thread function
/// </summary>
private void ThreadFun()
{
MethodInvoker mi = new MethodInvoker( ShowProcessBar );
this.BeginInvoke( mi );
Thread.Sleep( 1000 );//Sleep a while to show window
bool blnIncreased = false;
object objReturn = null;
do
{
Thread.Sleep( 50 );
objReturn = this.Invoke( this.myIncrease,
new object[]{ 2 } );
blnIncreased = (bool)objReturn ;
}
while( blnIncreased );
}
注意以上,在打開進度條窗體和增加進度條進度的時候,一個用的是BeginInvoke,一個是Invoke,
這里的區別是BeginInvoke 不需要等待方法運行完畢,而Invoke 是要等待方法運行完畢。還有一點,此處用
返回值來判斷進度條是否到頭了,如果需要有其他的控制,可以類似前面的方法來進行擴展。
啟動線程,可以如下:
Thread thdSub = new Thread( new ThreadStart( ThreadFun ) );
thdSub.Start();
這樣,一個用模式打開進度條窗體就做完了。
用戶不喜歡反應慢的程序。在執行耗時較長的操作時,使用多線程是明智之舉,它可以提高程序 UI 的響應速度,
使得一切運行顯得更為快速。在 Windows 中進行多線程編程曾經是 C++開發人員的專屬特權,但是現在,可
以使用所有兼容 Microsoft .NET 的語言來編寫。
不過Windows 窗體體系結構對線程使用制定了嚴格的規則。如果只是編寫單線程應用程序,則沒必要知道這些
規則,這是因為單線程的代碼不可能違反這些規則。然而,一旦采用多線程,就需要理解 Windows 窗體中最重
要的一條線程規則:除了極少數的例外情況,否則都不要在它的創建線程以外的線程中使用控件的任何成員。本
規則的例外情況有文檔說明,但這樣的情況非常少。這適用于其類派生自 System.Windows.Forms.Control
的任何對象,其中幾乎包括 UI 中的所有元素。所有的 UI 元素(包括表單本身)都是從 Control 類派生的對象。
此外,這條規則的結果是一個被包含的控件(如,包含在一個表單中的按鈕)必須與包含它控件位處于同一個線
程中。也就是說,一個窗口中的所有控件屬于同一個 UI 線程。實際中,大部分 Windows 窗體應用程序最終都
只有一個線程,所有 UI 活動都發生在這個線程上。這個線程通常稱為 UI 線程。這意味著您不能調用用戶界面
中任意控件上的任何方法,除非在該方法的文檔說明中指出可以調用。該規則的例外情況(總有文檔記錄)非常
少而且它們之間關系也不大。請注意,以下代碼是非法的:
private Thread myThread;
private void Form1_Load(object sender, EventArgs e)
{
myThread = new Thread(new ThreadStart(RunsOnWorkerThread));
myThread.Start();
}
private void RunsOnWorkerThread()
{
label1.Text = "myThread 線程調用UI 控件";
}
如果您在 .NET Framework 1.0 版本中嘗試運行這段代碼,也許會僥幸運行成功,或者初看起來是如此。這就
是多線程錯誤中的主要問題,即它們并不會立即顯現出來。甚至當出現了一些錯誤時,在第一次演示程序之前一
切看起來也都很正常。但不要搞錯 —我剛才顯示的這段代碼明顯違反了規則,并且可以預見,任何抱希望于“試
運行時良好,應該就沒有問題”的人在即將到來的調試期是會付出沉重代價的。
下面我們來看看有哪些方法可以解決這一問題。
一、System.Windows.Forms.MethodInvoker 類型是一個系統定義的委托,用于調用不帶參數的方法。
private Thread myThread;
private void Form1_Load(object sender, EventArgs e)
{
myThread = new Thread(new ThreadStart(RunsOnWorkerThread));
myThread.Start();
}
private void RunsOnWorkerThread()
{
MethodInvoker mi = new MethodInvoker(SetControlsProp);
BeginInvoke(mi);
}
private void SetControlsProp()
{
label1.Text = "myThread 線程調用UI 控件";
}
二、直接用System.EventHandle(可帶參數)
private Thread myThread;
private void Form1_Load(object sender, EventArgs e)
{
myThread = new Thread(new ThreadStart(RunsOnWorkerThread));
myThread.Start();
}
private void RunsOnWorkerThread()
{
//DoSomethingSlow();
string pList = "myThread 線程調用UI 控件";
label1.BeginInvoke(new System.EventHandler(UpdateUI), pList);
}
//直接用System.EventHandler,沒有必要自定義委托
private void UpdateUI(object o, System.EventArgs e)
{
//UI 線程設置label1 屬性
label1.Text = o.ToString() + "成功!";
}
三、包裝 Control.Invoke
雖然第二個方法中的代碼解決了這個問題,但它相當繁瑣。如果輔助線程希望在結束時提供更多的反饋信息,而
不是簡單地給出“Finished!”消息,則 BeginInvoke 過于復雜的使用方法會令人生畏。為了傳達其他消息,
例如“正在處理”、“一切順利”等等,需要設法向 UpdateUI 函數傳遞一個參數。可能還需要添加一個進度欄
以提高反饋能力。這么多次調用 BeginInvoke 可能導致輔助線程受該代碼支配。這樣不僅會造成不便,而且考
慮到輔助線程與 UI 的協調性,這樣設計也不好。對這些進行分析之后,我們認為包裝函數可以解決這兩個問題。
private Thread myThread;
private void Form1_Load(object sender, EventArgs e)
{
myThread = new Thread(new ThreadStart(RunsOnWorkerThread));
myThread.Start();
}
private void RunsOnWorkerThread()
{
////DoSomethingSlow();
for (int i = 0; i < 100; i++)
{
ShowProgress( Convert.ToString(i)+"%", i);
Thread.Sleep(100);
}
}
public void ShowProgress(string msg, int percentDone)
{
// Wrap the parameters in some EventArgs-derived custom class:
System.EventArgs e = new MyProgressEvents(msg, percentDone);
object[] pList = { this, e };
BeginInvoke(new MyProgressEventsHandler(UpdateUI), pList);
}
private delegate void MyProgressEventsHandler(object sender, MyProgressEvents e);
private void UpdateUI(object sender, MyProgressEvents e)
{
lblStatus.Text = e.Msg;
myProgressControl.Value = e.PercentDone;
}
public class MyProgressEvents : EventArgs
{
public string Msg;
public int PercentDone;
public MyProgressEvents(string msg, int per)
{
Msg = msg;
PercentDone = per;
}
}
ShowProgress 方法對將調用引向正確線程的工作進行封裝。這意味著輔助線程代碼不再擔心需要過多關注 UI
細節,而只要定期調用 ShowProgress 即可。
如果我提供一個設計為可從任何線程調用的公共方法,則完全有可能某人會從 UI 線程調用這個方法。在這種情
況下,沒必要調用 BeginInvoke,因為我已經處于正確的線程中。調用 Invoke 完全是浪費時間和資源,不如
直接調用適當的方法。為了避免這種情況,Control 類將公開一個稱為 InvokeRequired 的屬性。這是“只限 UI
線程”規則的另一個例外。它可從任何線程讀取,如果調用線程是 UI 線程,則返回假,其他線程則返回真。這
意味著我可以按以下方式修改包裝:
public void ShowProgress(string msg, int percentDone)
{
if (InvokeRequired)
{
// As before
//...
}
else
{
// We're already on the UI thread just
// call straight through.
UpdateUI(this, new MyProgressEvents(msg,PercentDone));
}
}
在當前線程中訪問主線程的控件
在做廣州移動集團短信平臺的時候,需要定時刷新 SP 的告警信息。于是創建了一個 Timer 對象,在Timer 對
象的 Elapsed 事件中需要訪問主窗體中的一個 LinkLabel。剛開始直接用this.lnklblMsg.Text="你有
"+intMsg+"條新告警信息";結果運行出錯:System.InvalidOperationException:線程間操作無效:從不是
創建控件“lnklblMsg”的線程訪問它。主要原因是控件“lnklblMsg”是由主窗體創建的,而訪問它是在 Timer
創建的線程中訪問。解決這個問題的方法是用 Invoke 來執行委托,間接調用 lnklblMsg 控件。
首先創建委托和訪問 LinkLabel 控件的方法:
//創建委托,用于訪問主線程的控件
public delegate void delegateNewMsg(int msgs);
void dgtNewMsgMethod(int intMsgs)
{
if (intMsgs > 0)
{
this.lnklblNewWarn.Text = "有" + intMsgs.ToString() + "條新告警信息";
}
else
{
this.lnklblNewWarn.Text = "";
}
}
然后再在 Timer 的 Elapsed 方法中執行委托:
delegateNewMsg d = dgtNewMsgMethod;
Invoke(d,intMsgs);//訪問主線程資源
C#中在線程中訪問主Form 控件的問題
C#不允許直接從線程中訪問Form 里的控件,比如希望在線程里修改Form 里的一個TextBox 的內容等等,
唯一的做法是使用Invoke 方法,下面是一個MSDN 里的Example,很說明問題:
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
public class MyFormControl : Form
...{
public delegate void AddListItem(String myString);
public AddListItem myDelegate;
private Button myButton;
private Thread myThread;
private ListBox myListBox;
public MyFormControl()
...{
myButton = new Button();
myListBox = new ListBox();
myButton.Location = new Point(72, 160);
myButton.Size = new Size(152, 32);
myButton.TabIndex = 1;
myButton.Text = "Add items in list box";
myButton.Click += new EventHandler(Button_Click);
myListBox.Location = new Point(48, 32);
myListBox.Name = "myListBox";
myListBox.Size = new Size(200, 95);
myListBox.TabIndex = 2;
ClientSize = new Size(292, 273);
Controls.AddRange(new Control[] ...{myListBox,myButton});
Text = " 'Control_Invoke' example ";
myDelegate = new AddListItem(AddListItemMethod);
}
static void Main()
...{
MyFormControl myForm = new MyFormControl();
myForm.ShowDialog();
}
public void AddListItemMethod(String myString)
...{
myListBox.Items.Add(myString);
}
private void Button_Click(object sender, EventArgs e)
...{
myThread = new Thread(new ThreadStart(ThreadFunction));
myThread.Start();
}
private void ThreadFunction()
...{
MyThreadClass myThreadClassObject = new MyThreadClass(this);
myThreadClassObject.Run();
}
}
public class MyThreadClass
...{
MyFormControl myFormControl1;
public MyThreadClass(MyFormControl myForm)
...{
myFormControl1 = myForm;
}
String myString;
public void Run()
...{
for (int i = 1; i <= 5; i++)
...{
myString = "Step number " + i.ToString() + " executed";
Thread.Sleep(400);
// Execute the specified delegate on the thread that owns
// 'myFormControl1' control's underlying window handle with
// the specified list of arguments.
myFormControl1.Invoke(myFormControl1.myDelegate,
new Object[] ...{myString});
}
}
}
如何在子線程中操作窗體上的控件 - 愚翁專欄 - CSDNBlog 正在處理您的請求...
如何在子線程中操作窗體上的控件
一般來說,直接在子線程中對窗體上的控件操作是會出現異常,這是由于子線程和運行窗體的線程是不同的空間,
因此想要在子線程來操作窗體上的控件,是不可能簡單的通過控件對象名來操作,但不是說不能進行操作,微軟
提供了Invoke 的方法,其作用就是讓子線程告訴窗體線程來完成相應的控件操作。
現在用一個用線程控制的進程條來說明,大致的步驟如下:
1. 創建Invoke 函數,大致如下:
/// <summary>
/// Delegate function to be invoked by main thread
/// </summary>
private void InvokeFun()
{
if( prgBar.Value < 100 )
prgBar.Value = prgBar.Value + 1;
}
2. 子線程入口函數:
/// <summary>
/// Thread function interface
/// </summary>
private void ThreadFun()
{
//Create invoke method by specific function
MethodInvoker mi = new MethodInvoker( this.InvokeFun );
for( int i = 0; i < 100; i++ )
{
this.BeginInvoke( mi );
Thread.Sleep( 100 );
}
}
3. 創建子線程:
Thread thdProcess = new Thread( new ThreadStart( ThreadFun ) );
thdProcess.Start();
備注:
using System.Threading;
private System.Windows.Forms.ProgressBar prgBar;
運行后的效果如下圖所示:
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=626584
[收藏到我的網摘] Knight94 發表于 2006 年03 月16 日 19:25:00
<<如何在MDI 程序中把子窗體菜單合并到主窗體上 |
# knight94 發表于2006-04-21 09:21:00 IP: 218.71.239.*
首先,你在創建這個類的對象時,要把當前的窗體對象傳進去(為了到時候能通過它調用
其的方法)。
然后把
Thread thdProcess = new Thread( new ThreadStart( ThreadFun ) );
改成
Thread thdProcess = new Thread( new ThreadStart( yourObj.ThreadFun ) );
即可。
# User 發表于2006-04-29 16:03:00 IP: 219.238.164.*
我想實現批量注冊用戶的功能,首先在文本域里錄入用戶信息(按照約定的規則),然后
執行。
現在我希望在界面上有一個文本域滾動提示每個用戶導入是否成功,失敗的原因,另外可
以有一個滾動條顯示進度。
麻煩 knight94 ,指教。
# knight94 發表于2006-05-01 13:17:00 IP: 218.71.239.*
to User
對于你的問題,參看我的例子就可以了,我的例子是如何滾動進度條,那么你可以在上面
的基礎上進行擴展,而且Invoke 調用方法,也可以加參數,例如:
-------in your form class ------
public void ShowText( string sData )
{
//Handle "sData" here
}
--------in thread fun ----------
MethodInvoker mi = new MethodInvoker( this.ShowText );
this.BeginInvoke( mi, new object[]{ yourText } );
# aicsharp 發表于2006-05-26 17:25:00 IP: 60.216.137.*
不錯,msdn 的解釋太不中國了,光看那個根本不知道干什么的。他是這么解釋的
“MethodInvoker 提供一個簡單委托,該委托用于調用含 void 參數列表的方法。在
對控件的 Invoke
方法進行調用時或需要一個簡單委托又不想自己定義時可以使用該委托。

這是msdn for vs2005 的解釋。
# DisonWorld 發表于2006-05-30 13:51:00 IP: 61.186.185.*
I have a question as the following:
foreach(int i in ...)
{
//the order is important, that is:
//DoSomthing(2) must be done after the DoSomething(1) is completed
DoSomething(i);
}
private void DoSomething(int i)
{
//In here, a function to upload file will be called,
//and it will be implemeted by using class Upload, who is inherited from
BackgroundWorker and will call a WSE
// to upload the file, so the speed is a bit slow
}
So my question is how to make the DoSomthing(2) only begins when the
DoSomthing(1) is finished.
# knight94 發表于2006-05-30 14:04:00 IP: 218.71.239.*
to DisonWorld
you can reprace "BeginInvoke" method with "Invoke" method.
See difference between them in:
http://blog.csdn.net/Knight94/archive/2006/05/27/757351.aspx
# DisonWorld 發表于2006-05-31 12:00:00 IP: 61.186.185.*
Hello Knight94,
I have tried the Invoke, but the ui seem to be dead, and the import order
seems to wrong.
For example:
PackageA: no files to import
PackageB: need to import files
PackageC: no files to import
After the PackageA is finished, and the PackageB will start and begin
import file, but even the PackageB has not been finished, the PackageC
will start.
private delegate bool ImportSinglePackage();
private ImportSinglePackage importSinglePackage = null;
//The UI will call the BeginImport to start the importing
public BeginImport()
{
Cursor.Current = Cursors.WaitCursor;
this.importSinglePackage = new
ImportSinglePackage(this.ImportBySinglePackage);
mImportingDataThread = new Thread(new
ThreadStart(this.ImportPackages));
mImportingDataThread.Start();
}
private void ImportPackages()
{
bool IsAllPackagesHaveBeenImported = false;
do{
this.currentImportRecordKey =
recordKeysToImport[recordsHasBeenImported];
Thread.Sleep(1000);
IsAllPackagesHaveBeenImported =
(bool)this.Invoke(this.importSinglePackage, null);
}while (!IsAllPackagesHaveBeenImported);
}
private bool ImportBySinglePackage()
{
//ImportFiles will call the a class inherited from the BackgroundWorker to
download/upload file
Thread threadImportFiles = new Thread(new ThreadStart(this.ImportFiles));
threadImportFiles.Start();
threadImportFiles.Join();
# knight94 發表于2006-05-31 13:09:00 IP: 218.71.239.*
As the upper code, there is no return value in your function named
"ImportBySinglePackage".
By the way, you should check the invoke's return value in your
"ImportPackages" function.
# tianjj 發表于2006-08-17 17:10:00 IP: 203.86.72.*
to:knight94
--------in thread fun ----------
MethodInvoker mi = new MethodInvoker( this.ShowText );
這個調用帶有參數,好像有誤。
提示showText 重載與MethodInvoker 不匹配。
# tianjj 發表于2006-08-17 17:12:00 IP: 203.86.72.*
to:knight94
--------in thread fun ----------
MethodInvoker mi = new MethodInvoker( this.ShowText );
這個調用帶有參數,好像有誤。
提示showText 重載與MethodInvoker 不匹配。
# tianjj 發表于2006-08-17 17:27:00 IP: 203.86.72.*
to:knight94
MethodInvoker mi = new MethodInvoker( this.ShowText );
這個調用帶參數方法,好像有誤,
顯示showText 重載與MethodInvoker 不匹配。
# knight94 發表于2006-08-17 18:07:00 IP: 218.71.239.*
to tianjj
參看:
http://blog.csdn.net/knight94/archive/2006/05/27/757351.aspx
# knight94 發表于2006-08-17 18:10:00 IP: 218.71.239.*
to tianjj
MethodInvoker 是一個delegate 類型,你可以自己定義,例如:
public delegate int MyInvoker( string strValue );
MyInvoder mi = new MyInvoker( yourMethod );
this.Invoke( mi, new object[]{ "test" } );
# 菜鳥 發表于2006-08-23 16:29:00 IP: 219.140.184.*
進度條為什么不用TIMER 控件來做?
# knight94 發表于2006-08-23 17:48:00 IP: 218.71.239.*
Timer 的交互性太差。
# 大漠狂沙 發表于2006-11-02 14:00:00 IP: 219.142.3.*
工作線程類:
public class CommissionHelper
{
private System.Windows.Forms.ProgressBar proBar;
public CommissionHelper(System.Windows.Forms.ProgressBar proBar)
{
this.proBar=proBar;
}
public void Run()
{
int i=1;
for()
{
proBar.Value=i;
i=i+1;
}
}
}
主線程窗體按鈕調用:
private void btnTransfer_Click(object sender, System.EventArgs e)
{
CommissionHelper helper=new CommissionHelper(proBar);
Thread tdh=new Thread( new ThreadStart( helper.Run ) );
tdh.Start();
}
這樣也可以實現。我沒明白為什么要用Invoke?!
謝謝!
# knight94 發表于2006-11-02 14:49:00 IP: 218.71.239.*
to 大漠狂沙
問題是很對情況下,在子線程中不能直接操縱UI 線程上的控件,這時候就需要用Invoke
或者BeginInvoke 來完成。
也就是說現在有些程序是可以跨線程互相操作,有些程序是不允許的。
前者,按照你所說的沒問題;后者按照你所寫的,則是有問題的。
# 大漠狂沙 發表于2006-11-03 11:16:00 IP: 219.142.3.*
嗯,明白了,謝謝你的回復,學到很多東西!
# 小魯 發表于2006-11-04 14:16:00 IP: 58.61.133.*
愚翁老師,非常感謝,我鉆研了阻塞線程中打開新窗體,窗體定死的問題,一天一夜都沒有答
案,你的文章個給了我啟發,現在解決了非常感謝.
# wc_king 發表于2006-11-06 00:44:00 IP: 222.90.59.*
knight94 發表于2006-11-02 14:49:00 IP: 218.71.239.*
to 大漠狂沙
問題是很對情況下,在子線程中不能直接操縱UI 線程上的控件,這時候就需要用Invoke
或者BeginInvoke 來完成。
也就是說現在有些程序是可以跨線程互相操作,有些程序是不允許的。
前者,按照你所說的沒問題;后者按照你所寫的,則是有問題的。
--------------------------
請問,能舉個例子說明什么時候會出現跨線程互相操作有問題?那么應該根據什么標準來
判斷會出現這種問題?
# knight94 發表于2006-11-06 17:56:00 IP: 218.71.239.*
to wc_king
對于你所說的,可以通過Control.InvokeRequired 來進行判斷是否需要Invoke 或者
BeginInvoke 來操作。
# 老實和尚 發表于2006-11-24 14:25:00 IP: 211.162.77.*
樓主:
我新學C#,以前一直在unix 下面混的,現在我根據您的介紹寫一個界面,需求是:當
我按了“begin”button 以后,listview 上不停的打印東西出來,按了“stop”button 以后,
停止打印。源代碼如下所示:
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace WindowsApplication1
{
public partial class Form1 : Form
{
private volatile bool flag = false;
private Thread th1 = null;
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
flag = true;
th1 = new Thread(new ThreadStart(th_fun));
th1.Start();
}
private void fun()
{
while (flag)
{
listView1.Items.Add("ssss");
listView1.Refresh();
Thread.Sleep(1000);
}
Console.WriteLine("stopppppp");
}
private void th_fun()
{
MethodInvoker me = new MethodInvoker(this.fun);
this.BeginInvoke(me);
}
private void button2_Click(object sender, EventArgs e)
{
Console.WriteLine("stop clicked");
# knight94 發表于2006-11-24 16:58:00 IP: 218.71.239.*
to 老實和尚
你的循環放錯位置了,你這樣寫,也就是線程只執行一次函數,而這個函數永遠在執行。
你如下去修改
private void fun()
{
listView1.Items.Add("ssss");
listView1.Refresh();
}
private void th_fun()
{
MethodInvoker me = new MethodInvoker(this.fun);
while (flag)
{
this.BeginInvoke(me);
Thread.Sleep(1000);
}
Console.WriteLine("stopppppp");
}
# 老實和尚 發表于2006-11-24 17:37:00 IP: 211.162.77.*
謝謝樓主。
thx~~~~
# atealxt 發表于2006-12-12 11:31:06 IP: 221.216.0.*
謝謝你的幾篇Thread 的文章讓我解決了和上面"小魯"一樣的問題
# dingsea 發表于2007-01-09 16:50:37 IP: 219.74.131.*
如果把InvokeFun()的實現改為
label1.Text+="1";
這樣一句的話,在程序沒有運行完之前退出會有異常:
Cannot call Invoke or InvokeAsync on a control until the window handle has
been created.
# superwat 發表于2007-01-19 16:01:17 IP: 218.104.7.*
老大,我郁悶了,救救我啊.
using System.Threading;
private Boolean bTasking = false;
private Thread tr_Task;
public void UF_Task(Boolean bstart)
{
bTasking = bstart;
if (bstart) sBarP2.Value = 0;
if (tr_Task == null)
{
tr_Task = new Thread(new ThreadStart(UF_DoProcess));
tr_Task.IsBackground = true;
tr_Task.Name = "QBSProcess";
tr_Task.Start();
}
if (!bstart) sBarP2.Value = 100;
}
private void UF_DoProcess()
{
try
{
MethodInvoker mip = new MethodInvoker(this.UF_ProcStep);
while (bTasking)
{
this.BeginInvoke(mip);
Thread.Sleep(100);
}
}
catch { }
}
private void UF_ProcStep()
{
if (!bTasking) return;
if (sBarP2.Value == 100) sBarP2.Value = 0;
else sBarP2.PerformStep();
}
假設我在調用一個任務A 時 ,大約需要1 秒,調用
UF_Task(true);
A 任務;
UF_Task(false);
可是進度條沒有中間狀態啊,進度條只有0 和100....
怎么回事啊???
在線等你,老大.
# Knight94 發表于2007-01-20 11:40:05 IP: 210.77.27.*
to superwat
你出現的問題是由于用Invoke 是提交給UI 所在線程來完成,而UI 線程此時忙于處理A
任務,因此得不到及時響應。
因此你可以把這部分
UF_Task(true);
A 任務;
UF_Task(false);
放到一個單獨的線程去處理,這樣就避免了UI 線程被占用。
# superwat 發表于2007-01-22 16:33:00 IP: 218.104.7.*
:)
謝謝老大,我也明白其中的道理了.現在想到另外一個辦法.我試試效果,行的通的話,回來
獻丑,呵呵
# sunrobust 發表于2007-01-26 11:50:23 IP: 221.122.45.*
愚翁,您好!這篇文章使我收益匪淺,致謝!
我是新手,有幾個問題向你咨詢.
1. 如果我將子線程單獨寫在一個類定義里面,應如何安排InvokeFun() 和
ThreadFun()兩個函數?
即誰在子線程定義中,誰應該寫在主窗體定義中?
2. 是不是在子線程中仍然要引入System.Windows.Forms 命名控件?
# sunrobust 發表于2007-01-26 11:51:31 IP: 221.122.45.*
3. 是否要修改ProcessBar 的屬性為Public?
# Knight94 發表于2007-01-31 10:13:11 IP: 210.77.27.*
to sunrobust
不一定,既然是通過委托來操作,沒必要把processbar 屬性設為public,只需要給出
相應的修改方法就行。
你可以參看這篇文章
http://blog.csdn.net/Knight94/archive/2006/08/24/1111267.aspx
委托
[轉]如何智能客戶端應用程序性能
智能客戶端應用程序性能
發布日期: 08/20/2004 | 更新日期: 08/20/2004
智能客戶端體系結構與設計指南
David Hill、Brenton Webster、Edward A. Jezierski、Srinath Vasireddy、Mohamma
d Al-Sabt,Microsoft Corporation,Blaine Wastell Ascentium Corporation,Jonath
an Rasmusson 和 Paul Gale ThoughtWorks 和 Paul Slater Wadeware LLC
相關鏈接
Microsoft? patterns & practices 庫 http://www.microsoft.com/resources/practice
s/default.mspx
.NET 的應用程序體系結構:設計應用程序和服務http://msdn.microsoft.com/library/enus/
dnbda/html/distapp.asp
摘要:本章討論如何優化您的智能客戶端應用程序。本章分析您可以在設計時采取的步驟,并介
紹如何調整智能客戶端應用程序以及診斷任何性能問題。
本頁內容
4針對性能進行設計
5H 6H性能調整和診斷
7H 8H小結
9H 10H參考資料
智能客戶端應用程序可以提供比 Web 應用程序更豐富和響應速度更快的用戶界面,并且可以
利用本地系統資源。如果應用程序的大部分駐留在用戶的計算機上,則應用程序不需要到 Web
服務器的持續的往返行程。這有利于提高性能和響應性。然而,要實現智能客戶端應用程序的
全部潛能,您應該在應用程序的設計階段仔細考慮性能問題。通過在規劃和設計您的應用程序時
解決性能問題,可以幫助您及早控制成本,并減小以后陷入性能問題的可能性。
注改善智能客戶端應用程序的性能并不僅限于應用程序設計問題。您可以在整個應用程序生存期
中采取許多個步驟來使 .NET 代碼具有更高的性能。雖然 .NET 公共語言運行庫 (CLR) 在執
行代碼方面非常有效,但您可以使用多種技術來提高代碼的性能,并防止在代碼級引入性能問題。
有關這些問題的詳細信息,請參閱1Hhttp://msdn.microsoft.com/perf。
在應用程序的設計中定義現實的性能要求并識別潛在的問題顯然是重要的,但是性能問題通常只
在編寫代碼之后對其進行測試時出現。在這種情況下,您可以使用一些工具和技術來跟蹤性能問
題。
本章分析如何設計和調整您的智能客戶端應用程序以獲得最佳性能。它討論了許多設計和體系結
構問題(包括線程處理和緩存注意事項),并且分析了如何增強應用程序的 Windows 窗體部
分的性能。本章還介紹了您可以用來跟蹤和診斷智能客戶端應用程序性能問題的一些技術和工
具。
針對性能進行設計
您可以在應用程序設計或體系結構級完成許多工作,以確保智能客戶端應用程序具有良好的性
能。您應該確保在設計階段盡可能早地制定現實的且可度量的性能目標,以便評估設計折衷,并
且提供最劃算的方法來解決性能問題。只要可能,性能目標就應該基于實際的用戶和業務要求,
因為這些要求受到應用程序所處的操作環境的強烈影響。性能建模是一種結構化的且可重復的過
程,您可以使用該過程來管理您的應用程序并確保其實現性能目標。有關詳細信息,請參閱 I
mproving .NET Application Performance and Scalability 中的第 2 章“Performance
Modeling”,網址為:12Hhttp://msdn.microsoft.com/library/default.asp?url=/library/e
n-us/dnpag/html/scalenetchapt02.asp。
智能客戶端通常是較大的分布式應用程序的組成部分。很重要的一點是在完整應用程序的上下文
中考慮智能客戶端應用程序的性能,包括該客戶端應用程序使用的所有位于網絡中的資源。微調
并優化應用程序中的每一個組件通常是不必要或不可能的。相反,性能調整應該基于優先級、時
間、預算約束和風險。一味地追求高性能通常并不是一種劃算的策略。
智能客戶端還將需要與用戶計算機上的其他應用程序共存。當您設計智能客戶端應用程序時,您
應該考慮到您的應用程序將需要與客戶端計算機上的其他應用程序共享系統資源,例如,內存、
CPU 時間和網絡利用率。
注有關設計可伸縮的高性能遠程服務的信息,請參閱Improving .NET Performance and S
calability,網址為:13Hhttp://msdn.microsoft.com/library/default.asp?url=/library/enus/
dnpag/html/scalenet.asp。本指南包含有關如何優化 .NET 代碼以獲得最佳性能的詳細
信息。
要設計高性能的智能客戶端,請考慮下列事項:
? 在適當的位置緩存數據。數據緩存可以顯著改善智能客戶端應用程序的性能,使您可以在本地使用數據,
而不必經常從網絡檢索數據。但是,敏感數據或頻繁更改的數據通常不適合進行緩存。
? 優化網絡通訊。如果通過“健談的”接口與遠程層服務進行通訊,并且借助于多個請求/響應往返行程來執
行單個邏輯操作,則可能消耗系統和網絡資源,從而導致低劣的應用程序性能。
? 有效地使用線程。如果您使用用戶界面 (UI) 線程執行阻塞 I/O 綁定調用,則 UI 似乎不對用戶作出響
應。因為創建和關閉線程需要系統開銷,所以創建大量不必要的線程可能導致低劣的性能。
? 有效地使用事務。如果客戶端具有本地數據,則使用原子事務可幫助您確保該數據是一致的。因為數據是
本地的,所以事務也是本地的而不是分布式的。對于脫機工作的智能客戶端而言,對本地數據進行的任何
更改都是暫時的。客戶端在重新聯機時需要同步更改。對于非本地數據而言,在某些情況下可以使用分布
式事務(例如,當服務位于具有良好連接性的同一物理位置并且服務支持它時)。諸如 Web 服務和消
息隊列之類的服務不支持分布式事務。
? 優化應用程序啟動時間。較短的應用程序啟動時間使用戶可以更為迅速地開始與應用程序交互,從而使用
戶立刻對應用程序的性能和可用性產生好感。應該對您的應用程序進行適當的設計,以便在應用程序啟動
時僅加載那些必需的程序集。因為加載每個程序集都會引起性能開銷,所以請避免使用大量程序集。
? 有效地管理可用資源。低劣的設計決策(例如,實現不必要的完成器,未能在 Dispose 方法中取消終
止,或者未能釋放非托管資源)可能導致在回收資源時發生不必要的延遲,并且可能造成使應用程序性能
降低的資源泄漏。如果應用程序未能正確地釋放資源,或者應用程序顯式強制進行垃圾回收,則可能會妨
礙 CLR 有效地管理內存。
? 優化 Windows 窗體性能。智能客戶端應用程序依靠 Windows 窗體來提供內容豐富且響應迅速的用
戶界面.您可以使用多種技術來確保 Windows 窗體提供最佳性能。這些技術包括降低用戶界面的復雜
性,以及避免同時加載大量數據。
在許多情況下,從用戶角度感受到的應用程序性能起碼與應用程序的實際性能同樣重要。您可以
通過對設計進行某些特定的更改來創建在用戶看來性能高得多的應用程序,例如:使用后臺異步
處理(以使 UI 能作出響應);顯示進度欄以指示任務的進度;提供相應的選項以便用戶取消
長期運行的任務。
本節將專門詳細討論這些問題。
數據緩存原則
緩存是一種能夠改善應用程序性能并提供響應迅速的用戶界面的重要技術。您應該考慮下列選
項:
? 緩存頻繁檢索的數據以減少往返行程。如果您的應用程序必須頻繁地與網絡服務交互以檢索數據,則應該
考慮在客戶端緩存數據,從而減少通過網絡重復獲取數據的需要。這可以極大地提高性能,提供對數據的
近乎即時的訪問,并且消除了可能對智能客戶端應用程序性能造成不利影響的網絡延遲和中斷風險。
? 緩存只讀引用數據。只讀引用數據通常是理想的緩存對象。此類數據用于提供進行驗證和用戶界面顯示所
需的數據,例如,產品說明、ID 等等。因為客戶端無法更改此類數據,所以通常可以在客戶端緩存它而
無須進行任何進一步的特殊處理。
? 緩存要發送給位于網絡上的服務的數據。您應該考慮緩存要發送給位于網絡上的服務的數據。例如,如果
您的應用程序允許用戶輸入由在多個窗體中收集的一些離散數據項組成的定單信息,則請考慮允許用戶輸
入全部數據,然后在輸入過程的結尾在一個網絡調用中發送定單信息。
? 盡量少地緩存高度不穩定的數據。在緩存任何不穩定的數據之前,您需要考慮在其變得陳舊或者由于其他
原因變得不可用之前,能夠將其緩存多長時間。如果數據高度不穩定并且您的應用程序依賴于最新信息,
則或許只能將數據緩存很短一段時間(如果可以緩存)。
? 盡量少地緩存敏感數據。您應該避免在客戶端上緩存敏感數據,因為在大多數情況下,您無法保證客戶端
的物理安全。但是,如果您必須在客戶端上緩存敏感數據,則您通常將需要加密數據,該操作本身也會影
響性能。
有關數據緩存的其他問題的詳細信息,請參閱本指南的14H第 2 章。另請參閱 Improving .NET
Application Performance and Scalability 的第 3 章“Design Guidelines for Applicat
ion Performance”(15Hhttp://msdn.microsoft.com/library/default.asp?url=/library/enus/
dnpag/html/scalenetchapt03.asp) 的“Caching”一節以及 Improving .NET Applic
ation Performance and Scalability 的第 4 章“Architecture and Design Review of .
NET Application for Performance and Scalability”(16Hhttp://msdn.microsoft.com/libr
ary/default.asp?url=/library/en-us/dnpag/html/scalenetchapt04.asp)。
網絡通訊原則
您將面臨的另一個決策是如何設計和使用網絡服務,例如,Web 服務。特別地,您應該考慮與
網絡服務交互的粒度、同步性和頻率。要獲得最佳的性能和可伸縮性,您應該在單個調用中發送
更多的數據,而不是在多個調用中發送較少量的數據。例如,如果您的應用程序允許用戶在定單
中輸入多個項,則較好的做法是為所有項收集數據,然后將完成的采購定單一次性發送給服務,
而不是在多個調用中發送單個項的詳細信息。除了降低與進行大量網絡調用相關聯的系統開銷以
外,這還可以減少服務和/或客戶端內的復雜狀態管理的需要。
應該將您的智能客戶端應用程序設計為盡可能地使用異步通訊,因為這將有助于使用戶界面快速
響應以及并行執行任務。有關如何使用 BeginInvoke 和 EndInvoke 方法異步啟動調用
和檢索數據的詳細信息,請參閱“Asynchronous Programming Overview”(17Hhttp://msdn.
microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpovrasynchro
nousprogrammingoverview.asp)。
注有關設計和構建偶爾連接到網絡的智能客戶端應用程序的詳細信息,請參閱18H第 3 章“建立連
接”和19H第 4 章“偶爾連接的智能客戶端”。
線程處理原則
在應用程序內使用多個線程可能是一種提高其響應性和性能的好方法。特別地,您應該考慮使用
線程來執行可以在后臺安全地完成且不需要用戶交互的處理。通過在后臺執行此類工作,可以使
用戶能夠繼續使用應用程序,并且使應用程序的主用戶界面線程能夠維持應用程序的響應性。
適合于在單獨的線程上完成的處理包括:
? 應用程序初始化。請在后臺線程上執行漫長的初始化,以便用戶能夠盡快地與您的應用程序交互,尤其是
在應用程序功能的重要或主要部分并不依賴于該初始化完成時。
? 遠程服務調用。請在單獨的后臺線程上通過網絡進行所有遠程調用。很難(如果不是無法)保證位于網絡
上的服務的響應時間。在單獨的線程上執行這些調用可以減少發生網絡中斷或延遲的風險,從而避免對應
用程序性能造成不利影響。
? IO 綁定處理。應該在單獨的線程上完成諸如在磁盤上搜索和排序數據之類的處理。通常,這種工作要受
到磁盤 I/O 子系統而不是處理器可用性的限制,因此當該工作在后臺執行時,您的應用程序可以有效地
維持其響應性。
盡管使用多個線程的性能好處可能很顯著,但需要注意,線程使用它們自己的資源,并且使用太
多的線程可能給處理器(它需要管理線程之間的上下文切換)造成負擔。要避免這一點,請考慮
使用線程池,而不是創建和管理您自己的線程。線程池將為您有效地管理線程,重新使用現有的
線程對象,并且盡可能地減小與線程創建和處置相關聯的系統開銷。
如果用戶體驗受到后臺線程所執行的工作的影響,則您應該總是讓用戶了解工作的進度。以這種
方式提供反饋可以增強用戶對您的應用程序的性能的感覺,并且防止他或她假設沒有任何事情發
生。請努力確保用戶可以隨時取消漫長的操作。
您還應該考慮使用 Application 對象的 Idle 事件來執行簡單的操作。Idle 事件提供了使
用單獨的線程來進行后臺處理的簡單替代方案。當應用程序不再有其他用戶界面消息需要處理并
且將要進入空閑狀態時,該事件將激發。您可以通過該事件執行簡單的操作,并且利用用戶不活
動的情況。例如:
[C#]
public Form1()
{
InitializeComponent();
Application.Idle += new EventHandler( OnApplicationIdle );
}
private void OnApplicationIdle( object sender, EventArgs e )
{
}
[Visual Basic .NET]
Public Class Form1
Inherits System.Windows.Forms.Form
Public Sub New()
MyBase.New()
InitializeComponent()
AddHandler Application.Idle, AddressOf OnApplicationIdle
End Sub
Private Sub OnApplicationIdle(ByVal sender As System.Object, ByVal e
As System.EventArgs)
End Sub
End Class
注有關在智能客戶端中使用多個線程的詳細信息,請參閱20H第 6 章“使用多個線程”。
事務原則
事務可以提供重要的支持,以確保不會違反業務規則并維護數據一致性。事務可以確保一組相關
任務作為一個單元成功或失敗。您可以使用事務來維護本地數據庫和其他資源(包括消息隊列的
隊列)之間的一致性。
對于需要在網絡連接不可用時使用脫機緩存數據的智能客戶端應用程序,您應該將事務性數據排
隊,并且在網絡連接可用時將其與服務器進行同步。
您應該避免使用涉及到位于網絡上的資源的分布式事務,因為這些情況可能導致與不斷變化的網
絡和資源響應時間有關的性能問題。如果您的應用程序需要在事務中涉及到位于網絡上的資源,
則應該考慮使用補償事務,以便使您的應用程序能夠在本地事務失敗時取消以前的請求。盡管補
償事務在某些情況下可能不適用,但它們使您的應用程序能夠按照松耦合方式在事務的上下文內
與網絡資源交互,從而減少了不在本地計算機控制之下的資源對應用程序的性能造成不利影響的
可能性。
注有關在智能客戶端中使用事務的詳細信息,請參閱21H第 3 章“建立連接”。
優化應用程序啟動時間
快速的應用程序啟動時間幾乎可以使用戶立即開始與應用程序交互,從而使用戶立刻對應用程序
的性能和可用性產生好感。
當應用程序啟動時,首先加載 CLR,再加載應用程序的主程序集,隨后加載為解析從應用程序
的主窗體中引用的對象的類型所需要的所有程序集。CLR 在該階段不會 加載所有相關程序集;
它僅加載包含主窗體類上的成員變量的類型定義的程序集。在加載了這些程序集之后,實時 (J
IT) 編譯器將在方法運行時編譯方法的代碼(從 Main 方法開始)。同樣,JIT 編譯器不會
編譯您的程序集中的所有代碼。相反,將根據需要逐個方法地編譯代碼。
要盡可能減少應用程序的啟動時間,您應該遵循下列原則:
? 盡可能減少應用程序主窗體類中的成員變量。這將在 CLR 加載主窗體類時盡可能減少必須解析的類型數
量。
? 盡量不要立即使用大型基類程序集(XML 庫或 ADO.NET 庫)中的類型。這些程序集的加載很費時間。
使用應用程序配置類和跟蹤開關功能時將引入 XML 庫。如果要優先考慮應用程序啟動時間,請避免這一
點。
? 盡可能使用惰性加載。僅在需要時獲取數據,而不是提前加載和凍結 UI。
? 將應用程序設計為使用較少的程序集。帶有大量程序集的應用程序會招致性能開銷增加。這些開銷來自加
載元數據、訪問 CLR 中的預編譯映像中的各種內存頁以加載程序集(如果它是用本機映像生成器工具
Ngen.exe 預編譯的)、JIT 編譯時間、安全檢查等等。您應該考慮基于程序集的使用模式來合并程序
集,以便降低相關聯的性能開銷。
? 避免設計將多個組件的功能組合到一個組件中的單一類。將設計分解到多個只須在實際調用時進行編譯的
較小類。
? 將應用程序設計為在初始化期間對網絡服務進行并行調用。通過在初始化期間調用可以并行運行的網絡
服務,可以利用服務代理提供的異步功能。這有助于釋放當前執行的線程并且并發地調用服務以完成任務。
? 使用 NGEN.exe 編譯和試驗 NGen 和非 NGen 程序集,并且確定哪個程序集保存了最大數量的工
作集頁面。NGEN.exe(它隨附在 .NET Framework 中)用于預編譯程序集以創建本機映像,該映像
隨后被存儲在全局程序集緩存的特殊部分,以便應用程序下次需要它時使用。通過創建程序集的本機映像,
可以使程序集更快地加載和執行,因為 CLR 不需要動態生成程序集中包含的代碼和數據結構。有關詳細
信息,請參閱 Improving .NET Application Performance and Scalability 的第 5 章“Improvin
g Managed Code Performance”中的“Working Set Considerations”和“NGen.exe Explained”
部分,網址為:2Hhttp://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/ht
ml/scalenetchapt05.asp。
注如果您使用 NGEN 預編譯程序集,則會立即加載它的所有依賴程序集。
管理可用資源
公共語言運行庫 (CLR) 使用垃圾回收器來管理對象生存期和內存使用。這意味著無法再訪問的
對象將被垃圾回收器自動回收,并且自動回收內存。由于多種原因無法再訪問對象。例如,可能
沒有對該對象的任何引用,或者對該對象的所有引用可能來自其他可作為當前回收周期的一部分
進行回收的對象。盡管自動垃圾回收使您的代碼不必負責管理對象刪除,但這意味著您的代碼不
再對對象的確切刪除時間具有顯式控制。
請考慮下列原則,以確保您能夠有效地管理可用資源:
? 確保在被調用方對象提供 Dispose 方法時該方法得到調用。如果您的代碼調用了支持 Dispose 方法
的對象,則您應該確保在使用完該對象之后立即調用此方法。調用 Dispose 方法可以確保搶先釋放非
托管資源,而不是等到發生垃圾回收。除了提供 Dispose 方法以外,某些對象還提供其他管理資源的
方法,例如,Close 方法。在這些情況下,您應該參考文檔資料以了解如何使用其他方法。例如,對于
SqlConnection 對象而言,調用 Close 或 Dispose 都足可以搶先將數據庫連接釋放回連接池中。
一種可以確保您在對象使用完畢之后立即調用 Dispose 的方法是使用 Visual C# .NET 中的 usin
g 語句或 Visual Basic .NET 中的 Try/Finally 塊。
下面的代碼片段演示了 Dispose 的用法。
C# 中的 using 語句示例:
using( StreamReader myFile = new StreamReader("C://ReadMe.Txt")){
string contents = myFile.ReadToEnd();
//... use the contents of the file
} // dispose is called and the StreamReader's resources
released
Visual Basic .NET 中的 Try/Finally 塊示例:
Dim myFile As StreamReader
myFile = New StreamReader("C://ReadMe.Txt")
Try
String contents = myFile.ReadToEnd()
'... use the contents of the file
Finally
myFile.Close()
End Try
注在 C# 和 C++ 中,Finalize 方法是作為析構函數實現的。在 Visual Basic .NET 中,Finalize
方法是作為 Object 基類上的 Finalize 子例程的重寫實現的。
? 如果您在客戶端調用過程中占據非托管資源,則請提供 Finalize 和 Dispose 方法。如果您在公共或
受保護的方法調用中創建訪問非托管資源的對象,則應用程序需要控制非托管資源的生存期。在圖 8.1
中,第一種情況是對非托管資源的調用,在此將打開、獲取和關閉資源。在此情況下,您的對象無須提供
Finalize 和 Dispose 方法。在第二種情況下,在方法調用過程中占據非托管資源;因此,您的對象
應該提供 Finalize 和 Dispose 方法,以便客戶端在使用完該對象后可以立即顯式釋放資源。
圖 8.1:Dispose 和 Finalize 方法調用的用法
垃圾回收通常有利于提高總體性能,因為它將速度的重要性置于內存利用率之上。只有當內存資
源不足時,才需要刪除對象;否則,將使用所有可用的應用程序資源以使您的應用程序受益。但
是,如果您的對象保持對非托管資源(例如,窗口句柄、文件、GDI 對象和網絡連接)的引用,
則程序員通過在這些資源不再使用時顯式釋放它們可以獲得更好的性能。如果您要在客戶端方法
調用過程中占據非托管資源,則對象應該允許調用方使用 IDisposable 接口(它提供 Disp
ose 方法)顯式管理資源。通過實現 IDisposable,對象將通知它可被要求明確進行清理,
而不是等待垃圾回收。實現 IDisposable 的對象的調用方在使用完該對象后將簡單地調用
Dispose 方法,以便它可以根據需要釋放資源。
有關如何在某個對象上實現 IDisposable 的詳細信息,請參閱 Improving .NET Applicat
ion Performance and Scalability 中的第 5 章“Improving Managed Code Performa
nce”,網址為:23Hhttp://msdn.microsoft.com/library/default.asp?url=/library/en-us/d
npag/html/scalenetchapt05.asp。
注如果您的可處置對象派生自另一個也實現了 IDisposable 接口的對象,則您應該調用基類的
Dispose 方法以使其可以清理它的資源。您還應該調用實現了 IDisposable 接口的對象
所擁有的所有對象上的 Dispose。
Finalize 方法也使您的對象可以在刪除時顯式釋放其引用的任何資源。由于垃圾回收器所具有
的非確定性,在某些情況下,Finalize 方法可能長時間不會被調用。實際上,如果您的應用程
序在垃圾回收器刪除對象之前終止,則該方法可能永遠不會被調用。然而,需要使用 Finalize
方法作為一種后備策略,以防調用方沒有顯式調用 Dispose 方法(Dispose 和 Finalize
方法共享相同的資源清理代碼)。通過這種方式,可能在某個時刻釋放資源,即使這發生在最
佳時刻之后。
注要確保 Dispose 和 Finalize 中的清理代碼不會被調用兩次,您應該調用 GC.Suppress
Finalize 以通知垃圾回收器不要調用 Finalize 方法。
垃圾回收器實現了 Collect 方法,該方法強制垃圾回收器刪除所有對象掛起刪除。不應該從應
用程序內調用該方法,因為回收周期在高優先級線程上運行。回收周期可能凍結所有 UI 線程,
從而使得用戶界面停止響應。
有關詳細信息,請參閱 Improving .NET Application Performance and Scalability 中的
“Garbage Collection Guidelines”、“Finalize and Dispose Guidelines”、“Dispose Pa
ttern”和“Finalize and Dispose Guidelines”,網址為:24Hhttp://msdn.microsoft.com/libr
ary/default.asp?url=/library/en-us/dnpag/html/scalenetchapt05.asp。
優化 Windows 窗體性能
Windows 窗體為智能客戶端應用程序提供了內容豐富的用戶界面,并且您可以使用許多種技術
來幫助確保 Windows 窗體提供最佳性能。在討論特定技術之前,對一些可以顯著提高 Wind
ows 窗體性能的高級原則進行回顧是有用的。
? 小心創建句柄。Windows 窗體將句柄創建虛擬化(即,它動態創建和重新創建窗口句柄對象)。創建句
柄對象的系統開銷可能非常大;因此,請避免進行不必要的邊框樣式更改或者更改 MDI 父對象。
? 避免創建帶有太多子控件的應用程序。Microsoft? Windows? 操作系統限制每個進程最多有 10,000
個控件,但您應該避免在窗體上使用成百上千個控件,因為每個控件都要消耗內存資源。
本節的其余部分討論您可以用來優化應用程序用戶界面性能的更為具體的技術。
使用 BeginUpdate 和 EndUpdate
許多 Windows 窗體控件(例如,ListView 和 TreeView 控件)實現了 BeginUpdate
和 EndUpdate 方法,它們在操縱基礎數據或控件屬性時取消了控件的重新繪制。通過使用
BeginUpdate 和 EndUpdate 方法,您可以對控件進行重大更改,并且避免在應用這些
更改時讓控件經常重新繪制自身。此類重新繪制會導致性能顯著降低,并且用戶界面閃爍且不反
應。
例如,如果您的應用程序具有一個要求添加大量節點項的樹控件,則您應該調用 BeginUpda
te,添加所有必需的節點項,然后調用 EndUpdate。下面的代碼示例顯示了一個樹控件,該
控件用于顯示許多個客戶的層次結構表示形式及其定單信息。
[C#]
// Suppress repainting the TreeView until all the objects have been c
reated.
TreeView1.BeginUpdate();
// Clear the TreeView.
TreeView1.Nodes.Clear();
// Add a root TreeNode for each Customer object in the ArrayList.
foreach( Customer customer2 in customerArray )
{
TreeView1.Nodes.Add( new TreeNode( customer2.CustomerName ) );
// Add a child TreeNode for each Order object in the current Custome
r.
foreach( Order order1 in customer2.CustomerOrders )
{
TreeView1.Nodes[ customerArray.IndexOf(customer2) ].Nodes.Add(
new TreeNode( customer2.CustomerName + "." + order1.OrderID ) );
}
}
// Begin repainting the TreeView.
TreeView1.EndUpdate();
[Visual Basic .NET]
' Suppress repainting the TreeView until all the objects have
been created.
TreeView1.BeginUpdate()
' Clear the TreeView
TreeView1.Nodes.Clear()
' Add a root TreeNode for each Customer object in the ArrayList
For Each customer2 As Customer In customerArray
TreeView1.Nodes.Add(New TreeNode(customer2.CustomerName))
' Add a child TreeNode for each Order object in the current Customer.
For Each order1 As Order In customer2.CustomerOrders
TreeView1.Nodes(Array.IndexOf(customerArray, customer2)).Nodes.Add( _
New TreeNode(customer2.CustomerName & "." & order1.OrderID))
Next
Next
' Begin repainting the TreeView.
TreeView1.EndUpdate()
即使在您不希望向控件添加許多對象時,您也應該使用 BeginUpdate 和 EndUpdate 方
法。在大多數情況下,您在運行之前將不知道要添加的項的確切個數。因此,為了妥善處理大量
數據以及應付將來的要求,您應該總是調用 BeginUpdate 和 EndUpdate 方法。
注調用 Windows 窗體控件使用的許多 Collection 類的 AddRange 方法時,將自動為您
調用 BeginUpdate 和 EndUpdate 方法。
使用 SuspendLayout 和 ResumeLayout
許多 Windows 窗體控件(例如,ListView 和 TreeView 控件)都實現了 SuspendLa
yout 和 ResumeLayout 方法,它們能夠防止控件在添加子控件時創建多個布局事件。
如果您的控件以編程方式添加和刪除子控件或者執行動態布局,則您應該調用 SuspendLay
out 和 ResumeLayout 方法。通過 SuspendLayout 方法,可以在控件上執行多個操
作,而不必為每個更改執行布局。例如,如果您調整控件的大小并移動控件,則每個操作都將引
發單獨的布局事件。
這些方法按照與 BeginUpdate 和 EndUpdate 方法類似的方式操作,并且在性能和用戶
界面穩定性方面提供相同的好處。
下面的示例以編程方式向父窗體中添加按鈕:
[C#]
private void AddButtons()
{
// Suspend the form layout and add two buttons.
this.SuspendLayout();
Button buttonOK = new Button();
buttonOK.Location = new Point(10, 10);
buttonOK.Size = new Size(75, 25);
buttonOK.Text = "OK";
Button buttonCancel = new Button();
buttonCancel.Location = new Point(90, 10);
buttonCancel.Size = new Size(75, 25);
buttonCancel.Text = "Cancel";
this.Controls.AddRange(new Control[]{buttonOK, buttonCancel});
this.ResumeLayout();
}
[Visual Basic .NET]
Private Sub AddButtons()
' Suspend the form layout and add two buttons
Me.SuspendLayout()
Dim buttonOK As New Button
buttonOK.Location = New Point(10, 10)
buttonOK.Size = New Size(75, 25)
buttonOK.Text = "OK"
Dim buttonCancel As New Button
buttonCancel.Location = New Point(90, 10)
buttonCancel.Size = New Size(75, 25)
buttonCancel.Text = "Cancel"
Me.Controls.AddRange(New Control() { buttonOK, buttonCancel } )
Me.ResumeLayout()
End Sub
每當您添加或刪除控件、執行子控件的自動布局或者設置任何影響控件布局的屬性(例如,大小、
位置、定位點或停靠屬性)時,您都應該使用 SuspendLayout 和 ResumeLayout 方法。
處理圖像
如果您的應用程序顯示大量圖像文件(例如,.jpg 和 .gif 文件),則您可以通過以位圖格式
預先呈現圖像來顯著改善顯示性能。
要使用該技術,請首先從文件中加載圖像,然后使用 PARGB 格式將其呈現為位圖。下面的代
碼示例從磁盤中加載文件,然后使用該類將圖像呈現為預乘的、Alpha 混合 RGB 格式。例如:
[C#]
if ( image != null && image is Bitmap )
{
Bitmap bm = (Bitmap)image;
Bitmap newImage = new Bitmap( bm.Width, bm.Height,
System.Drawing.Imaging.PixelFormat.Format32bppPArgb );
using ( Graphics g = Graphics.FromImage( newImage ) )
{
g.DrawImage( bm, new Rectangle( 0,0, bm.Width, bm.Height ) );
}
image = newImage;
}
[Visual Basic .NET]
If Not(image Is Nothing) AndAlso (TypeOf image Is Bitmap) Th
en
Dim bm As Bitmap = CType(image, Bitmap)
Dim newImage As New Bitmap(bm.Width, bm.Height, _
System.Drawing.Imaging.PixelFormat.Format32bppPArgb)
Using g As Graphics = Graphics.FromImage(newImage)
g.DrawImage(bm, New Rectangle(0, 0, bm.Width, bm.Height))
End Using
image = newImage
End If
使用分頁和惰性加載
在大多數情況下,您應該僅在需要時檢索或顯示數據。如果您的應用程序需要檢索和顯示大量信
息,則您應該考慮將數據分解到多個頁面中,并且一次顯示一頁數據。這可以使用戶界面具有更
高的性能,因為它無須顯示大量數據。此外,這可以提高應用程序的可用性,因為用戶不會同時
面對大量數據,并且可以更加容易地導航以查找他或她需要的確切數據。
例如,如果您的應用程序顯示來自大型產品目錄的產品數據,則您可以按照字母順序顯示這些項,
并且將所有以“A”開頭的產品顯示在一個頁面上,將所有以“B”開頭的產品顯示在下一個頁面上。
然后,您可以讓用戶直接導航到適當的頁面,以便他或她無須瀏覽所有頁面就可以獲得他或她需
要的數據。
以這種方式將數據分頁還使您可以根據需要獲取后臺的數據。例如,您可能只需要獲取第一頁信
息以便顯示并且讓用戶與其進行交互。然后,您可以獲取后臺中的、已經準備好供用戶使用的下
一頁數據。該技術在與數據緩存技術結合使用時可能特別有效。
您還可以通過使用惰性加載技術來提高智能客戶端應用程序的性能。您無須立即加載可能在將來
某個時刻需要的數據或資源,而是可以根據需要加載它們。您可以在構建大型列表或樹結構時使
用惰性加載來提高用戶界面的性能。在此情況下,您可以在用戶需要看到數據時(例如,在用戶
展開樹節點時)加載它。
優化顯示速度
根據您用于顯示用戶界面控件和應用程序窗體的技術,您可以用多種不同的方式來優化應用程序
的顯示速度。
當您的應用程序啟動時,您應該考慮盡可能地顯示簡單的用戶界面。這將減少啟動時間,并且向
用戶呈現整潔且易于使用的用戶界面。而且,您應該努力避免引用類以及在啟動時加載任何不會
立刻需要的數據。這將減少應用程序和 .NET Framework 初始化時間,并且提高應用程序的
顯示速度。
當您需要顯示對話框或窗體時,您應該在它們做好顯示準備之前使其保持隱藏狀態,以便減少需
要的繪制工作量。這將有助于確保窗體僅在初始化之后顯示。
如果您的應用程序具有的控件含有覆蓋整個客戶端表面區域的子控件,則您應該考慮將控件背景
樣式設置為不透明。這可以避免在發生每個繪制事件時重繪控件的背景。您可以通過使用 Set
Style 方法來設置控件的樣式。使用 ControlsStyles.Opaque 枚舉可以指定不透明控件
樣式。
您應該避免任何不必要的控件重新繪制操作。一種方法是在設置控件的屬性時隱藏控件。在 O
nPaint 事件中具有復雜繪圖代碼的應用程序能夠只重繪窗體的無效區域,而不是繪制整個窗
體。OnPaint 事件的 PaintEventArgs 參數包含一個 ClipRect 結構,它指示窗口的哪
個部分無效。這可以減少用戶等待查看完整顯示的時間。
使用標準的繪圖優化,例如,剪輯、雙緩沖和 ClipRectangle。這還將通過防止對不可見或
要求重繪的顯示部分執行不必要的繪制操作,從而有助于改善智能客戶端應用程序的顯示性能。
有關增強繪圖性能的詳細信息,請參閱 Painting techniques using Windows Forms for
the Microsoft .NET Framework,網址為:25Hhttp://windowsforms.net/articles/window
sformspainting.aspx。
如果您的顯示包含動畫或者經常更改某個顯示元素,則您應該使用雙緩沖或多緩沖,在繪制當前
圖像的過程中準備下一個圖像。System.Windows.Forms 命名空間中的 ControlStyle
s 枚舉適用于許多控件,并且 DoubleBuffer 成員可以幫助防止閃爍。啟用 DoubleBuff
er 樣式將使您的控件繪制在離屏緩沖中完成,然后同時繪制到屏幕上。盡管這有助于防止閃爍,
但它的確為分配的緩沖區使用了更多內存。
26H 27H返回頁首
性能調整和診斷
在設計和實現階段處理性能問題是實現應用程序性能目標的最劃算的方法。但是,您只有在開發
階段經常且盡早測試應用程序的性能,才能真正有效地優化應用程序的性能。
盡管針對性能進行設計和測試都很重要,但在這些早期階段優化每個組件和所有代碼不是有效的
資源用法,因此應該予以避免。所以,應用程序可能存在您在設計階段未預料到的性能問題。例
如,您可能遇到由于兩個系統或組件之間的無法預料的交互而產生的性能問題,或者您可能使用
原來存在的、未按希望的方式執行的代碼。在此情況下,您需要追究性能問題的根源,以便您可
以適當地解決該問題。
本節討論一些將幫助您診斷性能問題以及調整應用程序以獲得最佳性能的工具和技術。
制定性能目標
當您設計和規劃智能客戶端應用程序時,您應該仔細考慮性能方面的要求,并且定義合適的性能
目標。在定義這些目標時,請考慮您將如何度量應用程序的實際性能。您的性能度量標準應該明
確體現應用程序的重要性能特征。請努力避免無法準確度量的模糊或不完整的目標,例如,“應
用程序必須快速運行”或“應用程序必須快速加載”。您需要了解應用程序的性能和可伸縮性目標,
以便您可以設法滿足這些目標并且圍繞它們來規劃您的測試。請確保您的目標是可度量的和可驗
證的。
定義良好的性能度量標準使您可以準確跟蹤應用程序的性能,以便您可以確定應用程序是否能夠
滿足它的性能目標。這些度量標準應該包括在應用程序測試計劃中,以便可以在應用程序的測試
階段度量它們。
本節重點討論與智能客戶端應用程序相關的特定性能目標的定義。如果您還要設計和生成客戶端
應用程序將消耗的網絡服務,則您還需要為這些服務定義適當的性能目標。在此情況下,您應該
確保考慮整個系統的性能要求,以及應用程序各個部分的性能與其他部分以及整個系統之間存在
怎樣的關系。
考慮用戶的觀點
當您為智能客戶端應用程序確定合適的性能目標時,您應該仔細考慮用戶的觀點。對于智能客戶
端應用程序而言,性能與可用性和用戶感受有關。例如,只要用戶能夠繼續工作并且獲得有關操
作進度的足夠反饋,用戶就可以接受漫長的操作。
在確定要求時,將應用程序的功能分解為多個使用情景或使用案例通常是有用的。您應該識別對
于實現特定性能目標而言關鍵且必需的使用案例和情景。應該將許多使用案例所共有且經常執行
的任務設計得具有較高性能。同樣,如果任務要求用戶全神貫注并且不允許用戶從其切換以執行
其他任務,則需要提供優化的且有效的用戶體驗。如果任務不太經常使用且不會阻止用戶執行其
他任務,則可能無須進行大量調整。
對于您識別的每個性能敏感型任務,您都應該精確地定義用戶的操作以及應用程序的響應方式。
您還應該確定每個任務使用的網絡和客戶端資源或組件。該信息將影響性能目標,并且將驅動對
性能進行度量的測試。
可用性研究提供了非常有價值的信息源,并且可能大大影響性能目標的定義。正式的可用性研究
在確定用戶如何執行他們的工作、哪些使用情景是共有的以及哪些不是共有的、用戶經常執行哪
些任務以及從性能觀點看來應用程序的哪些特征是重要的等方面可能非常有用。如果您要生成新
的應用程序,您應該考慮提供應用程序的原型或模型,以便可以執行基本的可用性測試。
考慮應用程序操作環境
對應用程序的操作環境進行評估是很重要的,因為這可能對應用程序施加必須在您制定的性能目
標中予以反映的約束。
位于網絡上的服務可能對您的應用程序施加性能約束。例如,您可能需要與您無法控制的 Web
服務進行交互。在這種情況下,需要確定該服務的性能,并且確定這是否將對客戶端應用程序
的性能產生影響。
您還應該確定任何相關服務和組件的性能如何隨著時間的變化而變化。某些系統會經受相當穩定
的使用,而其他系統則會在一天或一周的特定時間經受變動極大的使用。這些區別可能在關鍵時
間對應用程序的性能造成不利影響。例如,提供應用程序部署和更新服務的服務可能會在星期一
早上 9 點緩慢響應,因為所有用戶都在此時升級到應用程序的最新版本。
另外,還需要準確地對所有相關系統和組件的性能進行建模,以便可以在嚴格模擬應用程序的實
際部署環境的環境中測試您的應用程序。對于每個系統,您都應該確定性能概況以及最低、平均
和最高性能特征。然后,您可以在定義應用程序的性能要求時根據需要使用該數據。
您還應該仔細考慮用于運行應用程序的硬件。您將需要確定在處理器、內存、圖形功能等方面的
目標硬件配置,或者至少確定一個如果得不到滿足則無法保證性能的最低配置。
通常,應用程序的業務操作環境將規定一些更為苛刻的性能要求。例如,執行實時股票交易的應
用程序將需要執行這些交易并及時顯示所有相關數據。
性能調整過程
對應用程序進行性能調整是一個迭代過程。該過程由一些重復執行直至應用程序滿足其性能目標
的階段組成。(請參見圖 8.2。)
圖 8.2:性能調整過程
正如圖 8.2 所闡明的,性能調整要求您完成下列過程:
? 建立基準。在您開始針對性能調整應用程序時,您必須具有與性能目標、目標和度量標準有關的定義良好
的基準。這可能包括應用程序工作集大小、加載數據(例如,目錄)的時間、事務持續時間等等。
? 收集數據。您將需要通過針對您已經定義的性能目標度量應用程序的性能,來對應用程序性能進行評價。
性能目標應該體現特定的且可度量的度量標準,以使您可以在任何時刻量化應用程序的性能。要使您可以
收集性能數據,您可能必須對應用程序進行規范,以便可以發布和收集必需的性能數據。下一節將詳細討
論您可以用來完成這一工作的一些選項。
? 分析結果。在收集應用程序的性能數據之后,您將能夠通過確定哪些應用程序功能要求最多的關注,來區
分性能調整工作的輕重緩急。此外,您可以使用該數據來確定任何性能瓶頸的位置。通常,您將只能夠通
過收集更詳細的性能數據來確定瓶頸的確切位置:例如,通過使用應用程序規范。性能分析工具可能幫助
您識別瓶頸。
? 調整應用程序。在已經識別瓶頸之后,您可能需要修改應用程序或其配置,以便嘗試解決問題。您應該致
力于將更改降低至最低限度,以便可以確定更改對應用程序性能的影響。如果您同時進行多項更改,可能
難以確定每項更改對應用程序的總體性能的影響。
? 測試和度量。在更改應用程序或其配置之后,您應該再次測試它以確定更改具有的效果,并且使新的性能
數據得以收集。性能工作通常要求進行體系結構或其他具有較高影響的更改,因此徹底的測試是很關鍵的。
您的應用程序測試計劃應該針對預料到的所有情況,在配置了適當硬件和軟件的客戶計算機上演習應用程
序所實現的完整范圍的功能。如果您的應用程序使用網絡資源,則應該加載這些資源,以便您可以獲得有
關應用程序在此類環境中所具有的性能的準確度量。
上述過程將使您可以通過針對特定目標度量應用程序的總體性能,來重點解決特定的性能問題。
性能工具
您可以使用許多工具來幫助您收集和分析應用程序的性能數據。本節中介紹的每種工具都具有不
同的功能,您可以使用這些功能來度量、分析和查找應用程序中的性能瓶頸。
注除了這里介紹的工具以外,您還可以使用其他一些選項和第三方工具。有關其他日志記錄和異
常管理選項的說明,請參閱Exception Management Architecture Guide,網址為:
28Hhttp://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/exc
eptdotnet.asp
在決定哪些工具最適合您的需要之前,您應該仔細考慮您的確切要求。
使用性能日志和警報
性能日志和警報是作為 Windows 操作系統的一部分發行的一種管理性能監控工具。它依靠由
各種 Windows 組件、子系統和應用程序發布的性能計數器,使您可以跟蹤資源使用情況以及
針對時間以圖形方式繪制它們。
您可以使用 Performance Logs and Alerts 來監控標準的性能計數器(例如,內存使用情況
或處理器使用情況),或者您可以定義您自己的自定義計數器來監控應用程序特定的活動。
.NET CLR 提供了許多有用的性能計數器,它們使您可以洞察應用程序性能的好壞。關系比較
大的一些性能對象是:
? .NET CLR 內存。提供有關托管 .NET 應用程序內存使用情況的數據,包括應用程序正在使用的內存數
量以及對未使用的對象進行垃圾回收所花費的時間。
? .NET CLR 加載。提供有關應用程序正在使用的類和應用程序域的數量的數據,并且提供有關它們的加
載和卸載速率的數據。
? .NET CLR 鎖和線程。提供與應用程序內使用的線程有關的性能數據,包括線程個數以及試圖同時對受
保護的資源進行訪問的線程之間的爭用率。
? .NET CLR 網絡。提供與通過網絡發送和接收數據有關的性能計數器,包括每秒發送和接收的字節數以
及活動連接的個數。
? .NET CLR 異常。提供有關應用程序所引發和捕獲的異常個數的報告。
有關上述計數器、它們的閾值、要度量的內容以及如何度量它們的詳細信息,請參閱 Improvi
ng .NET Application Performance and Scalability 的第 15 章“Measuring .NET Ap
plication Performance”中的“CLR and Managed Code”部分,網址為:29Hhttp://msdn.mi
crosoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenetchapt15.a
sp。
您的應用程序還可以提供您可以通過使用性能日志和警報輕松監控的、應用程序特定的性能計數
器。您可以像以下示例所顯示的那樣,定義自定義性能計數器:
[C#]
PerformanceCounter counter = new PerformanceCounter( "Category",
"CounterName", false );
[Visual Basic .NET]
Dim counter As New PerformanceCounter("Category", "CounterName", Fals
e)
在創建性能計數器對象之后,您可以為您的自定義性能計數器指定類別,并將所有相關計數器保
存在一起。PerformanceCounter 類在 System.Diagnostics 命名空間中定義,該命
名空間中還定義了其他一些可用于讀取和定義性能計數器和類別的類。有關創建自定義性能計數
器的詳細信息,請參閱知識庫中編號為 317679 的文章“How to create and make chang
es to a custom counter for the Windows Performance Monitor by using Visual
Basic .NET”,網址為:30Hhttp://support.microsoft.com/default.aspx?scid=kb;en-us;31
7679。
注要注冊性能計數器,您必須首先注冊該類別。您必須具有足夠的權限才能注冊性能計數器類別
(它可能影響您部署應用程序的方式)。
規范
您可以使用許多工具和技術來幫助您對應用程序進行規范,并且生成度量應用程序性能所需的信
息。這些工具和技術包括:
? Event Tracing for Windows (ETW)。該 ETW 子系統提供了一種系統開銷較低(與性能日志和
警報相比)的手段,用以監控具有負載的系統的性能。這主要用于必須頻繁記錄事件、錯誤、警告或審核
的服務器應用程序。有關詳細信息,請參閱 Microsoft Platform SDK 中的“Event Tracing”,網址為:
31Hhttp://msdn.microsoft.com/library/default.asp?url=/library/en-us/perfmon/base/event_tr
acing.asp。
? Enterprise Instrumentation Framework (EIF)。EIF 是一種可擴展且可配置的框架,您可以
使用它來對智能客戶端應用程序進行規劃。它提供了一種可擴展的事件架構和統一的 API — 它使用 W
indows 中內置的現有事件、日志記錄和跟蹤機制,包括 Windows Management Instrumentation
(WMI)、Windows Event Log 和 Windows Event Tracing。它大大簡化了發布應用程序事件所需
的編碼。如果您計劃使用 EIF,則需要通過使用 EIF .msi 在客戶計算機上安裝 EIF。如果您要在智能
客戶端應用程序中使用 EIF,則需要在決定應用程序的部署方式時考慮這一要求。有關詳細信息,請參
閱“How To:Use EIF”,網址為:32Hhttp://msdn.microsoft.com/library/default.asp?url=/library/
en-us/dnpag/html/scalenethowto14.asp。
? Logging Application Block。Logging Application Block 提供了可擴展且可重用的代碼組件,
以幫助您生成規范化的應用程序。它建立在 EIF 的功能基礎之上,以提供某些功能,例如,針對事件架
構的增強功能、多個日志級別、附加的事件接收等等。有關詳細信息,請參閱“Logging Application Bl
ock”,網址為“3Hhttp://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/htm
l/Logging.asp。
? Windows Management Instrumentation (WMI)。WMI 組件是 Windows 操作系統的一部
分,并且提供了用于訪問企業中的管理信息和控件的編程接口。系統管理員常用它來自動完成管理任務(通
過使用調用 WMI 組件的腳本)。有關詳細信息,請參閱 Windows Management Information,網
址為:34Hhttp://msdn.microsoft.com/library/default.asp?url=/library/en-us/wmisdk/wmi/wmi
_start_page.asp。
? 調試和跟蹤類。.NET Framework 在 System.Diagnosis 下提供了 Debug 和 Trace 類來對代
碼進行規范。Debug 類主要用于打印調試信息以及檢查是否有斷言。Trace 類使您可以對發布版本進
行規范,以便在運行時監控應用程序的完好狀況。在 Visual Studio .NET 中,默認情況下啟用跟蹤。
在使用命令行版本時,您必須為編譯器添加 /d:Trace 標志,或者在 Visual C# .NET 源代碼中添加
#define TRACE,以便啟用跟蹤。對于 Visual Basic .NET 源代碼,您必須為命令行編譯器添加
/d:TRACE=True。有關詳細信息,請參閱知識庫中編號為 815788 的文章“HOW TO:Trace and
Debug in Visual C# .NET”,網址為:35Hhttp://support.microsoft.com/default.aspx?scid=kb;e
n-us;815788。
CLR Profiler
CLR Profiler 是 Microsoft 提供的一種內存分析工具,并且可以從 MSDN 下載。它使您能
夠查看應用程序進程的托管堆以及調查垃圾回收器的行為。使用該工具,您可以獲取有關應用程
序的執行、內存分配和內存消耗的有用信息。這些信息可以幫助您了解應用程序的內存使用方式
以及如何優化應用程序的內存使用情況。
CLR Profiler 可從 36Hhttp://msdn.microsoft.com/netframework/downloads/tools/defa
ult.aspx 獲得。有關如何使用 CLR Profiler 工具的詳細信息,另請參閱“How to use CLR
Profiler”,網址為:37Hhttp://msdn.microsoft.com/library/default.asp?url=/library/en-u
s/dnpag/html/scalenethowto13.asp?frame=true。
CLR Profiler 在日志文件中記錄內存消耗和垃圾回收器行為信息。然后,您可以使用一些不同
的圖形視圖,通過 CLR Profiler 來分析該數據。一些比較重要的視圖是:
? Allocation Graph。顯示有關對象分配方式的調用堆棧。您可以使用該視圖來查看方法進行的每個分
配的系統開銷,隔離您不希望發生的分配,以及查看方法可能進行的過度分配。
? Assembly, Module, Function, and Class Graph。顯示哪些方法造成了哪些程序集、函數、
模塊或類的加載。
? Call Graph。使您可以查看哪些方法調用了其他哪些方法以及相應的調用頻率。您可以使用該圖表來確
定庫調用的系統開銷,以及調用了哪些方法或對特定方法進行了多少個調用。
? Time Line。提供了有關應用程序執行的基于文本的、按時間順序的層次結構視圖。使用該視圖可以查
看分配了哪些類型以及這些類型的大小。您還可以使用該視圖查看方法調用使得哪些程序集被加載,并且
分析您不希望發生的分配。您可以分析完成器的使用情況,并且識別尚未實現或調用 Close 或 Dispo
se 從而導致瓶頸的方法。
您可以使用 CLR Profiler.exe 來識別和隔離與垃圾回收有關的問題。這包括內存消耗問題(例
如,過度或未知的分配、內存泄漏、生存期很長的對象)以及在執行垃圾回收時花費的時間的百
分比。
注有關如何使用 CLR Profiler 工具的詳細信息,請參閱“Improving .NET Application Per
formance and Scalability”,網址為:38Hhttp://msdn.microsoft.com/library/default.asp?
url=/library/en-us/dnpag/html/scalenethowto13.asp?frame=true。
39H 40H返回頁首
小結
要完全實現智能客戶端應用程序的潛能,您需要在應用程序的設計階段仔細考慮性能問題。通過
在早期階段解決這些性能問題,您可以在應用程序設計過程中控制成本,并減小在開發周期的后
期陷入性能問題的可能性。
本章分析了許多不同的技術,您可以在規劃和設計智能客戶端應用程序時使用這些技術,以確保
優化它們的性能。本章還考察了您可以用來確定智能客戶端應用程序內的性能問題的一些工具和
技術。
41H 42H返回頁首
參考資料
有關詳細信息,請參閱以下內容:
? 43Hhttp://msdn.microsoft.com/perf
? 4Hhttp://www.windowsforms.net/Default.aspx
? 45Hhttp://msdn.microsoft.com/vstudio/using/understand/perf/
? 46Hhttp://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetcomp/html/netcfi
mproveformloadperf.asp
? 47Hhttp://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndotnet/html/highperf
managedapps.asp
? 48Hhttp://msdn.microsoft.com/msdnmag/issues/02/08/AdvancedBasics/default.aspx
? 49Hhttp://msdn.microsoft.com/library/default.asp?url=/msdnmag/issues/04/01/NET/toc.asp?
frame=true
? 50Hhttp://msdn.microsoft.com/library/default.asp?url=/msdnmag/issues/03/02/Multithreadin
g/toc.asp?frame=true
posted on 2007-12-10 17:04 51H寒蟬 閱讀(288) 52H評論(0) 53H編輯 54H收藏 所屬分類: 5HSmart
Client(智能客戶端)系列學習筆記
今天看 MSDN 里頭的 C# 委托示例,代碼中的注釋是英文的,看著不舒服,我把它翻譯了一下,
示例中提到的書也換成了中文的,
using System;
// 書店
namespace Bookstore
{
using System.Collections;
// 描述圖書列表中的書
public struct Book
{
public string Title; // 書名
public string Author; // 作者
public decimal Price; // 價格
public bool Paperback; // 是不是平裝
public Book(string title, string author, decimal price, bool paperBack)
{
Title = title;
Author = author;
Price = price;
Paperback = paperBack;
}
}
// 聲明一個委托類型,用來處理圖書:
public delegate void ProcessBookDelegate(Book book);
// 維護一個圖書數據庫
public class BookDB
{
// 數據庫中所有圖書的列表
ArrayList list = new ArrayList();
// 向數據庫添加一本書
public void AddBook(string title, string author, decimal price, bool paperBack)
{
list.Add(new Book(title, author, price, paperBack));
}
// 調用傳遞進來的委托,處理每一本書
public void ProcessPaperbackBooks(ProcessBookDelegate processBook)
{
foreach (Book b in list)
{
if (b.Paperback)
// 調用委托:
processBook(b);
}
}
}
}
// 使用 Bookstore 中的類:
namespace BookTestClient
{
using Bookstore;
// 計算圖書總價和平均價格
class PriceTotaller
{
int countBooks = 0;// 圖書本數
decimal priceBooks = 0.0m;// 圖書總價
// 添加圖書
internal void AddBookToTotal(Book book)
{
countBooks += 1;
priceBooks += book.Price;
}
// 平均價格
internal decimal AveragePrice()
{
return priceBooks / countBooks;
}
}
// 這個類用來測試圖書數據庫
class Test
{
// 打印書名
static void PrintTitle(Book b)
{
Console.WriteLine(" {0}", b.Title);
}
// 主函數,程序從這里開始執行
static void Main()
{
BookDB bookDB = new BookDB();
// 用幾本書初始化數據庫
AddBooks(bookDB);
// 打印所有平裝書的書名
Console.WriteLine("平裝書:");
// 創建一個與靜態方法 Test.PrintTitle 相關聯的委托:
bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(PrintTitle));
// 用一個 PriceTotaller 對象獲取平裝書的平均價格
PriceTotaller totaller = new PriceTotaller();
// 創建一個與對象 totaller 的實例方法AddBookToTotal 相關聯的委托實例
bookDB.ProcessPaperbackBooks(new
ProcessBookDelegate(totaller.AddBookToTotal));
Console.WriteLine(" 平裝書的平均價格是: ${0:#.##}",
totaller.AveragePrice());
}
// 用幾本書初始化數據庫
static void AddBooks(BookDB bookDB)
{
bookDB.AddBook("吶喊", "魯迅", 19.95m,true);
bookDB.AddBook("我的人生哲學","王蒙", 39.95m,true);
bookDB.AddBook("三重門", "韓寒", 129.95m,false);
bookDB.AddBook("那小子真帥", "可愛淘", 12.00m,true);
}
}
}
C# 異步調用機制的理解(一) - 思索的秋天 - 博客園
通常,在進行一個對象的方法調用時,對象執行期間客戶端通常都是堵塞的,只有等方法執行完
畢返回控制權時才會回到客戶端,然而某些時候,我們需要異步調用方法,即對象在后臺執行方
法調用,控制權可以立即返回到客戶端,隨后能以某種方式通知客戶端已經執行完畢,這種執行
模式就是所謂的異步調用方法,而操作就是大家所知的異步調用。異步操作通常用于執行完成時
間可能較長的任務,如打開大文件、連接遠程計算機或查詢數據庫。異步操作在主應用程序線程
以外的線程中執行。應用程序調用方法異步執行某個操作時,應用程序可在異步方法執行其任務
時繼續執行。
.NET Framework 為異步操作提供兩種設計模式:
使用 IAsyncResult 對象的異步操作。
使用事件的異步操作
今天主要講的是使用 IAsyncResult 對象的異步操作,利用一個委托來進行異步編程。
一: 異步調用編程模型
支持異步調用使用多線程肯定是必須的,但是如果每一次異步調用都創建一個新線程的話肯定是
一個資源的浪費。在.Net 中,為我們提供了線程池,線程池在我們每次進行異步調用的時候都
會給調用方分配一個線程,然后控制權會在很短的時間內返回到客戶端。
總的來說BeginInvoke()方法發起一個異步調用,EndInvoke()管理方法的完成,包括獲取輸
出參數和返回值。要進行異步調用的說明還需要理解很多其他的概念,包括委托,事件,多線程
等,這些概念我就不一一道來了。
下面是整個說明過程中用到的一個類(摘抄而來):
public class CalCulator
{
public int Add(int argument1, int argument2)
{
return argument1 + argument2;
}
public int Subtract(int argument1, int argument2)
{
return argument1 - argument2;
}
}
一個委托: public delegate int BinaryOperation(int argument1, int argument2);
二:使用beginInvoke()和EndInvoke() 方法
異步委托提供以異步方式調用同步方法的能力。當同步調用一個委托時,“Invoke”方法直接
對當前線程調用目標方法。如果編譯器支持異步委托,則它將生成“Invoke”方法以及
“BeginInvoke”和“EndInvoke”方法。如果調用“BeginInvoke”方法,則公共語言運行

(CLR)
將對請求進行排隊并立即返回到調用方。將對來自線程池的線程調用該目標方法。提交請求的原
始線程自由地繼續與目標方法并行執行,該目標方法是對線程池線程運行的。如果在對
“BeginInvoke”方法的調用中指定了回調方法,則當目標方法返回時將調用該回調方法。在
回調方法中,“EndInvoke”方法獲取返回值和所有輸入/輸出參數。如果在調用“BeginInvoke”
時未指定任何回調方法,則可以從調用“BeginInvoke”的線程中調用“EndInvoke”。
對于上面的文字意思用代碼表示如下:
1.編譯器生成的BinaryOperaion 定義:
public sealed class BinaryOperaion : MulticastDelgate
{
public BinaryOperaion(object target, int methodPtr)
{
...
}
public virtual int Invoke(int argument1, int argument2)
{ ...}
public virtual IAsyncResult BeginInvoke(int argument1, int
argument2, AsyncCallback callback, object asyncState)
{
...
}
public virtual int EndInvoke(IAsyncResult result)
{ ...}
}
2.同步調用:
public delegate int BinaryOperation(int argument1, int argument2);
CalCulator calculator = new CalCulator();
BinaryOperaion oppDel = calculator.Add;
oppDel(2,3);
3 .異步調用:
首先說明下BeginInvoke()方法返回的IAsyncResult 接口對象,在.Net 中
IAsyncResult 的定義如下:
// 摘要:
// 表示異步操作的狀態。
[ComVisible(true)]
public interface IAsyncResult
{
// 摘要:
// 獲取用戶定義的對象,它限定或包含關于異步操作的信息。
//
// 返回結果:
// 用戶定義的對象,它限定或包含關于異步操作的信息。
object AsyncState { get; }
//
// 摘要:
// 獲取用于等待異步操作完成的 System.Threading.WaitHandle。
//
// 返回結果:
// 用于等待異步操作完成的 System.Threading.WaitHandle。
WaitHandle AsyncWaitHandle { get; }
//
// 摘要:
// 獲取異步操作是否同步完成的指示。
//
// 返回結果:
// 如果異步操作同步完成,則為 true;否則為 false。
bool CompletedSynchronously { get; }
//
// 摘要:
// 獲取異步操作是否已完成的指示。
//
// 返回結果:
// 如果操作完成則為 true,否則為 false。
bool IsCompleted { get; }
}
}
從它的定義我們可以看出很多東西, 如獲取異步操作是否同步完成的指示,我們可以把
IAsyncResult 對象傳遞給EndInvoke()
方法,來標識你希望檢索的那個特定的異步方法;如下所示:
CalCulator calculator = new CalCulator();
BinaryOperaion oppDel = calculator.Add;
IAsyncResult asynResult = oppDel.BeginInvoke(2, 3, null, null);
int iResult=oppDel.EndInvoke(asynResult);
System.Diagnostics.Debug.Assert(iResult==5);
雖然例子很簡單,但是要運用到實際的程序編寫中的話,還是要多下點苦功夫的,在上面有幾個
關鍵點,其中使用EndInvoke()方法我們可以獲取所有輸出參數和方法的返回值,它堵塞了調
用者一直到它等待的對象返回。這里有幾個需要注意的地方:
首先:EndInvoke()方法在每次調用異步操作時只能執行一次;
其次:當使用委托進行異步調用時,委托內部列表之允許一個目標方法;
最后,在將IAsynResult 對象傳遞給EndInvoke()方法時,委托和IAsynResult 對象必須
要匹配。
三:輪詢或等待完成
有的時候客戶端只是想知道某個操作是否完成了,或者想等待一段時間然后再做一些有限的處
理,然后再繼續等待或處理。這個時候我們可以使用IAsynResult 的AsyncWaitHandle 屬性
進行堵塞,一直到方法完成
,方式如下:asyncResult.AsyncWaitHandle.WaitOne();當然也可以指定超時
;其實在有的時候我們有很多方法需要在異步環境下進行的時候我們可以這樣來等待多個方法的
完成,我們可以獲取到一個WaitHandle 數組,然后調用它的WaitAll()方法等待多個異步方
法的完成,這個在管理多個異步方法尤其有優勢。
總結:
由于篇幅有限,還有些沒講的怎么詳細,希望在下篇能分開慢慢道來,在進行異步的過程中,其
實對于怎樣完成回調方法也是很有用的,我們可以在回調方法中進行我們需要的一些處理,如發
出通知給客戶端(這個過程應該是事件驅動的)。
自己其實也只是在一個自動更新模塊中用到了一些,理解肯定還不深,還是那句話,多多指教。
posted on 2007-09-15 12:58 寒蟬 閱讀(339) 評論(0) 編輯 收藏 所屬分類: C#讀書
筆記
Copyright ?2007 寒蟬 Powered by: 博客園 模板提供:滬江博客
C#的多線程能力
樓主celineshi()2006-11-08 14:40:47 在 專題開發/技術/項目 / 英特爾多核計算技術
提問
線程是允許進行并行計算的一個抽象概念:在另一個線程完成計算任務的同時,一個線程可
以對圖像進行更新,二個線程可以同時處理同一個進程發出的二個網絡請求。我們在這篇文
章中將重點討論Java 和C#在線程方面的不同之處,并將一些Java 中線程的常用模式轉換
為C#。
從概念上講,線程提供了一種在一個軟件中并行執行代碼的方式━━每個線程都“同時”
在一個共享的內存空間中執行指令,(當然是在一個處理器上,這是通過處于運行狀態的線
程的交替執行完成的。),因此,每個線程都可以訪問一個程序內的數據結構。由于這種原
因,多線程編程的難度就可想而知了,因為一個程序內有許多不同的線程需要安全地共享數
據。
線程的創建和運行
Java 在java.lang.Thread 和java.lang.Runnable 類中提供了大部分的線程功能。創
建一個線程非常簡單,就是擴展Thread 類,并調用start()。通過創建一個執行Runnable()
的類,并將該類作為參數傳遞給Thread(),也可以定義一個線程。仔細地閱讀下面這個簡
單的Java 程序,其中有2 個線程同時在從1 數到5,并將結果打印出來。
public class ThreadingExample
extends Object {
public static void main( String args[] ) {
Thread[] threads = new Thread[2];
for( int count=1;count<=threads.length;count ) {
threads[count] = new Thread( new Runnable() {
public void run() {
count();
}
} );
threads[count].start();
}
}
public static void count() {
for( int count=1;count<=5;count )
System.out.print( count " " );
}
}
我們可以使用System.Threading.Thread 和System.Threading.ThreadStart 二個類將
上述的Java 程序轉換為C#語言:
using System.Threading;
public class ThreadingExample : Object {
public static void Main() {
Thread[] threads = new Thread[2];
for( int count=1;count<=threads.Length;count ) {
threads[count] = new Thread( new ThreadStart( Count ) );
threads[count].Start();
}
}
public static void Count() {
for( int count=1;count<=5;count )
Console.Write( count " " );
}
}
這個例子中有一些小技巧。Java 允許擴展java.lang.Thread 類和執行
java.lang.Runnable 接口,C#則沒有為我們提供這些便利。一個C#中的Thread 對象是不可
知的,必須通過ThreadStart 進行創建,這意味著不能使用內部的類模式,而必須創建一個
對象,而且必須傳遞給線程一個對象的方法供線程執行用。
線程的使用
Java 中存在許多編程人員希望能夠對線程使用的標準操作:例如,測試線程是否存在、
加入一個線程直到它死亡、殺死一個線程等。
表1:線程管理的函數
Java 中java.lang.Thread 中的方法和C#中System.Threading.Thread 對象的對
比。
setDaemon( boolean on) 方法
IsBackground 設置屬性值
使一個存在的進程成為一個新線程(如果剩下的所有進程都成了新線程,程序將停止運
行)。
isDaemon()方法
IsBackground 獲取屬性
如果該線程是一個后臺線程,則返回真值。
isAlive() 方法
IsAlive 獲取屬性
如果該線程處于活動狀態,則返回真值。
interrupt() 方法
Interrupt() 方法
盡管在Java 中這一方法可以用來設置線程的中斷狀態,而且可以用來檢查線程是否被
中斷。在C#中沒有相應的方法,對一個沒有處于阻塞狀態的線程執行Interrupt 方法將使
下一次阻塞調用自動失效。
isInterrupted() 方法
n/a
如果該線程處于阻塞狀態,則返回真值。
sleep( long millis )和sleep( long millis, int nanos )
Sleep( int millisecondTimeout ) and Sleep( System.TimeSpan )
方法
使正在執行的線程暫停一段給定的時間,或直到它被中斷。這一方法將在Java 中將產
生一個java.lang.InterruptedException 狀態,在C#中將產生
System.Threading. ThreadInterruptedException 狀態。
join()、join( long millis )和
join( long millis, int nanos ) 方法
Join()、Join( int millisecondTimeout )和
Join( System.TimeSpan ) 方法 與Java 中僅依靠超時設定不同的是,在C#語言
中則依據線程停止運行是由于線程死亡(返回真)或是超時(返回假)而返回一個布爾型變
量。
suspend() 方法
Suspend() 方法
二者的功能相同。這一方法容易引起死循環,如果一個占有系統關健資源的線程被掛起
來,則在這一線程恢復運行之前,其他的線程不能訪問該資源。
resume() 方法
Resume() 方法
恢復一個被掛起的線程。
stop() 方法
Abort() 方法
參見下面的“線程停止”部分。
(特別說明,在上面的表中,每個小節的第一行是java 中的方法,第二行是C#中的方
法,第三行是有關的注釋,由于在文本文件中不能組織表格,請編輯多費點心組織表格,原
文中有表格的格式。)
線程的中止
由于能夠在沒有任何征兆的情況下使運行的程序進入一種混亂的狀態,Java 中的
Thread.stop 受到了普遍的反對。根據所調用的stop()方法,一個未經檢查的
java.lang.ThreadDeath 錯誤將會破壞正在運行著的程序的棧,隨著它的不斷運行,能夠解
除任何被鎖定的對象。由于這些鎖被不分青紅皂白地被打開,由它們所保護的數據就非常可
能陷入混亂狀態中。
根據當前的Java 文檔,推薦的中止一個線程的方法是讓運行的線程檢查一個由其他的
線程能夠改變的變量,該變量代表一個“死亡時間”條件。下面的程序就演示了這種方法。
// 條件變量
private boolean timeToDie = false;
// 在每次迭代中對條件變量進行檢查。
class StoppableRunnable
extends Runnable {
public void run() {
while( !timeToDie ) {
// 進行相應的操作
}
}
}
上述的討論對C#中的Abort 方法也適合。根據調用的Abort 方法,令人捉摸不定的
System.Threading.ThreadAbortException 可能會破壞線程的棧,它可能釋放線程保持的一
些變量,使處于保護狀態中的數據結構出現不可預測的錯誤。我建議使用與上面所示的相似
的方法來通知一個應該死亡的線程。
線程的同步
從概念上來看,線程非常易于理解,實際上,由于他們可能交互地對同一數據結構進行
操作,因此它們成為了令編程人員頭疼的一種東西。以本文開始的ThreadingExample 為例,
當它運行時,會在控制臺上輸出多種不同的結果。從 1 2 3 4 5 1 2 3 4 5
到 1 1 2 2 3 3 4 4 5 5 或 1 2 1 2 3 3 4 5 4 5 在內
的各種情況都是可能出現的,輸出結果可能與操作系統的線程調度方式之間的差別有關。有
時,需要確保只有一個線程能夠訪問一個給定的數據結構,以保證數據結構的穩定,這也是
我們需要線程同步機制的原因所在。
為了保證數據結構的穩定,我們必須通過使用“鎖”來調整二個線程的操作順序。二種
語言都通過對引用的對象申請一個“鎖”,一旦一段程序獲得該“鎖”的控制權后,就可以
保證只有它獲得了這個“鎖”,能夠對該對象進行操作。同樣,利用這種鎖,一個線程可以
一直處于等待狀態,直到有能夠喚醒它信號通過變量傳來為止。
表2:線程同步
需要對線程進行同步時需要掌握的關健字
synchronized
lock
C#中的lock 命令實際上是為使用System.Threading.Monitor 類中的Enter 和Exit 方法的
語法上的準備
Object.wait()
Monitor.Wait( object obj )
C#中沒有等待對象的方法,如果要等待一個信號,則需要使用System.Threading.Monitor
類,這二個方法都需要在同步的程序段內執行。
Object.notify()
Monitor.Pulse( object obj )
參見上面的Monitor.Wait 的注釋。
Object.notify()
Monitor.PulseAll( object obj )
參見上面的Monitor.Wait 的注釋。
(特別說明,在上面的表中,每個小節的第一行是java 中的方法,第二行是C#中的方
法,第三行是有關的注釋,由于在文本文件中不能組織表格,請編輯多費點心組織表格,原
文中有表格的格式。)
我們可以對上面的例子進行一些適當的修改,通過首先添加一個進行同步的變量,然后
對count()方法進行如下的修改,使變量在“鎖”中被執行加1 操作。
public static Object synchronizeVariable = "locking variable";
public static void count() {
synchronized( synchronizeVariable ) {
for( int count=1;count<=5;count ) {
System.out.print( count " " );
synchronizeVariable.notifyAll();
if( count < 5 )
try {
synchronizeVariable.wait();
} catch( InterruptedException error ) {
}
}
}
}
作了上述的改變后, 每次只有一個線程( 因為一次只能有一個線程獲得
synchronizeVariable)能夠執行for loop 循環輸出數字1;然后,它會喚醒所有等待
synchronizeVariable 的線程(盡管現在還沒有線程處于等待狀態。),并試圖獲得被鎖著的變
量,然后等待再次獲得鎖變量;下一個線程就可以開始執行for loop 循環輸出數字1,調
用notifyAll()喚醒前面的線程,并使它開始試圖獲得synchronizeVariable 變量,使自己處于
等待狀態,釋放synchronizeVariable,允許前面的線程獲得它。這個循環將一直進行下去,
直到它們都輸出完從1 到5 的數字。
通過一些簡單的語法變化可以將上述的修改在C#中實現:
public static Object synchronizeVariable = "locking variable";
public static void count() {
lock( synchronizeVariable ) {
for( int count=1;count<=5;count ) {
System.out.print( count " " );
Monitor.PulseAll( synchronizeVariable );
if( count < 5 )
Monitor.Wait( synchronizeVariable );
}
}
}
C#中特有的線程功能
象我們一直對C#所抱的期望那樣,C#中確實有一些Java 不支持的方法、類和函數,對
于鐵桿的Java 線程編程人員而言,這可是一件好事,因為他們可以用C#編寫代碼,然后在
Java 代碼中引用。
Enter/TryEnter/Exit
要在Java 中獲得某一變量的鎖,必須在代碼的首尾二端加上synchronized 關健字,指明
需要獲得鎖的對象。一旦線程開始執行synchronized 塊中的代碼,它就獲得了對這一對象的
鎖的控制權。同樣,一旦線程已經離開了synchronized 塊,它也將釋放這一對象的鎖。我們
已經知道,C#也有一個相似的被稱作lock 的關健字。除了lock 這個關健字外,C#還提供了
內置的獲得和釋放鎖的方法: Monitor.Enter( object obj )
和 Monitor.Exit( object obj ),通過使用這些方法,編程人員可以獲得與使用lock 相
同的作用,但提供了更精確的控制方法。例如,可以在一個方法中鎖定幾個變量,而不同時
或在代碼中的不同部分釋放它們。
對一個需要進行同步的對象執行System.Threading.Monitor.Enter 操作將使線程獲得該對
象的鎖,或者在由其他線程控制著該對象的鎖時進行阻塞。通過執行Monitor.Exit 方法就可
以釋放鎖, 如果線程已經不控制著該對象的鎖了, 這一方法將會產生一個
System.Threading.SynchronizationLockException 異常信號。
C#中的Monitor 類不但包括Enter 方法,還包括TryEnter 方法,如果執行該方法,就會
或者獲得一個鎖,或者返回一個表明它不能獲得鎖的返回值。
原子操作
System.Threading.Interlocked 類提供了程序對由幾個線程共享的變量進行同步訪問的能
力,C#把一些操作抽象為“原子”操作或“不可分割”的操作。為了說明這一問題是如何
解決的,我們來看一下下面的Java 代碼:
public static int x = 1;
public static void increment() {
x = x 1;
}
如果有二個不同的線程同時調用increment(),x 最后的值可能是2 或3,發生這種情況
的原因可能是二個進程無序地訪問x 變量,在沒有將x 置初值時對它執行加1 操作;在任一
線程有機會對x 執行加1 操作之前,二個線程都可能將x 讀作1,并將它設置為新的值。
在Java 和C#中,我們都可以實現對x 變量的同步訪問,所有進程都可以按各自的方式
運行。但通過使用Interlocked 類,C#提供了一個對這一問題更徹底的解決方案。Interlocked
類有一些方法, 例如Increment( ref int location ) 、
Decrement( ref int location ),這二個方法都取得整數型參數,對該整數執行加或減
1 操作,并返回新的值,所有這些操作都以“不可分割的”方式進行,這樣就無需單獨創建
一個可以進行同步操作的對象,如下例所示:
public static Object locker = ...
public static int x = 1;
public static void increment() {
synchronized( locker ) {
x = x 1;
}
}
C#中的Interlocked 類可以用下面的代碼完成相同的操作:
public static int x = 1;
public static void Increment() {
Interlocked.Increment( ref x );
}
Interlocked 中還包括一個名字為Exchange 的方法,可以“不可分割”地將一個變量的值
設置為另一個變量的值。
線程池
如果許多利用了線程的應用軟件都創建線程,這些線程將會因等待某些條件(鍵盤或新
的I/O 輸入等)而在等待狀態中浪費大部分的時間,C#提供的System.Threading.ThreadPool
對象可以解決這一問題。使用ThreadPool 和事件驅動的編程機制,程序可以注冊一個
System.Threading.WaitHandle 對象(WaitHandle 是C#編程中等待和通知機制的對象模型。)
和System.Threading.WaitOrTimerCallback 對象,所有的線程無需自己等待WaitHandle 的釋
放,ThreadPool 將監控所有向它注冊的WaitHandle,然后在WaitHandle 被釋放后調用相應
WaitOrTimerCallback 對象的方法。
結束語
在本篇文章中我們簡單地討論了C#提供的用于線程和并行操作的機制,其中的大部分
與Java 相似━━C#提供了可以運行提供的方法的Thread 對象,同時提供了對代碼訪問進行
同步的方法。與在其他方面一樣,C#在線程方面也提供了一些Java 不支持的語法(在一定
程度上,揭示了同步操作的一些底層的內容。),Java 編程人員可能會發現這一部分非常有
用。
56HC#異步編程
同步方法和異步方法的區別
同步方法調用在程序繼續執行之前需要等待同步方法執行完畢返回結果
異步方法則在被調用之后立即返回以便程序在被調用方法完成其任務的同時執行其它操作
異步編程概覽
.NET Framework 允許您異步調用任何方法。定義與您需要調用的方法具有相同簽名的委托;
公共語言運行庫將自動為該委托定義具有適當簽名
的 BeginInvoke 和 EndInvoke 方法。
BeginInvoke 方法用于啟動異步調用。它與您需要異步執行的方法具有相同的參數,只不過
還有兩個額外的參數(將在稍后描述)。
BeginInvoke 立即返回,不等待異步調用完成。
BeginInvoke 返回 IasyncResult,可用于監視調用進度。
EndInvoke 方法用于檢索異步調用結果。調用 BeginInvoke 后可隨時調用 EndInvoke 方
法;如果異步調用未完成,EndInvoke 將一直阻塞到
異步調用完成。EndInvoke 的參數包括您需要異步執行的方法的 out 和 ref 參數(在
Visual Basic 中為 <Out> ByRef 和 ByRef)以及由
BeginInvoke 返回的 IAsyncResult。
四種使用 BeginInvoke 和 EndInvoke 進行異步調用的常用方法。調用了 BeginInvoke 后,
可以:
1.進行某些操作,然后調用 EndInvoke 一直阻塞到調用完成。
2.使用 IAsyncResult.AsyncWaitHandle 獲取 WaitHandle,使用它的 WaitOne 方法將執行
一直阻塞到發出 WaitHandle 信號,然后調用
EndInvoke。這里主要是主程序等待異步方法,等待異步方法的結果。
3.輪詢由 BeginInvoke 返回的 IAsyncResult,IAsyncResult.IsCompeted 確定異步調用何
時完成,然后調用 EndInvoke。此處理個人認為與
相同。
4.將用于回調方法的委托傳遞給 BeginInvoke。該方法在異步調用完成后在 ThreadPool 線
程上執行,它可以調用 EndInvoke。這是在強制裝
換回調函數里面IAsyncResult.AsyncState(BeginInvoke 方法的最后一個參數)成委托,然
后用委托執行EndInvoke。
警告 始終在異步調用完成后調用 EndInvoke。
以上有不理解的稍后可以再理解。
例子
1)先來個簡單的沒有回調函數的異步方法例子
請再運行程序的時候,仔細看注釋,對理解很有幫助。還有,若將注釋的中的兩
個方法都同步,你會發現異步運行的速度優越性。
using System;
namespace ConsoleApplication1
{
class Class1
{
//聲明委托
public delegate void AsyncEventHandler();
//異步方法
void Event1()
{
Console.WriteLine("Event1 Start");
System.Threading.Thread.Sleep(4000);
Console.WriteLine("Event1 End");
}
// 同步方法
void Event2()
{
Console.WriteLine("Event2 Start");
int i=1;
while(i<1000)
{
i=i+1;
Console.WriteLine("Event2 "+i.ToString());
}
Console.WriteLine("Event2 End");
}
[STAThread]
static void Main(string[] args)
{
long start=0;
long end=0;
Class1 c = new Class1();
Console.WriteLine("ready");
start=DateTime.Now.Ticks;
//實例委托
AsyncEventHandler asy = new AsyncEventHandler(c.Event1);
//異步調用開始,沒有回調函數和AsyncState,都為null
IAsyncResult ia = asy.BeginInvoke(null, null);
//同步開始,
c.Event2();
//異步結束,若沒有結束,一直阻塞到調用完成,在此返回該函數的return,若有返
回值。
asy.EndInvoke(ia);
//都同步的情況。
//c.Event1();
//c.Event2();
end =DateTime.Now.Ticks;
Console.WriteLine("時間刻度差="+ Convert.ToString(end-start) );
Console.ReadLine();
}
}
}
2)下面看有回調函數的WebRequest 和WebResponse 的異步操作。
using System;
using System.Net;
using System.Threading;
using System.Text;
using System.IO;
// RequestState 類用于通過
// 異步調用傳遞數據
public class RequestState
{
const int BUFFER_SIZE = 1024;
public StringBuilder RequestData;
public byte[] BufferRead;
public HttpWebRequest Request;
public Stream ResponseStream;
// 創建適當編碼類型的解碼器
public Decoder StreamDecode = Encoding.UTF8.GetDecoder();
public RequestState()
{
BufferRead = new byte[BUFFER_SIZE];
RequestData = new StringBuilder("");
Request = null;
ResponseStream = null;
}
}
// ClientGetAsync 發出異步請求
class ClientGetAsync
{
public static ManualResetEvent allDone = new ManualResetEvent(false);
const int BUFFER_SIZE = 1024;
public static void Main(string[] args)
{
if (args.Length < 1)
{
showusage();
return;
}
// 從命令行獲取 URI
Uri HttpSite = new Uri(args[0]);
// 創建請求對象
HttpWebRequest wreq = (HttpWebRequest)WebRequest.Create(HttpSite);
// 創建狀態對象
RequestState rs = new RequestState();
// 將請求添加到狀態,以便它可以被來回傳遞
rs.Request = wreq;
// 發出異步請求
IAsyncResult r = (IAsyncResult)wreq.BeginGetResponse(new AsyncCallback(RespCal
lback), rs);
// 將 ManualResetEvent 設置為 Wait,
// 以便在調用回調前,應用程序不退出
allDone.WaitOne();
}
public static void showusage()
{
Console.WriteLine("嘗試獲取 (GET) 一個 URL");
Console.WriteLine("/r/n 用法::");
Console.WriteLine("ClientGetAsync URL");
Console.WriteLine("示例::");
Console.WriteLine("ClientGetAsync http://www.microsoft.com/net/");
}
private static void RespCallback(IAsyncResult ar)
{
// 從異步結果獲取 RequestState 對象
RequestState rs = (RequestState)ar.AsyncState;
// 從 RequestState 獲取 HttpWebRequest
HttpWebRequest req = rs.Request;
// 調用 EndGetResponse 生成 HttpWebResponse 對象
// 該對象來自上面發出的請求
HttpWebResponse resp = (HttpWebResponse)req.EndGetResponse(ar);
// 既然我們擁有了響應,就該從
// 響應流開始讀取數據了
Stream ResponseStream = resp.GetResponseStream();
// 該讀取操作也使用異步完成,所以我們
// 將要以 RequestState 存儲流
rs.ResponseStream = ResponseStream;
// 請注意,rs.BufferRead 被傳入到 BeginRead。
// 這是數據將被讀入的位置。
IAsyncResult iarRead = ResponseStream.BeginRead(rs.BufferRead, 0, BUFFER_SIZ
E, new AsyncCallback(ReadCallBack), rs);
}
private static void ReadCallBack(IAsyncResult asyncResult)
{
// 從 asyncresult 獲取 RequestState 對象
RequestState rs = (RequestState)asyncResult.AsyncState;
// 取出在 RespCallback 中設置的 ResponseStream
Stream responseStream = rs.ResponseStream;
// 此時 rs.BufferRead 中應該有一些數據。
// 讀取操作將告訴我們那里是否有數據
int read = responseStream.EndRead(asyncResult);
if (read > 0)
{
// 準備 Char 數組緩沖區,用于向 Unicode 轉換
Char[] charBuffer = new Char[BUFFER_SIZE];
// 將字節流轉換為 Char 數組,然后轉換為字符串
// len 顯示多少字符被轉換為 Unicode
int len = rs.StreamDecode.GetChars(rs.BufferRead, 0, read, charBuffer, 0);
String str = new String(charBuffer, 0, len);
// 將最近讀取的數據追加到 RequestData stringbuilder 對象中,
// 該對象包含在 RequestState 中
rs.RequestData.Append(str);
// 現在發出另一個異步調用,讀取更多的數據
// 請注意,將不斷調用此過程,直到
// responseStream.EndRead 返回 -1
IAsyncResult ar = responseStream.BeginRead(rs.BufferRead, 0, BUFFER_SIZE, ne
w AsyncCallback(ReadCallBack), rs);
}
else
{
if (rs.RequestData.Length > 1)
{
// 所有數據都已被讀取,因此將其顯示到控制臺
string strContent;
strContent = rs.RequestData.ToString();
Console.WriteLine(strContent);
}
// 關閉響應流
responseStream.Close();
// 設置 ManualResetEvent,以便主線程可以退出
allDone.Set();
}
return;
}
}
在這里有回調函數,且異步回調中又有異步操作。
首先是異步獲得ResponseStream,然后異步讀取數據。
這個程序非常經典。從中可以學到很多東西的。我們來共同探討。
總結
上面說過,.net framework 可以異步調用任何方法。所以異步用處廣泛。
在.net framework 類庫中也有很多異步調用的方法。一般都是已Begin 開頭End 結尾構成
一對,異步委托方法,外加兩個回調函數和AsyncState 參數,組成異步操作的宏觀體現。
所以要做異步編程,不要忘了委托delegate、Begin,End,AsyncCallBack 委托,AsyncState
實例(在回調函數中通過IAsyncResult.AsyncState 來強制轉換),IAsycResult(監控異步),
就足以理解異步真諦了。
57H C#異步數據處理及進度顯示
對于C#的事件,指代(Delegate)總是感覺理解不太深刻。這幾天正好有機會學習
了一下。從一個程序中改了一部分代碼,實現了一個異步數據處理基本構架。
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 testManager
{
//progressbar 窗體
//有一個cancel 按鈕,和一個進度條
public class ProgressForm : Form
{
private System.ComponentModel.IContainer components = nul
l;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
Windows InitializeComponent
private System.Windows.Forms.ProgressBar poProgressBar;
private System.Windows.Forms.Button btnCancel;
//定義進度狀態的事件。
public delegate void StatusHandler();
public event StatusHandler StatusChanged;
private int iStatus = 1;//1:normal 0:cancel -1:exception
//返回進度狀態
public int GetStatus
{
get { return iStatus; }
}
public ProgressForm()
{
InitializeComponent();
this.poProgressBar.Value = 0;
}
//進度條加1
public void PerformStep()
{
try
{
this.poProgressBar.PerformStep();
this.poProgressBar.Refresh();
if (this.poProgressBar.Value == 100)
{
iStatus = 1;
this.StatusChanged();
}
}
catch
{
iStatus = -1;
StatusChanged();
}
}
private void btnCancel_Click(object sender, EventArgs e)
{
this.iStatus = 0;
StatusChanged();
}
}
//邏輯控制類,相當于主控部分
public class ProgressManager
{
//實際進行運算處理的類實例
private IRunAble poRun;
//進度條類實例
private ProgressForm poProgress;
public delegate void CompletedHandler();
public event CompletedHandler ProgressCompleted;
public int GetStatus
{
get { return poProgress.GetStatus; }
}
public ProgressManager(IRunAble RunAbleInstance)
{
this.poRun = RunAbleInstance;
poProgress = new ProgressForm();
this.poProgress.StartPosition = FormStartPosition.Cen
terScreen;
this.poProgress.Show();
poProgress.StatusChanged += new ProgressForm.StatusHa
ndler(poProgress_StatusChanged);
}
public void Start()
{
//異步調用,進行運算。
AsyncDelegate oDelegate = new AsyncDelegate(DoFunctio
n);
oDelegate.BeginInvoke(myCallBack, new object());
}
private delegate void AsyncDelegate();
private void DoFunction()
{
this.poRun.Run(this);
}
//回調函數
private void myCallBack(IAsyncResult iResult)
{
this.poProgress.Close();
}
public void PerformStep()
{
this.poProgress.PerformStep();
}
void poProgress_StatusChanged()
{
if (poProgress.GetStatus == 0 || poProgress.GetStatus
== 1)
{
poProgress.Close();
}
ProgressCompleted();
}
}
//實際運算類接口
public interface IRunAble
{
void Run(ProgressManager aoManager);
}
//實際運算外理類
public class CalRun : IRunAble
{
public void Run(ProgressManager aoManager)
{
double p = 3.1415926;
for (int i = 1; i <= 100000; i++)
{
if (aoManager.GetStatus == 0)
{
break;
}
System.Windows.Forms.Application.DoEvents();
double dd = i * i * p;
double dd2 = Math.Abs(dd * 456) * Math.Abs(dd * 4
56);
double dd3 = Math.Pow(dd, dd2) * Math.Pow(dd, dd2
);
if (i % 1000 == 0)
{
aoManager.PerformStep();
}
}
}
}
}
調用方式如下:
private CalRun cr;
private ProgressManager pm;
private void button1_Click(object sender, EventArgs e)
{
cr = new CalRun();
pm = new ProgressManager(cr);
pm.ProgressCompleted += new ProgressManager.Completed
Handler(pm_ProgressCompleted);
pm.Start();
}
void pm_ProgressCompleted()
{
switch (pm.GetStatus)
{
case 1:
MessageBox.Show("OK");
break;
case 0:
MessageBox.Show("Cancel");
break;
case -1:
MessageBox.Show("Error");
break;
}
}
英文原版地址:58Hhttp://www.sellsbrothers.com/writing/default.aspx?content=delegates.htm
.NET 委托:一個C#睡前故事
英文版原作者:Chris Sells(59Hhttp://www.sellsbrothers.com/)
翻譯:袁曉輝(60Hhttp://www.farproc.com/ 61Hhttp://dev.csdn.net/uoyevoli)
緊耦合
從前,在南方一塊奇異的土地上,有個工人名叫彼得,他非常勤奮,對他的
老板總是百依百順。但是他的老板是個吝嗇的人,從不信任別人,堅決要求隨時
知道彼得的工作進度,以防止他偷懶。但是彼得又不想讓老板呆在他的辦公室里
站在背后盯著他,于是就對老板做出承諾:無論何時,只要我的工作取得了一點
進展我都會及時讓你知道。彼得通過周期性地使用“帶類型的引用”(原文為:
“typed reference” 也就是delegate??)“回調”他的老板來實現他的承諾,
如下:
class Worker {
public void Advise(Boss boss) { _boss = boss; }
public void DoWork() {
Console.WriteLine(“工作: 工作開始”);
if( _boss != null ) _boss.WorkStarted();
Console.WriteLine(“工作: 工作進行中”);
if( _boss != null ) _boss.WorkProgressing();
Console.WriteLine("“工作: 工作完成”");
if( _boss != null ) {
int grade = _boss.WorkCompleted();
Console.WriteLine(“工人的工作得分=” + grade);
}
}
private Boss _boss;
}
class Boss {
public void WorkStarted() { /* 老板不關心。 */ }
public void WorkProgressing() { /*老板不關心。 */ }
public int WorkCompleted() {
Console.WriteLine(“時間差不多!”);
return 2; /* 總分為10 */
}
}
class Universe {
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.Advise(boss);
peter.DoWork();
Console.WriteLine(“Main: 工人工作完成”);
Console.ReadLine();
}
}
接口
現在,彼得成了一個特殊的人,他不但能容忍吝嗇的老板,而且和他周圍的
宇宙也有了密切的聯系,以至于他認為宇宙對他的工作進度也感興趣。不幸的是,
他必須也給宇宙添加一個特殊的回調函數Advise 來實現同時向他老板和宇宙報
告工作進度。彼得想要把潛在的通知的列表和這些通知的實現方法分離開來,于
是他決定把方法分離為一個接口:
interface IWorkerEvents {
void WorkStarted();
void WorkProgressing();
int WorkCompleted();
}
class Worker {
public void Advise(IWorkerEvents events) { _events = events; }
public void DoWork() {
Console.WriteLine(“工作: 工作開始”);
if( _events != null ) _events.WorkStarted();
Console.WriteLine(“工作: 工作進行中”);
if(_events != null ) _events.WorkProgressing();
Console.WriteLine("“工作: 工作完成”");
if(_events != null ) {
int grade = _events.WorkCompleted();
Console.WriteLine(“工人的工作得分=” + grade);
}
}
private IWorkerEvents _events;
}
class Boss : IWorkerEvents {
public void WorkStarted() { /* 老板不關心。 */ }
public void WorkProgressing() { /* 老板不關心。 */ }
public int WorkCompleted() {
Console.WriteLine(“時間差不多!”);
return 3; /* 總分為10 */
}
}
委托
不幸的是,每當彼得忙于通過接口的實現和老板交流時,就沒有機會及時通
知宇宙了。至少他應該忽略身在遠方的老板的引用,好讓其他實現了
IWorkerEvents 的對象得到他的工作報告。(”At least he'd abstracted the
reference of his boss far away from him so that others who implemented
the IWorkerEvents interface could be notified of his work progress” 原
話如此,不理解到底是什么意思:))
他的老板還是抱怨得很厲害。“彼得!”他老板吼道,“你為什么在工作一
開始和工作進行中都來煩我?!我不關心這些事件。你不但強迫我實現了這些方
法,而且還在浪費我寶貴的工作時間來處理你的事件,特別是當我外出的時候更
是如此!你能不能不再來煩我?”
于是,彼得意識到接口雖然在很多情況都很有用,但是當用作事件時,“粒
度”不夠好。他希望能夠僅在別人想要時才通知他們,于是他決定把接口的方法
分離為單獨的委托,每個委托都像一個小的接口方法:
delegate void WorkStarted();
delegate void WorkProgressing();
delegate int WorkCompleted();
class Worker {
public void DoWork() {
Console.WriteLine(“工作: 工作開始”);
if( started != null ) started();
Console.WriteLine(“工作: 工作進行中”);
if( progressing != null ) progressing();
Console.WriteLine("“工作: 工作完成”");
if( completed != null ) {
int grade = completed();
Console.WriteLine(“工人的工作得分=” + grade);
}
}
public WorkStarted started;
public WorkProgressing progressing;
public WorkCompleted completed;
}
class Boss {
public int WorkCompleted() {
Console.WriteLine("Better...");
return 4; /* 總分為10 */
}
}
class Universe {
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.completed = new WorkCompleted(boss.WorkCompleted);
peter.DoWork();
Console.WriteLine(“Main: 工人工作完成”);
Console.ReadLine();
}
}
靜態監聽者
這樣,彼得不會再拿他老板不想要的事件來煩他老板了,但是他還沒有把宇
宙放到他的監聽者列表中。因為宇宙是個包涵一切的實體,看來不適合使用實例
方法的委托(想像一下,實例化一個“宇宙”要花費多少資源…..),于是彼得
就需要能夠對靜態委托進行掛鉤,委托對這一點支持得很好:
class Universe {
static void WorkerStartedWork() {
Console.WriteLine("Universe notices worker starting work");
}
static int WorkerCompletedWork() {
Console.WriteLine("Universe pleased with worker's work");
return 7;
}
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.completed = new WorkCompleted(boss.WorkCompleted);
peter.started = new WorkStarted(Universe.WorkerStartedWork);
peter.completed = new WorkCompleted(Universe.WorkerCompletedWork);
peter.DoWork();
Console.WriteLine(“Main: 工人工作完成”);
Console.ReadLine();
}
}
事件
不幸的是,宇宙太忙了,也不習慣時刻關注它里面的個體,它可以用自己的
委托替換了彼得老板的委托。這是把彼得的Worker 類的的委托字段做成public
的一個無意識的副作用。同樣,如果彼得的老板不耐煩了,也可以決定自己來激
發彼得的委托(真是一個粗魯的老板):
// Peter's boss taking matters into his own hands
if( peter.completed != null ) peter.completed();
彼得不想讓這些事發生,他意識到需要給每個委托提供“注冊”和“反注冊”
功能,這樣監聽者就可以自己添加和移除委托,但同時又不能清空整個列表也不
能隨意激發彼得的事件了。彼得并沒有來自己實現這些功能,相反,他使用了
event 關鍵字讓C#編譯器為他構建這些方法:
class Worker {
...
public event WorkStarted started;
public event WorkProgressing progressing;
public event WorkCompleted completed;
}
彼得知道event 關鍵字在委托的外邊包裝了一個property,僅讓C#客戶通
過+= 和 -=操作符來添加和移除,強迫他的老板和宇宙正確地使用事件。
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.completed += new WorkCompleted(boss.WorkCompleted);
peter.started += new WorkStarted(Universe.WorkerStartedWork);
peter.completed += new WorkCompleted(Universe.WorkerCompletedWork);
peter.DoWork();
Console.WriteLine(“Main: 工人工作完成”);
Console.ReadLine();
}
“收獲”所有結果
到這時,彼得終于可以送一口氣了,他成功地滿足了所有監聽者的需求,同
時避免了與特定實現的緊耦合。但是他注意到他的老板和宇宙都為它的工作打了
分,但是他僅僅接收了一個分數。面對多個監聽者,他想要“收獲”所有的結果,
于是他深入到代理里面,輪詢監聽者列表,手工一個個調用:
public void DoWork() {
...
Console.WriteLine("“工作: 工作完成”");
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() ) {
int grade = wc();
Console.WriteLine(“工人的工作得分=” + grade);
}
}
}
異步通知:激發 & 忘掉
同時,他的老板和宇宙還要忙于處理其他事情,也就是說他們給彼得打分所
花費的事件變得非常長:
class Boss {
public int WorkCompleted() {
System.Threading.Thread.Sleep(3000);
Console.WriteLine("Better..."); return 6; /* 總分為10 */
}
}
class Universe {
static int WorkerCompletedWork() {
System.Threading.Thread.Sleep(4000);
Console.WriteLine("Universe is pleased with worker's work");
return 7;
}
...
}
很不幸,彼得每次通知一個監聽者后必須等待它給自己打分,現在這些通知
花費了他太多的工作事件。于是他決定忘掉分數,僅僅異步激發事件:
public void DoWork() {
...
Console.WriteLine("“工作: 工作完成”");
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() )
{
wc.BeginInvoke(null, null);
}
}
}
異步通知:輪詢
這使得彼得可以通知他的監聽者,然后立即返回工作,讓進程的線程池來調
用這些代理。隨著時間的過去,彼得發現他丟失了他工作的反饋,他知道聽取別
人的贊揚和努力工作一樣重要,于是他異步激發事件,但是周期性地輪詢,取得
可用的分數。
public void DoWork() {
...
Console.WriteLine("“工作: 工作完成”");
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() ) {
IAsyncResult res = wc.BeginInvoke(null, null);
while( !res.IsCompleted ) System.Threading.Thread.Sleep(1);
int grade = wc.EndInvoke(res);
Console.WriteLine(“工人的工作得分=” + grade);
}
}
}
異步通知:委托
不幸地,彼得有回到了一開始就想避免的情況中來,比如,老板站在背后盯
著他工作。于是,他決定使用自己的委托作為他調用的異步委托完成的通知,讓
他自己立即回到工作,但是仍可以在別人給他的工作打分后得到通知:
public void DoWork() {
...
Console.WriteLine("“工作: 工作完成”");
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() ) {
wc.BeginInvoke(new AsyncCallback(WorkGraded), wc);
}
}
}
private void WorkGraded(IAsyncResult res) {
WorkCompleted wc = (WorkCompleted)res.AsyncState;
int grade = wc.EndInvoke(res);
Console.WriteLine(“工人的工作得分=” + grade);
}
宇宙中的幸福
彼得、他的老板和宇宙最終都滿足了。彼得的老板和宇宙可以收到他們感興
趣的事件通知,減少了實現的負擔和非必需的往返“差旅費”。彼得可以通知他
們,而不管他們要花多長時間來從目的方法中返回,同時又可以異步地得到他的
結果。彼得知道,這并不*十分*簡單,因為當他異步激發事件時,方法要在另外
一個線程中執行,彼得的目的方法完成的通知也是一樣的道理。但是,62H邁克和63H彼
得是好朋友,他很熟悉線程的事情,可以在這個領域提供指導。
他們永遠幸福地生活下去……<完>
2005 年9 月2 日
作者Blog:64Hhttp://blog.csdn.net/uoyevoli/
65H銜接UI線程和管理后臺工作線程的類(多線程、異步調用)
一、引言
在編寫Windows form時,如果直接在UI線程要運行一個費時方法的話(如從數據庫查詢大量數據
時),會引起程序“假死”,從而導致用戶不滿。這個時候就需要通過多線程技術來解決,提高界面交
互性能,方便用戶使用。
一般通過三種方式解決:
1.通過System.Threading.Thread類,創建新的線程,Thread.Start運行費時方法。
2.通過System.Threading.ThreadPool類,將費時任務提交到線程池中,等待運行。
以上兩種方法,基本思路是在UI界面中控制線程的啟動和中止,在線程中回調用UI界面方法,更新界
面。在線程中回調UI界面方法時,特別是涉及更新控件屬性時,如果不注意,存在很大的隱患。這兩
種辦法,編碼和控制結構較為復雜,需要啟動和管理額外的線程占用資源。
3.通過異步委托調用,將該方法排隊到系統線程池的線程中運行,而在費時方法中也通過
Control.BeginInvoke異步回調,達到"啟動后不管"的目的。
這種方法,編碼簡單,程序結構較為清晰,充分利用.NET框架的異步委托功能,但要對異步調用知識
較熟悉。
6H相關知識點參見
現利用.NET異步委托調用功能,編寫Task抽象類,以方便管理后臺工作線程,銜接后臺線程與UI
線程的聯系。該抽象類提供了調用和管理的框架,沒有方法的實現細節,通過繼承類、重寫方法,可
以實現想要的功能。主要功能如下:
1.利用異步委托調用,實際多線程,不需要單獨后臺線程。
2.通過委托、事件驅動,實際后臺與前臺UI線程的聯系,實現事件廣播。
3.支持正常取消后臺工作方法(費時方法)運行,也可以強制中止線程。
4.能夠捕獲取消、強制中止和方法出錯三種情況,并突發相關事件,以便進行釋放資源等操作。
5.通過異步調用,在工作方法中安全調用涉及UI控件的方法。
6.自行管理工作進程狀態,提供狀態變化事件。
7.只要工作方法調用簽名,符合定義的TaskDelegate委托接口,可通過StartTask(TaskDelegate
worker ,params object[] args )方便調用。在實際使用時,可在繼承類中定義多個相同調用接口的方
法,避免重復編碼,較為方便。
給大家作個參考,而大牛呢,多點指正。當是扔個磚頭,想砸塊玉吧。
二、代碼
1 using System;
2 using System.Windows.Forms;
3
4 namespace Net66.AsynchThread
5 {
6 /// <summary>
7 /// 任務工作狀態
8 /// </summary>
9 public enum TaskStatus
10
37
38 /// <summary>
39 /// 任務狀態消息
40 /// </summary>
41 public class TaskEventArgs : EventArgs
42
137
138 /// <summary>
139 /// 任務的工作方法(Work)的委托接口
140 /// 傳入值:對象數組(object[])
141 /// 返回值:對象(object)
142 /// </summary>
143 public delegate object TaskDelegate( params object[] args );
144
145 /// <summary>
146 /// 任務事件的委托接口
147 /// </summary>
148 public delegate void TaskEventHandler( object sender, TaskEventArgs e );
149
150 abstract public class Task
151 {
152 內部屬性
178
179 事件
201
202 屬性
267
268 觸發事件
358
359 工作進程管理
497
498 工作方法的基礎
520 }
521 }
522
523 使用Task 類/*
525
526 使用 Task 類
527
528 一.在UI 線程中創建Task 類
529
530 Task 類負責管理后臺線程。要使用 Task 類,必須做的事情就是創建一個 Task 對象,注冊它激
發的事件,并且實現這些事件的處理。因為事件是在 UI 線程上激發的,所以您根本不必擔心代碼中
的線程處理問題。
531
532 下面的示例展示了如何創建 Task 對象。現假設UI 有兩個按鈕,一個用于啟動運算,一個用于
停止運算,還有一個進度欄顯示當前的計算進度。
533
534 // 創建任務管理對象
535 _Task = new Task();
536 // 掛接任務管理對象工作狀態變化事件
537 _Task.TaskStatusChanged += new TaskEventHandler( OnTaskStatusChanged );
538 // 掛接任務管理對象工作進度變化事件
539 _Task.TaskProgressChanged += new TaskEventHandler( OnTaskProgressChanged );
540
541 (1)
542 用于計算狀態和計算進度事件的事件處理程序相應地更新 UI,例如通過更新狀態欄控件。
543
544 private void OnTaskProgressChanged( object sender,TaskEventArgs e )
545 {
546 _progressBar.Value = e.Progress;
547 }
548 (2)
549 下面的代碼展示的 TaskStatusChanged 事件處理程序更新進度欄的值以反映當前的計算進度。
假定進度欄的最小值和最大值已經初始化。
550
551 private void OnTaskStatusChanged( object sender, TaskEventArgs e )
552 {
553 switch ( e.Status )
554 {
555 case TaskStatus.Running:
556 button1.Enabled = false;
557 button2.Enabled = true;
558 break;
559 case TaskStatus.Stop:
560 button1.Enabled = true;
561 button2.Enabled = false;
562 break;
563 case TaskStatus.CancelPending:
564 button1.Enabled = false;
565 button2.Enabled = false;
566 break;
567 }
568 }
569
570 在這個示例中,TaskStatusChanged 事件處理程序根據計算狀態啟用和禁用啟動和停止按鈕。
這可以防止用戶嘗試啟動一個已經在進行的計算,并且向用戶提供有關計算狀態的反饋。
571
572 通過使用 Task 對象中的公共方法,UI 為每個按鈕單擊實現了窗體事件處理程序,以便啟動和
停止計算。例如,啟動按鈕事件處理程序調用 StartTask 方法,如下所示。
573
574 private void startButton_Click( object sender, System.EventArgs e )
575 {
576 _Task.StartTask( new object[] {} );
577 }
578
579 類似地,停止計算按鈕通過調用 StopTask 方法來停止計算,如下所示。
580
581 private void stopButton_Click( object sender, System.EventArgs e )
582 {
583 _Task.StopTask();
584 }
585
586 二.可能在非UI 線程中使用Task 類時
587 (1)和(2)應作如下改變
588
589 (1)
590 用于計算狀態和計算進度事件的事件處理程序相應地更新 UI,例如通過更新狀態欄控件。
591
592 private void OnTaskProgressChanged( object sender,TaskEventArgs e )
593 {
594 if (InvokeRequired ) //不在UI 線程上,異步調用
595 {
596 TaskEventHandler TPChanged = new TaskEventHandler( OnTaskProgressChanged );
597 this.BeginInvoke(TPChanged,new object[] {sender,e});
598 }
599 else //更新
600 {
601 _progressBar.Value = e.Progress;
602 }
603 }
604 (2)
605 下面的代碼展示的 TaskStatusChanged 事件處理程序更新進度欄的值以反映當前的計算進度。
假定進度欄的最小值和最大值已經初始化。
606
607 private void OnTaskStatusChanged( object sender, TaskEventArgs e )
608 {
609 if (InvokeRequired ) //不在UI 線程上,異步調用
610 {
611 TaskEventHandler TSChanged = new TaskEventHandler( OnTaskStatusChanged );
612 this.BeginInvoke(TSChanged,new object[] {sender,e});
613 }
614 else //更新
615 {
616 switch ( e.Status )
617 {
618 case TaskStatus.Running:
619 button1.Enabled = false;
620 button2.Enabled = true;
621 break;
622 case TaskStatus.Stop:
623 button1.Enabled = true;
624 button2.Enabled = false;
625 break;
626 case TaskStatus.CancelPending:
627 button1.Enabled = false;
628 button2.Enabled = false;
629 break;
630 }
631 }
632 }
633
634 */
636
三、示例
1.啟動時的UI界面
2.后臺工作方法(費用方法)運行后,任務狀態為Running
3.強制中止工作方法,運行任務狀態Aborted
4.工作方法突發錯誤時,任務狀態ThrowErrorStoped
5.工作方法正常結束或正常取消而結束時,任務狀態Stopped
67H示例代碼下載
posted on 2005-08-03 00:19 68HNet66 閱讀(4810) 69H評論(25) 70H編輯 71H收藏 所屬分類: 72HC#
評論
73H#4 樓 2005-08-03 08:46 [email protected] [未注冊用戶]
System.Threading.ThreadPool 還是不要用的好,這東西牽扯進程的異步調用,這東西微軟就該設置成internal
74H回復 75H引用 76H查看
7H#5 樓 2005-08-03 08:49 學習 [未注冊用戶]
78Hhttp://www.vckbase.com/document/viewdoc/?id=1126
79H回復 80H引用 81H查看
82H#6 樓 2005-08-03 08:54 83HJames
這些BeginXXXX 的方法都是使用了ThreadPool 的。
84H回復 85H引用 86H查看
87H#7 樓 2005-08-03 09:06 8H張老三 [未注冊用戶]
剛完成了一個圖庫管理系統, 使用的就是這種方式. 感覺還不錯.
89H回復 90H引用 91H查看
92H#8 樓 2005-08-03 09:08 win [未注冊用戶]
文章的意思應該是不用顯式使用ThreadPool,通過系統默認調用吧
93H回復 94H引用 95H查看
96H#10 樓 2005-08-03 09:39 97H妖居
異步委托也是剛剛用過,做一個分析目錄的工具的時候用的。
98H回復 9H引用 10H查看
101H#11 樓 2005-08-03 09:41 102HJames
我的意思是說,這些異步委托調用什么的,其內部都是使用了ThreadPool 的,這樣當然不用你自己去直接使用
ThreadPool 了。
103H回復 104H引用 105H查看
106H#12 樓 2005-08-03 18:48 107Hyinh
最近寫的項目中因為用得很多網絡交互,頻繁的異步調用,但對控制這些線程卻沒有一點主意,希望能從你的文
章中得到一些啟示。
108H回復 109H引用 10H查看
1H#14 樓 2005-09-14 16:10 12H雙魚座
看了下樓主的代碼,大致上不錯。不過在封裝性方面做得不夠...其實封裝性是需要早一些構思的,比功能的實現
還要再早一些。
先搞清楚你的Task 的派生類依賴什么,也就是說可供派生類調用的方法,例如Fire***方法;接下來搞清楚你的
Task 的派生類不依賴什么,例如必須在重寫方法中調用base.Work 這樣的問題;最后是參數的傳遞問題。由此我
想到了在Delphi3 中對Thread 的封裝,有一個同步執行方法的方法,在調用者不知道任何細節的情況下可以進
行安全的調用。當然,那個封裝代價有點大了。
我作了如下修改:
1.定義一個抽象方法:
protected abstract object Execute(params object [] args);
這個方法才是真正需要繼承的方法。
2.基類的Work 方法改成私有方法(Work 這個名字取得不是太好),代碼這樣寫:
System.Threading.Thread.CurrentThread.IsBackground = true;
_workThread = System.Threading.Thread.CurrentThread;
return Execute(args);
3.加一個線程方法:
void start()
{
StartTask(new TaskDelegate(Work), tempArgs);
}
4.定義一個私有字段用來向線程傳遞參數:
private object[] tempArgs;
5.最后加一個啟動方法:
public void Start(params object[] args)
{
tempArgs = args;
Thread thread = new Thread(new ThreadStart(start));
thread.Start();
}
改造完成了。客戶端代碼由此變得非常簡潔:
1.當按下“開始執行”時的代碼:
_Task.Start(new object[] {});
2.Concrete 類型newasynchui 的代碼也簡單了,只需要重寫一個Execute 方法而不是重載一個Work 和另外寫一
個Work2,在方法中也不必調用base.Work,你想調用也調用不了,因為是那是私有方法。
當然還有其它一些更好的建議,例如不必定義那么多的事件,其實一兩個就足夠了,也不需要派生新的EventArgs
類型,因為從sender 中可以獲得所有的信息。
我的解決方案可能更邪一點了,因為我是用反射和Emit 實現的,原理和你的差不多,只不過客戶端代碼可以非
常簡單而已。
13H回復 14H引用 15H查看
16H#20 樓 2006-10-07 16:37 可樂[匿名] [未注冊用戶]
看了例程非常激動,也許是還有很多東西要學沒有看的很徹底,希望樓主能解釋,就是在UI 中啟動一個輔助線
程后往往會應為需要在UI 啟動的這個輔助線程中在啟動其他線程,這個時候該怎樣使用Task 抽象類,該如何將
輔助線程啟動的其他線程中的信息顯示在UI 中,還有怎么才能利用AsynchThread 在UI 中停止輔助線程的時候
能同時停止輔助線程啟動的其他線程,希望樓主能解答,謝謝!!
17H回復 18H引用 19H查看
在.NET 客戶端程序中使用多線程
admin 發表于:04-06-08
通常認為在編寫程序中用到多線程是一個高級的編程任務,容易發生錯誤。在本月的欄目中,我將在一個Windows 窗體應用程序中使用多線程,
它具有實際的意義,同時盡量使事情簡單。我的目標是在一個普通的需求描述中用最好的辦法講解多線程;客戶仍然比較喜歡使用戶交互方式的應
用程序。
多線程通常和服務器端軟件,可擴展性及性能技術聯系在一起。 然而,在微軟.NET 框架中,許多服務器端應用程序都駐留在ASP.NET 體系結
構中。同樣,這些應用程序在邏輯上是單線程的, 因為IIS 和ASP.NET 在ASP.NET Web Form 或Web 服務程序中執行了許多或所有的多線程。 在
ASP.NET 應用程序中你一般可以忽略線程性。 這就是為什么在.NET 框架中,多線程更傾向于在客戶端使用的一個原因,比如在保證同用戶交互的同
時而執行一個很長的操作。
線程背景
線程執行代碼。它們由操作系統實現,是CPU 本身的一種抽象。許多系統都只有一個CPU, 線程是把CPU 快速的處理能力分開而執行多個操作
的一種方法,使它們看起來好像同步似的。即使一個系統由多個CPU, 但運行的線程一般要比處理器多。
在一個Windows 為基礎的應用程序中,每一個進程至少要有一個線程,它能夠執行機器語言指令。 一旦一個進程的所有線程都中止了,進程本
身和它所占用的資源將會被Windows 清除。
許多應用程序都被設計為單線程程序,這意味著該程序實現的進程從來不會有超過一個線程在執行,即使在系統中有多個同樣的處理在進行。
一般一個進程不會關心系統中其他進程的線程的執行。
然而,在單個進程里的所有線程不僅共享虛擬地址空間,而且許多進程級的資源也被共享, 比如文件和窗口句柄等。由于進程資源共享的特征,
一個線程必須考慮同一進程中其它線程正在做什么。線程同步是在多線程的進程中保持各線程互不沖突的一門藝術。這也使得多線程比較困難。
最好的方式是只有在需要時才使用多線程,盡量保持事情簡單。而且要避免線程同步的情況。在本欄目中,我將向你展示如何為一個普通的客
戶應用程序做這些事情。
為什么使用多個線程?
已經有許多單線程的客戶端應用程序,而且每天還有許多正在被寫。在許多情況下,單線程的行為已經足夠了。
然而,在某些特定的應用程序中加入一些異步行為可以提高你的經驗。典型的數據庫前端程序是一個很好的例子。
數據庫查詢需要花費大量時間完成。在一個單線程的應用程序里,這些查詢會導致window 消息處理能力阻塞,導致程序的用戶交互被凍結。解
決辦法就是,這個我將要詳細描述,用一個線程處理來自操作系統的消息,而另外一個線程做一個很長的工作。在你的代碼中使用第二個線程的重
要原因就是即使在幕后有一個繁忙的工作在進行,也要保證你的程序的用戶交互有響應。
我們首先看一下執行一長串操作的單線程的GUI 程序。然后我們將用額外的線程整理該程序。
Figure 1 是用C#寫的一個程序的完整源代碼。它創建了一個帶有文本框和按鈕的窗體。如果你在文本框中鍵入了一個數字,然后按下按鈕,
這個程序將處理你輸入的那個數字,它表示秒數,每秒鐘響鈴一次代表后臺的處理。除了Figure 1 的代碼外,你可以從本文開頭的鏈接中下載完
整的代碼。下載或鍵入Figure 1 所示的代碼,在讀之前編譯運行它,(編譯前,在Visual Studio.NET 中右擊你的工程,加入Microsoft Visual Basic
運行時引用)當你試著運行Figure 1 中的
SingleThreadedForm.cs 應用程序時,你馬上就會看到幾個問題。
Figure 1 SingleThreadedForm.cs
using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using Microsoft.VisualBasic;
class App {
// Application entry point
public static void Main() {
// Run a Windows Forms message loop
Application.Run(new SingleThreadedForm());
}
}
// A Form-derived type
class SingleThreadedForm : Form {
// Constructor method
public SingleThreadedForm() {
// Create a text box
text.Location = new Point(10, 10);
text.Size = new Size(50, 20);
Controls.Add(text);
// Create a button
button.Text = "Beep";
button.Size = new Size(50, 20);
button.Location = new Point(80, 10);
// Register Click event handler
button.Click += new EventHandler(OnClick);
Controls.Add(button);
}
// Method called by the button's Click event
void OnClick(Object sender, EventArgs args) {
// Get an int from a string
Int32 count = 0;
try { count = Int32.Parse(text.Text); } catch (FormatException) {}
// Count to that number
Count(count);
}
// Method beeps once per second
void Count(Int32 seconds) {
for (Int32 index = 0; index < seconds; index++) {
Interaction.Beep();
Thread.Sleep(1000);
}
}
// Some private fields by which to reference controls
Button button = new Button();
TextBox text = new TextBox();
}
在你第一次測試運行時,在文本框中輸入20,按下按鈕。你將看到程序的用戶交互變得完全沒有響應了。你不能單擊按鈕或者編輯文本框,程
序也不能被從容的關閉,如果你覆蓋該窗體接著會顯示一個窗口的部分區域,它將不再重繪自己(見 Figure 2),這個程序被鎖定足足20 秒, 然
而它還可以繼續響鈴,證明它還沒有真正的死掉。這個簡單的程序解釋了單線程GUI 程序的問題。
我將用多線程解決第一個問題:未響應的用戶交互,但首先我將解釋是什么導致了這種現象。
線程和Windows 用戶界面
Windows Forms 類庫建立在大家所熟知的User32 Win32 API 基礎上。User32 實現了GUI 的基本元素,例如窗體,菜單及按鈕之類等。所有由
User32 實現的窗體和控件都使用了事件驅動型結構。
這里簡單的講講它們如何工作。發生在窗體上的事情,例如鼠標單擊,坐標變化,大小變化和重繪請求,都稱作事件。在User32 API 模型中的
事件是由窗體消息表示的。每一個窗體有一個函數,叫做窗口過程或WndProc,它由應用程序實現。WndProc 為窗體負責處理窗體消息。
但是WndProc 不是神奇的被系統調用。相反,應用程序必須調用GetMessage 主動地從系統中得到窗體消息。該消息被應用程序調用
DispatchMethod API 方法分配到它們的目標窗體的WndProc 方法中。應用程序只是簡單的循環接收和分配窗口消息,一般叫做消息泵或消息循環。
線程擁有所有窗體,這樣它就可以提取消息,WndProc 函數也被同樣的線程所調用。
現在回到Windows Forms 類來。Windows Forms 在應用程序中對User32 的消息結構進行了大約95%的抽象。代替了WndProc 函數,Windows Forms
程序定義了事件處理器和虛擬函數重載來處理與窗體(窗口)或控件有關的不同系統事件。然而消息提取必須要運行,它在Windows Forms API 的
Application.Run 方法里面實現。
Figure 1 所示的代碼似乎僅僅調用了Application.Run 接著就退出了。 然而這缺少了透明性:應用程序的主線程在其生命周期里只對
Application.Run 進行一次調用進行消息提取,其結果卻為用應用程序其它部分創造了不同事件處理器的調用。當窗體上的按鈕被單擊時,在Figure
1 中的OnClick 方法被主線程調用,該線程同樣要負責在Application.Run 中提取消息。
這解釋了為什么在一個長操作發生時,用戶交互沒有響應。如果在一個事件處理器中一個很長的操作 (如數據庫查詢)發生了,那么主線程就
被占用,它又需要不斷提取消息。沒有能力提取消息并發送到窗口或窗體上, 就沒有能力響應調整大小,重繪自己,處理單擊或響應用戶的任何交
互。
在接下來的部分為了執行長操作我將使用公共語言運行時的線程池來修改Figure 1 所示的例子代碼,這樣主線程仍然可以提取消息。
托管線程池
CLR 為每一個托管進程維護了一個線程池,這意味著當你的應用程序主線程需要進行某些異步處理時,你可以很容易的從線程池中借助某個線
程實現特定的處理。一旦處理工作完成,線程被歸還到線程池以便以后使用。讓我們看一個例子,修改使用線程池。
注意Figure 3 中FlawMultiThreadForm.cs 中紅色部分表示的行;它們是由Figure 1 中的單線程變為多線程程序 時唯一要修改的代碼。如果
你編譯Figure 3 所示的代碼,并設置運行20 秒,你將看到當處理20 個響鈴的請求時,仍然能夠響應用戶的交互。在客戶端程序中使用多線程來
響應用戶交互是一個吸引人的原因。
Figure 3 FlawedMultiThreadedForm.cs
using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using Microsoft.VisualBasic;
class App {
// Application entry point
public static void Main() {
// Run a Windows Forms message loop
Application.Run(new FlawedMultiThreadedForm());
}
}
// A Form-derived type
class FlawedMultiThreadedForm : Form {
// Constructor method
public FlawedMultiThreadedForm() {
// Create a text box
text.Location = new Point(10, 10);
text.Size = new Size(50, 20);
Controls.Add(text);
// Create a button
button.Text = "Beep";
button.Size = new Size(50, 20);
button.Location = new Point(80, 10);
// Register Click event handler
button.Click += new EventHandler(OnClick);
Controls.Add(button);
}
// Method called by the button's Click event
void OnClick(Object sender, EventArgs args) {
// Get an int from a string
Int32 count = 0;
try { count = Int32.Parse(text.Text); } catch (FormatException) {}
// Count to that number
WaitCallback async = new WaitCallback(Count);
ThreadPool.QueueUserWorkItem(async, count);
}
// Async method beeps once per second
void Count(Object param) {
Int32 seconds = (Int32) param;
for (Int32 index = 0; index < seconds; index++) {
Interaction.Beep();
Thread.Sleep(1000);
}
}
// Some private fields by which to reference controls
Button button = new Button();
TextBox text = new TextBox();
}
然而,在Figure 3 中所做的變化,卻引入了一個新問題(如 Figure 3 的名字一樣);現在用戶可以啟動多個同時響鈴的長操作。在許多實
時應用中這會導致線程間的沖突。為了修正這個線程同步請求,我將講述這些,但首先熟悉一下CLR''''s 線程池。
類庫中的System.Threading.ThreadPool 類提供了一個訪問CLR''''s 線程池的API 接口, ThreadPool 類型不能被實例化,它由靜態成員組成。
ThreadPool 類型最重要的方法是對ThreadPool.QueueUserWorkItem 的兩個重載。這兩種方法讓你定義一個你愿意被線程池中的一個線程進行回調
的函數。通過使用類庫中的WaitCallback 委托類型的一個實例來定義你的方法。一種重載讓你對異步方法定義一個參數;這是Figure 3 所使用的
版本。
下面的兩行代碼創建一個委托實例,代表了一個Count 方法,接下來的調用排隊等候讓線程池中的方法進行回調。
WaitCallback async = new WaitCallback(Count);
ThreadPool.QueueUserWorkItem(async, count);
ThreadPool.QueueUserWorkItem 的兩個方法讓你在隊列中定義一個異步回調方法,然后立即返回。 同時線程池監視這個隊列,接著出列方法,
并使用線程池中的一個或多個線程調用該方法。這是CLR''''s 線程池的主要用法。
CLR''''s 線程池也被系統的其它APIs 所使用。例如, System.Threading.Timer 對象在定時間隔到來時將會在線程池中排隊等候回調。
ThreadPool.RegisterWaitForSingleObject 方法當響應內核系統同步對象有信號時會在線程池中排隊等候調用。最后,回調由類庫中的不同異步方
法執行,這些異步方法又由CLR''''s 線程池來執行。
一般來說,一個應用程序僅僅對于簡單的異步操作需要使用多線程時毫無疑問應該使用線程池。相比較手工創建一個線程對象,這種方法是被
推薦的。調用ThreadPool.QueueUserWorkItem 執行簡單,而且相對于重復的手動創建線程來說能夠更好的利用系統資源。
最簡單的線程同步
在本欄目開始我就稱保持線程同步而不互相沖突是一門藝術。Figure 3 所示的FlawedMultiThreadForm.cs 應用程序有一個問題:用戶可以通
過單擊按鈕引發一個很長的響鈴操作,他們可以繼續單擊按鈕而引發更多的響鈴操作。如果不是響鈴,該長操作是數據庫查詢或者在進程的內存中
進行數據結構操作,你一定不想在同一時間內,有一個以上的線程做同樣的工作。最好的情況下這是系統資源的一種浪費,最壞的情況下會導致數
據毀滅。
最容易的解決辦法就是禁止按鈕一類的用戶交互元素;兩個進程間的通信稍微有點難度。過一會我將給你看如何做這些事情。但首先,讓我指
出所有線程同步使用的一些線程間通信的形式-從一個線程到另一個線程通信的一種手段。稍后我將討論大家所熟知的AutoResetEvent 對象類型,
它僅用在線程間通信。
現在讓我們首先看一下為Figure 3 中FlawedMultiThreadedForm.cs 程序中加入的線程同步代碼。再一次的,Figure 4
CorrectMultiThreadedForm.cs 程序中紅色部分表示的是其先前程序的較小的改動部分。 如果你運行這個程序你將看到當一個長響鈴操作在進行時
用戶交互被禁止了(但沒有掛起),響鈴完成的時候又被允許了。這次這些代碼的變化已經足夠了,我將逐個運行他們。
Figure 4 CorrectMultiThreadedForm.cs
using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using Microsoft.VisualBasic;
class App {
// Application entry point
public static void Main() {
// Run a Windows Forms message loop
Application.Run(new CorrectMultiThreadedForm());
}
}
// A Form-derived type
class CorrectMultiThreadedForm : Form{
// Constructor method
public CorrectMultiThreadedForm() {
// Create a textbox
text.Location = new Point(10, 10);
text.Size = new Size(50, 20);
Controls.Add(text);
// Create a button
button.Text = "Beep";
button.Size = new Size(50, 20);
button.Location = new Point(80, 10);
// Register Click event handler
button.Click += new EventHandler(OnClick);
Controls.Add(button);
// Cache a delegate for repeated reuse
enableControls = new BooleanCallback(EnableControls);
}
// Method called by the button's Click event
void OnClick(Object sender, EventArgs args) {
// Get an int from a string
Int32 count = 0;
try { count = Int32.Parse(text.Text); } catch (FormatException) {}
// Count to that number
EnableControls(false);
WaitCallback async = new WaitCallback(Count);
ThreadPool.QueueUserWorkItem(async, count);
}
// Async method beeps once per second
void Count(Object param) {
Int32 seconds = (Int32) param;
for (Int32 index = 0; index < seconds; index++) {
Interaction.Beep();
Thread.Sleep(1000);
}
Invoke(enableControls, new Object[]{true});
}
void EnableControls(Boolean enable) {
button.Enabled = enable;
text.Enabled = enable;
}
// A delegate type and matching field
delegate void BooleanCallback(Boolean enable);
BooleanCallback enableControls;
// Some private fields by which to reference controls
Button button = new Button();
TextBox text = new TextBox();
}
在Figure 4 的末尾處有一個EnableControls 的新方法,它允許或禁止窗體上的文本框和按鈕控件。在Figure 4 的開始我加入了一個
EnableControls 調用,在后臺響鈴操作排隊等候之前立即禁止文本框和按鈕。到這里線程的同步工作已經完成了一半,因為禁止了用戶交互,所以
用戶不能引發更多的后臺沖突操作。在Figure 4 的末尾你將看到一個名為BooleanCallback 的委托類型被定義,其簽名是同EnableControls 方法
兼容的。在那個定義之前,一個名為EnableControls 的委托域被定義(見例子),它引用了該窗體的EnableControls 方法。這個委托域在代碼的
開始處被分配。
你也將看到一個來自主線程的回調,該主線程為窗體和其控件擁有和提取消息。這個調用通過向EnableControls 傳遞一個true 參數來使能控
件。這通過后臺線程調用窗體的Invoke 方法來完成,當其一旦完成其長響鈴操時。代碼傳送的委托引用EnableControls 去Invoke,該方法的參數
帶有一個對象數組。Invoke 方法是線程間通信的一個非常靈活的方式,特別是對于Windows Forms 類庫中的窗口或窗體。在這個例子中,Invoke
被用來告訴主GUI 線程通過調用EnableControls 方法重新使能窗體上的控件。
Figure 4 中的CorrectMultiThreadedForm.cs 的變化實現了我早先的建議――當響鈴操作在執行時你不想運行,就禁止引發響鈴操作的用戶交
互部分。當操作完成時,告訴主線程重新使能被禁止的部分。對Invoke 的調用是唯一的,這一點應該注意。
Invoke 方法在 System.Windows.Forms.Controls 類型中定義,包含Form 類型讓類庫中的所有派生控件都可使用該方法。Invoke 方法的目的是
配置了一個從任何線程對為窗體或控件實現消息提取線程的調用。
當訪問控件派生類時,包括Form 類,從提取控件消息的線程來看你必須這樣做。這在單線程的應用程序中是很自然的事情。但是當你從線程池
中使用多線程時,要避免從后臺線程中調用用戶交互對象的方法和屬性是很重要的。相反,你必須使用控件的Invoke 方法間接的訪問它們。Invoke
是控件中很少見的一個可以安全的從任何線程中調用的方法,因為它是用Win32 的PostMessage API 實現的。
使用Control.Invoke 方法進行線程間的通信有點復雜。但是一旦你熟悉了這個過程,你就有了在你的客戶端程序中實現多線程目標的工具。本
欄目的剩余部分將覆蓋其它一些細節,但是Figure 4 中的CorrectMultiThreadedForm.cs 應用程序是一個完整的解決辦法:當執行任意長的操作
時仍然能夠響應用戶的其它操作。盡管大多數的用戶交互被禁止,但用戶仍然可以重新配置和調整窗口,也可以關閉程序。然而,用戶不能任意使
用程序的異步行為。這個小細節能夠讓你對你的程序保持自信心。
在我的第一個線程同步程序中,沒有使用任何傳統的線程結構,例如互斥或信號量,似乎一錢不值。然而,我卻使用了禁止控件的最普通的方
法。
細節-實現一個取消按鈕
有時你想為你的用戶提供一種取消長操作的方法。你所需要的就是你的主線程同后臺線程之間的一些通信方法,通知后臺線程操作不再被需要,
可以停止。System.Threading 名字空間為這個方法提供了一個類:AutoResetEvent。
AutoResetEvent 是線程間通信的一種簡單機制。一個AutoResetEvent 對象可以有兩種狀態中的一個:有信號的和無信號的。當你創建一個
AutoResetEvent 實例時,你可以通過構造函數的參數來決定其初始狀態。然后感知該對象的線程通過檢查AutoResetEvent 對象的狀態,或者用
AutoResetEvent 對象的Set 或Reset 方法調整其狀態,進行相互通信。
在某種程度上AutoResetEvent 很像一個布爾類型,但是它提供的特征使其更適合于在線程間進行通信。這樣的一個例子就是它有這種能力:一
個線程可以有效的等待直到一個AutoResetEvent 對象從一個無信號的狀態變為有信號的狀態。它是通過在該對象上調用WaitOne 實現的。任何一個
線程對一個無信號的AutoResetEvent 對象調用了WaitOne,就會被有效的阻塞直到其它線程使該對象有信號。使用布爾變量線程必須在一個循環中
登記該變量,這是無效率的。一般來說沒有必要使用Reset 來使一個AutoResetEvent 變為無信號,因為當其它線程感知到該對象為有信號時,它會
被立即自動的設為無信號的。
現在你需要一種讓你的后臺線程無阻塞的測試AutoResetEvent 對象的方法,你會有許多工具實現線程的取消。為了完成這些,調用帶有WaitOne
的重載窗體并指出一個零毫秒的超出時間,以零毫秒為超出時間的WaitOne 會立即返回,而不管AutoResetEvent 對象的狀態是否為有信號。如果返
回值為true,這個對象是有信號的;否則由于時間超出而返回。
我們整理一下實現取消的特點。如果你想實現一個取消按鈕,它能夠取消后臺線程中的一個長操作,按照以下步驟:
在你的窗體上加入AutoResetEvent 域類型
通過在AutoResetEvent 的構造函數中傳入false 參數,設置該對象初始狀態為無信號的。 接著在你的窗體上保 存該對象的引用域,這是為了
能夠在窗體的整個生命周期內可以對后臺線程的后臺操作實現取消操作。
在你窗體上加入一個取消按鈕。
在取消按鈕的Click 事件處理器中,通過調用AutoResetEvent 對象的Set 方法使其有信號。
同時,在你的后臺線程的邏輯中周期性地在AutoResetEvent 對象上調用WaitOne 來檢查用戶是否取消了。
if(cancelEvent.WaitOne(0, false)){
// cancel operation
}
你必須記住使用零毫秒參數,這樣可以避免在后臺線程操作中不必要的停頓。
如果用戶取消了操作,通過主線程AutoResetEvent 會被設為有信號的。 當WaitOne 返回true 時你的后臺線程會 得到警告,并停止操作。同
時在后臺線程中由于調用了WaitOne 該事件會被自動的置為無信號狀態。
為了能夠看到取消長操作窗體的例子,你可以下載CancelableForm.cs 文件。這個代碼是一個完整的程序,它與Figure 4 中的
CorrectMultiThreadedForm.cs 只有稍微的不同。
注意在CancelableForm.cs 也采用了比較高級的用法Control.Invoke, 在那里EnableControls 方法被設計用來調用它自己如果當它被一個錯
誤的線程所調用時。在它使用窗體上的任何GUI 對象的方法或屬性時要先做這個檢查。 這樣能夠使得EnableControls 能夠從任何線程中直接安全
的調用,在方法的實現中有效的隱藏了Invoke 調用的復雜性。這些可以使應用程序更加有維護性。注意在這個例子中同樣使用了
Control.BeginInvoke, 它是Control.Invoke 的異步版本。
你也許注意到取消的邏輯依賴于后臺線程通過WaitOne 調用周期性的取消檢查的能力。 但是如果正在討論的問題不能被取消怎么辦?如果后臺
操作是一個單個調用,像DataAdapter.Fill,它會花很長時間?有時會有解決辦法的,但并不總是。
如果你的長操作根本不能取消,你可以使用一個偽取消的方法來完成你的操作,但在你的程序中不要影響你的操作結果。這不是技術上的取消
操作,它把一個可忍受的操作幫定到一個線程池中,但這是在某種情況下的一種折中辦法。如果你實現了類似的解決辦法,你應該從你的取消按鈕
事件處理器中直接使能你已禁止的UI 元素,而不要還依賴于被綁定的后臺線程通過Invoke 調用使能你的控件。同樣重要的使設計你的后臺操作線
程,當其返回時測試一下它是否被取消,以便它不影響現在被取消的操作的結果。
這種長操作取消是比較高級的方法,它只在某些情況下才可行。例如,數據庫查詢的偽取消就是這樣,但是一個數據庫的更新,刪除,插入偽
取消是一個滯后的操作。有永久的操作結果或與反饋有關的操作,像聲音和圖像,就不容易使用偽取消方法,因為操作的結果在用戶取消以后是非
常明顯的。
更多細節-有關定時器
在應用程序中需要一個定時器來引發一個定期的任務一定不一般。例如,如果你的程序在窗體的狀態條上顯示當前時間,你可能每5 秒鐘更新
一次時間。System.Threading 名字空間包括了一個名為Timer 多線程定時器類。
當你創建一個定時器類的實例時,你為定時器回調指明了一個以毫秒為單位的周期,而且你也傳遞給該對象一個委托用來每過一個時鐘周期調
用你。回調發生在線程池中的線程上。事實上,每次時鐘周期到來時真正發生的是一個工作條目在線程池中排隊;一般來說一個調用會馬上發生的,
但是如果線程池比較忙,這個回調也許會在稍后的一個時間點發生。
如果你考慮在你的程序中使用多線程,你也許會考慮使用定時器類。然而,如果你的程序使用了Windows 窗體,你不必使用多線程的行為,在
System.Windows.Forms 名字空間中有另外一個也叫Timer 的定時器類。
System.Windows.Forms.Timer 與其多線程的同伴比起來有一個明顯的好處:因為它不是多線程的,所以不會在其它線程中對你進行回調,而且
更適合為應用程序提取窗口消息的主線程。實際上System.Windows.Forms.Timer 的實現是在系統中使用了WM_TIMER 的一個窗口消息。這種方法在
你的System.Windows.Forms.Timer 的事件處理器中不必擔心線程同步,線程間通信之類的問題。
對于Windows 窗體類程序,作為一個很好的技巧就是使用System.Windows.Forms.Timer 類, 除非你特別需要線程池中的線程對你進行回調。
既然這種要求很少見,為了使事情簡單,把使用System.Windows.Forms.Timer 作為一個規則,即使在你的程序的其它地方使用了多線程。
展望將來
微軟最近展示了一個即將出現的GUI API,代號為“Avalon”,本期MSDN 雜志的問題列表中(見70 頁)Charles Petzold''''s 的文章描述了
其特點。在Avalon 框架中用戶接口元素沒有被系與一個特殊的線程;作為更換每個用戶接口元素與一個單獨的邏輯線程上下文相關聯,在UIContext
類中實現。但是當你發現UIContext 類中包含了Invoke 方法,及其姊妹BeginInvoke 時,你就不會驚奇了,在名字上與窗體類中的控件類上名稱一
樣的目的是說明他們在邏輯作用上是一致的。
作者簡介
Jason Clark 為微軟和Wintellect 公司提供培訓和咨詢,他是Windows NT 和Windows 2000 服務器團隊的開發前輩。他是Windows 2000 服務
器程序編程一書的合著者。與Jason 的聯系方式:[email protected]
.net WinForm 控件的事件委托剖析 選擇自 120Hflashvan 的 Blog
關鍵字: Delegate, MulticastDelegate, EventHandler, EventHandlerList, EventHandlerList.ListEntry, Control,
Component
首先從controlInstance.Click 事件開始. 用Reflector 反編譯System.Windows.Forms.Control 類可以看到對
Click 事件的定義:
[System.Windows.Forms.SRCategory("CatAction"), System.Windows.Forms.SRDescription("Contr
olOnClickDescr")]
public event EventHandler Click
{
add
{
base.Events.AddHandler(Control.EventClick, value);
}
remove
{
base.Events.RemoveHandler(Control.EventClick, value);
}
}
這里的Control.EventClick 是一個只讀的靜態私有屬性,它是以后事件查找委托的鍵(key),請記住這個.
private static readonly object EventClick = new object();
Control 的Events 屬性是由System.ComponentModel.Component 繼承而來,它是EventHandlerList 的實例.
private EventHandlerList events;
protected EventHandlerList Events
{
get
{
if (this.events == null)
{
this.events = new EventHandlerList();
}
return this.events;
}
}
EventHandlerList 類有三個重要的方法:
public void AddHandler(object key, Delegate value);
public void RemoveHandler(object key, Delegate value);
private ListEntry Find(object key);
AddHandler 的作用是插入一個鍵和一個委托類型的值, 插入之前通過Find 方法檢查一下同樣的委托對象
是否存在,如果存在,則合并; 如果不存在,以要插入的委托對象(value)為頭.
public void AddHandler(object key, Delegate value)
{
EventHandlerList.ListEntry entry1 = this.Find(key);
if (entry1 != null)
{
entry1.handler = Delegate.Combine(entry1.handler, value);
}
else
{
this.head = new EventHandlerList.ListEntry(key, value, this.head);
}
}
如果是一個按鈕的Click 事件,我們一般定義為:
button1.Click += new EventHandler(OnButtonClick);
protected void OnButtonClick(object sender, System.EventArgs e)
{
// 你的處理函數
}
則通過了button1.Events.AddHandler(Control.EventClick, EventHandler handler),而這個handler 卻是一
個MulticastDelegate 的實例。看MS 公布的.net framework 部分源碼就知道了:
// ==++==
//
//
// Copyright (c) 2002 Microsoft Corporation. All rights reserved.
//
// The use and distribution terms for this software are contained in the file
// named license.txt, which can be found in the root of this distribution.
// By using this software in any fashion, you are agreeing to be bound by the
// terms of this license.
//
// You must not remove this notice, or any other, from this software.
//
//
// ==--==
namespace System {
using System;
/// <include file='doc/EventHandler.uex' path='docs/doc[@for="EventHandler"]/*' />
[Serializable()]
public delegate void EventHandler(Object sender, EventArgs e);}
現在我們轉到對委托(Delegate)和多播委托(MulticastDelegate)的研究了。
Delegate 類已經封裝好產生委托,消除委托和執行委法的方法,它是一個不能實例化的抽象類。但.net 的
編譯器支持由delegate 定義的類型來實例化一個Delegate 對象,它能讓這個對象的執行委托方法像普通
函數一樣調用(具體的可以看C#高級編程里面的實例),所以很多時候,delegate 類型會被認為是函數指針。
Delegate 還有兩個很重要的方法,組合委托Combine 和刪除委托Remove。在單播委托Delegate 中使用
這組合委托方法會拋出多播不支持異常(MulticastNotSupportedException)。而使用刪除委托方法時,如果
這個單播委托和要刪除的委托是同一個值時,則返回null,證明已經刪除;如果不是,則原封不動返回原
來的單播委托。
EventHandler 實際上是一個多播委托實例,所以它支持組合委托和刪除委托的方法。這個實例,實際上是
一個委托實例鏈,它是這個鏈的鏈尾。每次像調用普通函數調用這個委托的時候,這個委托會執行完委托
的代理函數,并查找鏈表中上一個委托實例,執行這個委托的代理函數,再查找鏈表中上上個委托實例,
執行當前委托的代理函數。。。 一直到鏈表被遍歷完。
protected override sealed Object DynamicInvokeImpl(Object[] args)
{
if (_prev != null)
_prev.DynamicInvokeImpl(args);
return base.DynamicInvokeImpl(args);
}
好了。那可以想象,一個用戶點擊按鈕button1,首先執行的函數是OnClick 函數
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected virtual void OnClick(EventArgs e)
{
if (this.CanRaiseEvents)
{
EventHandler handler1 = (EventHandler) base.Events[Control.EventClick];
if (handler1 != null)
{
handler1(this, e);
}
}
}
handler1 就是一個多播委托, 如果不為空,則執行它,而且這個執行就是執行所有的代理函數。這樣就
明白了WinForm控件Click事件所有的始終了!
參考文章: 121Hhttp://blog.sunmast.com/Sunmast/archive/2005/04/21/1769.aspx
以上是個人觀點,由于時間倉促及個人水平有限,難免錯誤,敬請斧正!
c#中使用多線程(圖) 選擇自 12Hiuhxq 的 Blog
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Threading;
namespace student
{
/// <summary>
/// Form1 的摘要說明。
/// </summary>
public class Form1 : System.Windows.Forms.Form
{
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.Button button3;
ArrayList threads = new ArrayList();
private System.Windows.Forms.ListView listView1;
private System.Windows.Forms.ColumnHeader columnHeader1;
private System.Windows.Forms.ColumnHeader columnHeader2;
/// <summary>
/// 必需的設計器變量。
/// </summary>
private System.ComponentModel.Container components = null;
public Form1()
{
//
// Windows 窗體設計器支持所必需的
//
InitializeComponent();
//
// TODO: 在 InitializeComponent 調用后添加任何構造函數代碼
//
}
/// <summary>
/// 清理所有正在使用的資源。
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#region Windows 窗體設計器生成的代碼
/// <summary>
/// 設計器支持所需的方法 - 不要使用代碼編輯器修改
/// 此方法的內容。
/// </summary>
private void InitializeComponent()
{
this.button1 = new System.Windows.Forms.Button();
this.button2 = new System.Windows.Forms.Button();
this.button3 = new System.Windows.Forms.Button();
this.listView1 = new System.Windows.Forms.ListView();
this.columnHeader1 = new System.Windows.Forms.ColumnHeader();
this.columnHeader2 = new System.Windows.Forms.ColumnHeader();
this.SuspendLayout();
//
// button1
//
this.button1.Location = new System.Drawing.Point(24, 216);
this.button1.Name = "button1";
this.button1.TabIndex = 1;
this.button1.Text = "Add";
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// button2
//
this.button2.Location = new System.Drawing.Point(104, 216);
this.button2.Name = "button2";
this.button2.TabIndex = 2;
this.button2.Text = "Del";
this.button2.Click += new System.EventHandler(this.button2_Click);
//
// button3
//
this.button3.Location = new System.Drawing.Point(184, 216);
this.button3.Name = "button3";
this.button3.TabIndex = 3;
this.button3.Text = "DelAll";
this.button3.Click += new System.EventHandler(this.button3_Click);
//
// listView1
//
this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeader1,
this.columnHeader2});
this.listView1.FullRowSelect = true;
this.listView1.GridLines = true;
this.listView1.Location = new System.Drawing.Point(0, 0);
this.listView1.Name = "listView1";
this.listView1.Size = new System.Drawing.Size(288, 208);
this.listView1.TabIndex = 5;
this.listView1.View = System.Windows.Forms.View.Details;
//
// columnHeader1
//
this.columnHeader1.Text = "線程編號";
this.columnHeader1.Width = 81;
//
// columnHeader2
//
this.columnHeader2.Text = "value";
this.columnHeader2.Width = 180;
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(6, 14);
this.ClientSize = new System.Drawing.Size(292, 266);
this.Controls.Add(this.listView1);
this.Controls.Add(this.button3);
this.Controls.Add(this.button2);
this.Controls.Add(this.button1);
this.Name = "Form1";
this.Text = "Form1";
this.Load += new System.EventHandler(this.Form1_Load);
this.ResumeLayout(false);
}
#endregion
/// <summary>
/// 應用程序的主入口點。
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
private void Form1_Load(object sender, System.EventArgs e)
{
}
private void button1_Click(object sender, System.EventArgs e)
{
Add();
}
private void button2_Click(object sender, System.EventArgs e)
{
Del();
}
private void button3_Click(object sender, System.EventArgs e)
{
while(threads.Count>0)
{
Thread t1 = (Thread)threads[0];
if(t1.IsAlive)
{
t1.Abort();
}
threads.RemoveAt(0);
lock(listView1)
{
listView1.Items.RemoveAt(0);
}
}
}
private void Add()
{
int count = threads.Count;
if(count<10)
{
Thread t = new Thread(new ThreadStart(Process));
t.Start();
threads.Add(t);
lock(listView1)
{
listView1.Items.Insert(count, new ListViewItem(new
string[]{count.ToString(),"0"}));
}
}
}
private void Del()
{
int count = threads.Count;
if(count>0)
{
Thread t1 = (Thread)threads[count-1];
if(t1.IsAlive)
{
t1.Abort();
}
threads.RemoveAt(count-1);
lock(listView1)
{
listView1.Items.RemoveAt(count-1);
}
}
}
private void Process()
{
int i = 1;
while(true)
{
i++;
int j = threads.IndexOf(Thread.CurrentThread);
lock(listView1)
{
listView1.Items[j].SubItems[1].Text = i.ToString();
}
Thread.Sleep(0);
}
}
}
}
作者Blog:123Hhttp://blog.csdn.net/iuhxq/
相關文章
124H[原創]c#中使用多線程(圖)二
125HSQL 存儲過程&算法
126H取漢字拼音首字母的存儲過程
127HDataGrid自定義分頁存儲過程
利用Visual C#打造一個平滑的進度條 選擇自 128Hswazn_yj 的 Blog
利用Visual C#打造一個平滑的進度條
概述
本文描述了如何建立一個簡單的、自定義的用戶控件——一個平滑的進度
條。
在早先的進度條控件版本中,例如在 Microsoft Windows Common Controls
ActiveX 控件中提供的版本,您可以看到進度條有兩種不同的視圖。您可以通過
設定 Scrolling 屬性來設定 Standard 視圖或是 Smooth 視圖。 Smooth 視圖
提供了一個區域來平滑的顯示進度, Standard 試圖則看上去是由一個一個方塊
來表示進度的。
在 Visual C# .NET 中提供的進度條控件只支持 Standard 視圖。
本文的代碼樣例揭示了如何建立一個有如下屬性的控件:
Minimum。該屬性表示了進度條的最小值。默認情況下是 0 ;您不能將該
屬性設為負值。
Maximum。該屬性表示了進度條的最大值。默認情況下是 100 。
Value。該屬性表示了進度條的當前值。該值必須介于 Minimum 和
Maximum 之間。
ProgressBarColor。該屬性表示了進度條的顏色。
建立一個自定義的進度條控件
1、按著下面的步驟,在 Visual C# .NET 中建立一個 Windows Control
Library 項目:
a、打開 Microsoft Visual Studio .NET。
b、點擊 File 菜單,點擊 New ,再點擊 Project 。
c、在 New Project 對話框中,在 Project Types 中選擇 Visual C#
Projects,然后在 Templates 中選擇 Windows Control Library 。
d、在 Name 框中,填上 SmoothProgressBar ,并點擊 OK 。
e、在 Project Explorer 中,重命名缺省的 class module ,將
UserControl1.cs 改為 SmoothProgressBar.cs 。
f、在該 UserControl 對象的 Property 窗口中,將其 Name 屬性從
UserControl1 改為 SmoothProgressBar 。
2、此時,您已經從 control 類繼承了一個新類,并可以添加新的功能。但
是,ProgressBar 累是密封(sealed)的,不能再被繼承。因此,您必須從頭開始
建立這個控件。
將下面的代碼添加到UserControl 模塊中,就在“Windows Form Designer
generated code”之后:
int min = 0; // Minimum value for progress range
int max = 100; // Maximum value for progress range
int val = 0; // Current progress
Color BarColor = Color.Blue; // Color of progress meter
protected override void OnResize(EventArgs e)
{
// Invalidate the control to get a repaint.
this.Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
Graphics g = e.Graphics;
SolidBrush brush = new SolidBrush(BarColor);
float percent = (float)(val - min) / (float)(max - min);
Rectangle rect = this.ClientRectangle;
// Calculate area for drawing the progress.
rect.Width = (int)((float)rect.Width * percent);
// Draw the progress meter.
g.FillRectangle(brush, rect);
// Draw a three-dimensional border around the control.
Draw3DBorder(g);
// Clean up.
brush.Dispose();
g.Dispose();
}
public int Minimum
{
get
{
return min;
}
set
{
// Prevent a negative value.
if (value < 0)
{
min = 0;
}
// Make sure that the minimum value is never set higher than
the maximum value.
if (value > max)
{
min = value;
min = value;
}
// Ensure value is still in range
if (val < min)
{
val = min;
}
// Invalidate the control to get a repaint.
this.Invalidate();
}
}
public int Maximum
{
get
{
return max;
}
set
{
// Make sure that the maximum value is never set lower than
the minimum value.
if (value < min)
{
min = value;
}
max = value;
// Make sure that value is still in range.
if (val > max)
{
val = max;
}
// Invalidate the control to get a repaint.
this.Invalidate();
}
}
public int Value
{
get
{
return val;
}
set
{
int oldValue = val;
// Make sure that the value does not stray outside the valid
range.
if (value < min)
{
val = min;
}
else if (value > max)
{
val = max;
}
else
{
val = value;
}
// Invalidate only the changed area.
float percent;
Rectangle newValueRect = this.ClientRectangle;
Rectangle oldValueRect = this.ClientRectangle;
// Use a new value to calculate the rectangle for progress.
percent = (float)(val - min) / (float)(max - min);
newValueRect.Width = (int)((float)newValueRect.Width *
percent);
// Use an old value to calculate the rectangle for progress.
percent = (float)(oldValue - min) / (float)(max - min);
oldValueRect.Width = (int)((float)oldValueRect.Width *
percent);
Rectangle updateRect = new Rectangle();
// Find only the part of the screen that must be updated.
if (newValueRect.Width > oldValueRect.Width)
{
updateRect.X = oldValueRect.Size.Width;
updateRect.Width = newValueRect.Width -
oldValueRect.Width;
}
else
{
updateRect.X = newValueRect.Size.Width;
updateRect.Width = oldValueRect.Width -
newValueRect.Width;
}
updateRect.Height = this.Height;
// Invalidate the intersection region only.
this.Invalidate(updateRect);
}
}
public Color ProgressBarColor
{
get
{
return BarColor;
}
set
{
BarColor = value;
// Invalidate the control to get a repaint.
this.Invalidate();
}
}
private void Draw3DBorder(Graphics g)
{
int PenWidth = (int)Pens.White.Width;
g.DrawLine(Pens.DarkGray, new
Point(this.ClientRectangle.Left, this.ClientRectangle.Top),
new Point(this.ClientRectangle.Width - PenWidth,
this.ClientRectangle.Top));
g.DrawLine(Pens.DarkGray, new
Point(this.ClientRectangle.Left, this.ClientRectangle.Top), new
Point(this.ClientRectangle.Left, this.ClientRectangle.Height -
PenWidth));
g.DrawLine(Pens.White, new Point(this.ClientRectangle.Left,
this.ClientRectangle.Height - PenWidth),
new Point(this.ClientRectangle.Width - PenWidth,
this.ClientRectangle.Height - PenWidth));
g.DrawLine(Pens.White, new Point(this.ClientRectangle.Width -
PenWidth, this.ClientRectangle.Top),
new Point(this.ClientRectangle.Width - PenWidth,
this.ClientRectangle.Height - PenWidth));
}
3、在 Build 菜單中,點擊 Build Solution 來編譯整個項目。
建立一個簡單的客戶端應用
1、在 File 菜單中,點擊 New ,再點擊Project。
2、在 Add New Project 對話框中,在 Project Types 中點擊 Visual C#
Projects,在 Templates 中點擊 Windows Application,并點擊 OK。
3、按照下面的步驟,在 Form 上添加兩個 SmoothProgressBar 實例:
a、在 Tools 菜單上,點擊 Customize Toolbox。
b、點擊 .NET Framework Components 頁。
c、點擊 Browse,然后選中你在 Create a Custom ProgressBar Control 段
中建立的 SmoothProgressBar.dll 文件。
d、點擊 OK。您可以看到在 toolbox 中已經有 SmoothProgressBar 控件了。
e、從 toolbox 中拖兩個 SmoothProgressBar 控件的實例到該 Windows
Application 項目中的默認 form 上。
4、從 toolbox 頁中拖一個 Timer 控件到 form 上。
5、將下面的代碼添加到 Timer 控件的 Tick 事件中:
if (this.smoothProgressBar1.Value > 0)
{
this.smoothProgressBar1.Value--;
this.smoothProgressBar2.Value++;
}
else
{
this.timer1.Enabled = false;
}
6、從 toolbox 頁中拖一個 Button 控件到 form 上。
7、將下面的代碼添加到 Button 控件的 Click 事件中:
this.smoothProgressBar1.Value = 100;
this.smoothProgressBar2.Value = 0;
this.timer1.Interval = 1;
this.timer1.Enabled = true;
8、在 Debug 菜單中,點擊 Start 來運行樣例項目。
9、點擊Button。注意觀察那兩個進度指示器。一個逐漸減小,另一個逐漸
增加。
作者Blog:129Hhttp://blog.csdn.net/swazn_yj/
130H Windows 窗體多線程
Windows 窗體多線程
當我們在編寫一個需要長時間運行的程序時(如數學計算,執行數據庫命令,訪問
WebService)
常常將它們寫在一個組件中,讓他們在后臺運行.從而不影響Windows 界面的顯示和界面上的

互操作.但我們有時還是感到不怎方便,如我們不能直接應用winForm里定義的變量等.那么在
UI 進程中能否直接執行長時間運行的程序,而不影響UI 進程呢?
下面的示例將解決這個問題.
本例利用多線程從長時間運行的操作(計算fbnc 數列(n>36))中分離出用戶界面 (UI),
以將用戶的后續輸入傳遞給輔助線程(CalHandler,showDel)以調節其行為與用戶界面元素
進行交互,從而實現穩定而正確的多線程處理的消息傳遞方
案。
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;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Messaging;
namespace AsynchCalcPi
{
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
Form2.calComplete += new CalHandler(Form2_calComplete);
}
void Form2_calComplete(string strTemp)
{
//為了對調用者屏蔽與此 UI 線程有關的線程安全通信信息,
//ShowCalcResult 方法在此 UI 線程上通過 Control.BeginInvoke 方法使
用 showDel 給自己發送消息。
//Control.BeginInvoke 異步隊列為 UI 線程提供服務,并且不等待結果就繼續運
行。
if(!bClose )
this.BeginInvoke(new showDel(showRes ),strTemp );
}
int times = 1;
private void showRes(string strTemp)
{
times += 1;
this.richTextBox1.AppendText("," + strTemp);
this.progressBar1.Value = iStep * times%100;
if (FinishFlag)
{
//timer1.Enabled = false;
MessageBox.Show(strTemp);
}
}
private delegate void showDel(string stemp);
private void button1_Click(object sender, EventArgs e)
{
try
{
j = Int32.Parse(this.textBox_Num.Text.Trim());
iStep = 100 / j;
if (j < 1)
return;
}
catch
{
MessageBox.Show("請在文本框內輸入數字字符");
return;
}
for (int i = 0; i < j; i++)
this.richTextBox1.AppendText(this.ComputeFibonacci(i).ToString() +
",");
}
private long ComputeFibonacci(int n)
{
//' The parameter n must be >= 0 and <= 91.
//' Fib(n), with n > 91, overflows a long.
if (n < 0 || n > 91)
{
MessageBox.Show("value must be >= 0 and <= 91", "n");
}
long result = 0;
if (n < 2)
result = 1;
else
{
result = ComputeFibonacci(n - 1) + ComputeFibonacci(n - 2);
}
return result;
}
public int AddInterlink(int i)
{
if (i <= 0)
return 0;
else if (i > 0 && i <= 2)
return 1;
else return AddInterlink(i - 1) + AddInterlink(i - 2);
}
private void button2_Click(object sender, EventArgs e)
{
try
{
j = Int32.Parse(this.textBox_Num.Text.Trim());
iStep = 100 / j;
if (j < 1)
return;
}
catch
{
MessageBox.Show("請在文本框內輸入數字字符");
return;
}
for (int i = 0; i < j; i++)
this.richTextBox1.AppendText(this.AddInterlink(i).ToString() + ",");
}
private void button3_Click(object sender, EventArgs e)
{
try
{
j = Int32.Parse(this.textBox_Num.Text.Trim());
iStep = 100 / j;
if (j < 1)
return;
}
catch
{
MessageBox.Show("請在文本框內輸入數字字符");
return;
}
ComputeFibonacciDel calcFbnc = new ComputeFibonacciDel(this.Compu
teFibonacci);
calcFbnc.BeginInvoke(j, callBack, null);
}
//實時顯示通知服務
private long ShowCalcResult(int n)
{
long result1 = 0;
for (int i = 0; i < n; i++)
{
result1 = this.ComputeFibonacci(i);
//委托calComplete 由輔助線程用于向 UI 線程回傳消息,通常是有關長時間運
行的操作的最新進度。
calComplete(result1.ToString() );
}
return result1;
}
//定義計算過程中用于傳遞消息的委托
public delegate void CalHandler(string strTemp);
//定義事件
public static event CalHandler calComplete;
//定義委托 進行異步計算Fibonacci 數列
private delegate long ComputeFibonacciDel(int n);
//定義引用在異步操作完成時調用的回調方法.用以在計算完成后取得返回值和當前狀
態.
AsyncCallback callBack = new AsyncCallback(ShowResult);
private static bool FinishFlag = false;
static void ShowResult(IAsyncResult ar)
{
// Asynchronous Callback method.
// Obtains the last parameter of the delegate call.
int value = Convert.ToInt32(ar.AsyncState);
// Obtains return value from the delegate call using EndInvoke.
AsyncResult aResult = (AsyncResult)ar;
ComputeFibonacciDel temp = (ComputeFibonacciDel)aResult.AsyncDele
gate;
long result = temp.EndInvoke(ar);
FinishFlag = true;
calComplete("當前狀態代號:" + value.ToString() + " " + "計算后的返回結
果:" + result.ToString());
}
int i = 0;
private void timer1_Tick(object sender, EventArgs e)
{
i += 1;
i = i % 100;
this.progressBar1.Value = i;
}
int j = 0;
int iStep = 1;
ComputeFibonacciDel calcFbnc;
private void button4_Click(object sender, EventArgs e)
{
FinishFlag = false;
//停止進度條的自動滾動.讓進度條根據當前進度顯示
this.timer1.Enabled = false;
this.progressBar1.Value = 0;
try
{
j= Int32.Parse(this.textBox_Num.Text.Trim());
iStep = 100 / j ;
if (j < 1)
return;
}
catch
{
MessageBox.Show("請在文本框內輸入數字字符");
return;
}
//ComputeFibonacciDel,用于捆綁要傳遞給(從線程池中分配的)輔助線程上的
ShowCalcResult 的參數。
//當用戶決定要計算 fbnc 數列 時,事件處理程序將創建此委托的一個實例。
//此工作通過調用 BeginInvoke 在線程池中進行排隊。該委托實際上是由 UI 線程
用于向輔助線程傳遞消息。
calcFbnc = new ComputeFibonacciDel(this.ShowCalcResult );
IAsyncResult aResult= calcFbnc.BeginInvoke(j,callBack , null);
//已在callBack 方法中寫出,此處不再寫此方法.
////Wait for the call to complete
//aResult.AsyncWaitHandle.WaitOne();
//long callResult = calcFbnc.EndInvoke(aResult);
}
bool bClose = false;
private void Form2_FormClosing(object sender, FormClosingEventArgs e)
{
bClose = true;
}
131H .NET Framework 異步調用
.NET Framework 異步調用
.NET Framework 允許您異步調用任何方法。定義與您需要調用的方法具有相同簽名的委托;公共語言運行
庫將自動為該委托定義具有適當簽名的 BeginInvoke 和 EndInvoke 方法。
BeginInvoke 方法用于啟動異步調用。它與您需要異步執行的方法具有相同的參數,只不過還有兩個額外
的參數(將在稍后描述)。BeginInvoke 立即返回,不等待異步調用完成。BeginInvoke 返回 IasyncResult,
可用于監視調用進度。
EndInvoke 方法用于檢索異步調用結果。調用 BeginInvoke 后可隨時調用 EndInvoke 方法;如果異步調
用未完成,EndInvoke 將一直阻塞到異步調用完成。EndInvoke 的參數包括您需要異步執行的方法的 out
和 ref 參數(在 Visual Basic 中為 <Out> ByRef 和 ByRef)以及由 BeginInvoke 返回的 IAsyncResult。
注意 Visual Studio .NET 中的智能感知功能會顯示 BeginInvoke 和 EndInvoke 的參數。如果您沒有使用
Visual Studio 或類似的工具,或者您使用的是 C# 和 Visual Studio .NET,請參見132H異步方法簽名獲取有關
運行庫為這些方法定義的參數的描述。
本主題中的代碼演示了四種使用 BeginInvoke 和 EndInvoke 進行異步調用的常用方法。調用了
BeginInvoke 后,可以:
? 進行某些操作,然后調用 EndInvoke 一直阻塞到調用完成。
? 使用 IAsyncResult.AsyncWaitHandle 獲取 WaitHandle,使用它的 WaitOne 方法將執行一直阻
塞到發出 WaitHandle 信號,然后調用 EndInvoke。
? 輪詢由 BeginInvoke 返回的 IAsyncResult,確定異步調用何時完成,然后調用 EndInvoke。
? 將用于回調方法的委托傳遞給 BeginInvoke。該方法在異步調用完成后在 ThreadPool 線程上執
行,它可以調用 EndInvoke。
警告 始終在異步調用完成后調用 EndInvoke。
測試方法和異步委托
四個示例全部使用同一個長期運行的測試方法 TestMethod。該方法顯示一個表明它已開始處理的控制臺信
息,休眠幾秒鐘,然后結束。TestMethod 有一個 out 參數(在 Visual Basic 中為 <Out> ByRef),它演示
了如何將這些參數添加到 BeginInvoke 和 EndInvoke 的簽名中。您可以用類似的方式處理 ref 參數(在
Visual Basic 中為 ByRef)。
下面的代碼示例顯示 TestMethod 以及代表 TestMethod 的委托;若要使用任一示例,請將示例代碼追加
到這段代碼中。
注意 為了簡化這些示例,TestMethod 在獨立于 Main() 的類中聲明。或者,TestMethod 可以是包含 Main()
的同一類中的 static 方法(在 Visual Basic 中為 Shared)。
using System;
using System.Threading;
public class AsyncDemo {
// The method to be executed asynchronously.
//
public string TestMethod(int callDuration, out int threadId) {
Console.WriteLine("Test method begins.");
Thread.Sleep(callDuration);
threadId = AppDomain.GetCurrentThreadId();
return "MyCallTime was " + callDuration.ToString();
}
}
// The delegate must have the same signature as the method
// you want to call asynchronously.
public delegate string AsyncDelegate(int callDuration, out int threadId);
using System;
using System.Threading;
public class AsyncDemo {
// The method to be executed asynchronously.
//
public string TestMethod(int callDuration, out int threadId) {
Console.WriteLine("Test method begins.");
Thread.Sleep(callDuration);
threadId = AppDomain.GetCurrentThreadId();
return "MyCallTime was " + callDuration.ToString();
}
}
// The delegate must have the same signature as the method
// you want to call asynchronously.
public delegate string AsyncDelegate(int callDuration, out int threadId);
使用 EndInvoke 等待異步調用
異步執行方法的最簡單方式是以 BeginInvoke 開始,對主線程執行一些操作,然后調用 EndInvoke。
EndInvoke 直到異步調用完成后才返回。這種技術非常適合文件或網絡操作,但是由于它阻塞 EndInvoke,
所以不要從用戶界面的服務線程中使用它。
public class AsyncMain {
static void Main(string[] args) {
// The asynchronous method puts the thread id here.
int threadId;
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.
AsyncDelegate dlgt = new AsyncDelegate(ad.TestMethod);
// Initiate the asychronous call.
IAsyncResult ar = dlgt.BeginInvoke(3000,
out threadId, null, null);
Thread.Sleep(0);
Console.WriteLine("Main thread {0} does some work.",
AppDomain.GetCurrentThreadId());
// Call EndInvoke to Wait for the asynchronous call to complete,
// and to retrieve the results.
string ret = dlgt.EndInvoke(out threadId, ar);
Console.WriteLine("The call executed on thread {0}, with return value /"{1}/".", threadId, ret);
}
}
使用 WaitHandle 等待異步調用
等待 WaitHandle 是一項常用的線程同步技術。您可以使用由 BeginInvoke 返回的 IAsyncResult 的
AsyncWaitHandle 屬性來獲取 WaitHandle。異步調用完成時會發出 WaitHandle 信號,而您可以通過調用
它的 WaitOne 等待它。
如果您使用 WaitHandle,則在異步調用完成之后,但在通過調用 EndInvoke 檢索結果之前,可以執行其
他處理。
public class AsyncMain {
static void Main(string[] args) {
// The asynchronous method puts the thread id here.
int threadId;
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.
AsyncDelegate dlgt = new AsyncDelegate(ad.TestMethod);
// Initiate the asychronous call.
IAsyncResult ar = dlgt.BeginInvoke(3000,
out threadId, null, null);
Thread.Sleep(0);
Console.WriteLine("Main thread {0} does some work.",
AppDomain.GetCurrentThreadId());
// Wait for the WaitHandle to become signaled.
ar.AsyncWaitHandle.WaitOne();
// Perform additional processing here.
// Call EndInvoke to retrieve the results.
string ret = dlgt.EndInvoke(out threadId, ar);
Console.WriteLine("The call executed on thread {0}, with return value /"{1}/".", threadId, ret);
}
}
輪詢異步調用完成
您可以使用由 BeginInvoke 返回的 IAsyncResult 的 IsCompleted 屬性來發現異步調用何時完成。從用
戶界面的服務線程中進行異步調用時可以執行此操作。輪詢完成允許用戶界面線程繼續處理用戶輸入。
public class AsyncMain {
static void Main(string[] args) {
// The asynchronous method puts the thread id here.
int threadId;
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.
AsyncDelegate dlgt = new AsyncDelegate(ad.TestMethod);
// Initiate the asychronous call.
IAsyncResult ar = dlgt.BeginInvoke(3000,
out threadId, null, null);
// Poll while simulating work.
while(ar.IsCompleted == false) {
Thread.Sleep(10);
}
// Call EndInvoke to retrieve the results.
string ret = dlgt.EndInvoke(out threadId, ar);
Console.WriteLine("The call executed on thread {0}, with return value /"{1}/".", threadId, ret);
}
}
異步調用完成時執行回調方法
如果啟動異步調用的線程不需要處理調用結果,則可以在調用完成時執行回調方法。回調方法在
ThreadPool 線程上執行。
要使用回調方法,必須將代表該方法的 AsyncCallback 委托傳遞給 BeginInvoke。也可以傳遞包含回調方
法將要使用的信息的對象。例如,可以傳遞啟動調用時曾使用的委托,以便回調方法能夠調用 EndInvoke。
public class AsyncMain {
// Asynchronous method puts the thread id here.
private static int threadId;
static void Main(string[] args) {
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.
AsyncDelegate dlgt = new AsyncDelegate(ad.TestMethod);
// Initiate the asychronous call. Include an AsyncCallback
// delegate representing the callback method, and the data
// needed to call EndInvoke.
IAsyncResult ar = dlgt.BeginInvoke(3000,
out threadId,
new AsyncCallback(CallbackMethod),
dlgt );
Console.WriteLine("Press Enter to close application.");
Console.ReadLine();
}
// Callback method must have the same signature as the
// AsyncCallback delegate.
static void CallbackMethod(IAsyncResult ar) {
// Retrieve the delegate.
AsyncDelegate dlgt = (AsyncDelegate) ar.AsyncState;
// Call EndInvoke to retrieve the results.
string ret = dlgt.EndInvoke(out threadId, ar);
Console.WriteLine("The call executed on thread {0}, with return value /"{1}/".", threadId, ret);
}
}
C#的多線程機制探索
13H
注:本文中出現的代碼均在.net Framework RC3 環境中運行通過
一.多線程的概念
Windows 是一個多任務的系統,如果你使用的是windows 2000 及 其以上版本,你可以通過
任務管理器查看當前系統運行的程序和進程。什么是進程呢?當一個程序開始運行時,它就是一個
進程,進程所指包括運行中的程序和程序 所使用到的內存和系統資源。而一個進程又是由多個線
程所組成的,線程是程序中的一個執行流,每個線程都有自己的專有寄存器(棧指針、程序計數器等),
但代碼區是共享的,即不同的線程可以執行同樣的函數。多線程是指程序中包含多個執行流,即在
一個程序中可以同時運行多個不同的線程來執行不同的任務,也就是說允許單個程序創建多個并行
執行的線程來完成各自的任務。瀏覽器就是一個很好的多線程的例子,在瀏覽器中你可以在下載
JAVA 小應用程序或圖象的同時滾動頁面,在訪問新頁面時,播放動畫和聲音,打印文件等。
多線程的好處在于可以提高CPU 的利用率——任何一個程序員都不希望自己的程序很多時候
沒事可干,在多線程程序中,一個線程必須等待的時候,CPU 可以運行其它的線程而不是等待,這
樣就大大提高了程序的效率。
然而我們也必須認識到線程本身可能影響系統性能的不利方面,以正確使用線程:
? 線程也是程序,所以線程需要占用內存,線程越多占用內存也越多
? 多線程需要協調和管理,所以需要CPU 時間跟蹤線程
? 線程之間對共享資源的訪問會相互影響,必須解決競用共享資源的問題
? 線程太多會導致控制太復雜,最終可能造成很多Bug
基 于以上認識,我們可以一個比喻來加深理解。假設有一個公司,公司里有很多各司其職的
職員,那么我們可以認為這個正常運作的公司就是一個進程,而公司里的職 員就是線程。一個公
司至少得有一個職員吧,同理,一個進程至少包含一個線程。在公司里,你可以一個職員干所有的
事,但是效率很顯然是高不起來的,一個人的 公司也不可能做大;一個程序中也可以只用一個線
程去做事,事實上,一些過時的語言如fortune,basic 都是如此,但是象一個人的公司一樣,效率很
低,如果做大程序,效率更低——事實上現在幾乎沒有單線程的商業軟件。公司的職員越多,老板
就得發越多的薪水給他們,還得耗費大量精力去管理他們,協調他們之間的矛盾和利益;程序也是
如此,線程越多耗費的資源也越多,需要CPU 時間去跟蹤線程,還得解決諸如死鎖,同步等問題。
總之,如果你不想你的公司被稱為“皮包公司”,你就得多幾個員工;如果你不想讓你的程序顯得稚
氣,就在你的程序里引入多線程吧!
本文將對C#編程中的多線程機制進行探討,通過一些實例解決對線程的控制,多線程間通訊
等問題。為了省去創建GUI 那些繁瑣的步驟,更清晰地逼近線程的本質,下面所有的程序都是控制
臺程序,程序最后的Console.ReadLine()是為了使程序中途停下來,以便看清楚執行過程中的輸出。
好了,廢話少說,讓我們來體驗一下多線程的C#吧!
二.操縱一個線程
任何程序在執行時,至少有一個主線程,下面這段小程序可以給讀者一個直觀的印象:
//SystemThread.cs
using System;
using System.Threading;
namespace ThreadTest
{
class RunIt
{
[STAThread]
static void Main(string[] args)
{
Thread.CurrentThread.Name="System Thread";//給當前線程起名為"System
Thread"
Console.WriteLine(Thread.CurrentThread.Name+"'Status:"+Thread.CurrentThread.ThreadState);
Console.ReadLine();
}
}
}
編譯執行后你看到了什么?是的,程序將產生如下輸出:
System Thread's Status:Running
在這里,我們通過Thread 類的靜態屬性CurrentThread 獲取了當前執行的線程,對其Name
屬性賦值“System Thread”,最后還輸出了它的當前狀態(ThreadState)。所謂靜態屬性,就是這
個類所有對象所公有的屬性,不管你創建了多少個這個類的實例,但是類的靜態屬性在內存中只有
一個。很容易理解CurrentThread 為什么是靜態的——雖然有多個線程同時存在,但是在某一個時
刻,CPU 只能執行其中一個。
就像上面程序所演示的,我們通過Thread 類來創建和控制線程。注意到程序的頭部,我們使
用了如下命名空間:
using System;
using System.Threading;
在.net framework class library 中,所有與多線程機制應用相關的類都是放在
System.Threading 命名空間中的。其中提供Thread 類用于創建線程,ThreadPool 類用于管理線
程池等等,此外還提供解決了線程執行安排,死鎖,線程間通訊等實際問題的機制。如果你想在你
的應用程序中使用多線程,就必須包含這個類。Thread 類有幾個至關重要的方法,描述如下:
? Start():啟動線程
? Sleep(int):靜態方法,暫停當前線程指定的毫秒數
? Abort():通常使用該方法來終止一個線程
? Suspend():該方法并不終止未完成的線程,它僅僅掛起線程,以后還可恢復。
? Resume():恢復被Suspend()方法掛起的線程的執行
1
下面我們就動手來創建一個線程,使用Thread 類創建線程時,只需提供線程入口即可。
線程入口使程序知道該讓這個線程干什么事,在C#中,線程入口是通過ThreadStart
代理(delegate)來提供的,你可以把ThreadStart 理解為一個函數指針,指向線程要
執行的函數,當調用Thread.Start()方法后,線程就開始執行ThreadStart 所代表或者說
指向的函數。
打開你的VS.net,新建一個控制臺應用程序(Console Application),下面這些代
碼將讓你體味到完全控制一個線程的無窮樂趣!
//ThreadTest.cs
using System;
using System.Threading;
namespace ThreadTest
{
public class Alpha
{
public void Beta()
{
while (true)
{
Console.WriteLine("Alpha.Beta is running in its own thread.");
}
}
};
public class Simple
{
public static int Main()
{
Console.WriteLine("Thread Start/Stop/Join Sample");
Alpha oAlpha = new Alpha();
file://這里創建一個線程,使之執行Alpha 類的Beta()方法
Thread oThread = new Thread(new ThreadStart(oAlpha.Beta));
oThread.Start();
while (!oThread.IsAlive);
Thread.Sleep(1);
oThread.Abort();
oThread.Join();
Console.WriteLine();
Console.WriteLine("Alpha.Beta has finished");
try
{
Console.WriteLine("Try to restart the Alpha.Beta thread");
oThread.Start();
}
catch (ThreadStateException)
{
Console.Write("ThreadStateException trying to restart
Alpha.Beta. ");
Console.WriteLine("Expected since aborted threads cannot be
restarted.");
Console.ReadLine();
}
return 0;
}
}
}
這段程序包含兩個類Alpha 和Simple,在創建線程oThread 時我們用指向
Alpha.Beta()方法的初始化了ThreadStart 代理(delegate)對象,當我們創建的線程
oThread 調用oThread.Start()方法啟動時,實際上程序運行的是Alpha.Beta()方法:
Alpha oAlpha = new Alpha();
Thread oThread = new Thread(new ThreadStart(oAlpha.Beta));
oThread.Start();
然后在Main()函數的while 循環中,我們使用靜態方法Thread.Sleep()讓主線程停
了1ms,這段時間CPU 轉向執行線程oThread。然后我們試圖用Thread.Abort()方法
終止線程oThread,注意后面的oThread.Join(),Thread.Join()方法使主線程等待,直
到oThread 線程結束。你可以給Thread.Join()方法指定一個int 型的參數作為等待的最
長時間。之后,我們試圖用Thread.Start()方法重新啟動線程oThread,但是顯然Abort()
方法帶來的后果是不可恢復的終止線程,所以最后程序會拋出ThreadStateException
異常。
程序最后得到的結果將如下圖:
在這里我們要注意的是其它線程都是依附于Main()函數所在的線程的,Main()函數是
C#程序的入口,起始線程可以稱之為主線程,如果所有的前臺線程都停止了,那么主
線程可以終止,而所有的后臺線程都將無條件終止。而所有的線程雖然在微觀上是串行
執行的,但是在宏觀上你完全可以認為它們在并行執行。
讀者一定注意到了Thread.ThreadState 這個屬性,這個屬性代表了線程運行時狀
態,在不同的情況下有不同的值,于是我們有時候可以通過對該值的判斷來設計程序流
程。ThreadState 在各種情況下的可能取值如下:
? Aborted:線程已停止
? AbortRequested:線程的Thread.Abort()方法已被調
用,但是線程還未停止
? Background:線程在后臺執行,與屬性
Thread.IsBackground 有關
? Running:線程正在正常運行
? Stopped:線程已經被停止
? StopRequested:線程正在被要求停止
? Suspended:線程已經被掛起(此狀態下,可以通
過調用Resume()方法重新運行)
? SuspendRequested:線程正在要求被掛起,但是未
來得及響應
? Unstarted:未調用Thread.Start()開始線程的運行
? WaitSleepJoin:線程因為調用了Wait(),Sleep()或
Join()等方法處于封鎖狀態
上面提到了Background 狀態表示該線程在后臺運行,那么后臺運行的線程有什么
特別的地方呢?其實后臺線程跟前臺線程只有一個區別,那就是后臺線程不妨礙程序的
終止。一旦一個進程所有的前臺線程都終止后,CLR(通用語言運行環境)將通過調用
任意一個存活中的后臺進程的Abort()方法來徹底終止進程。
當線程之間爭奪CPU 時間時,CPU 按照是線程的優先級給予服務的。在C#應用
程序中,用戶可以設定5 個不同的優先級,由高到低分別是Highest,AboveNormal,
Normal,BelowNormal,Lowest,在創建線程時如果不指定優先級,那么系統默認為
ThreadPriority.Normal。給一個線程指定優先級
,我們可以使用如下代碼:
//設定優先級為最低
myThread.Priority=ThreadPriority.Lowest;
通過設定線程的優先級,我們可以安排一些相對重要的線程優先執行,例如對用戶
的響應等等。
現在我們對怎樣創建和控制一個線程已經有了一個初步的了解,下面我們將深入研
究線程實現中比較典型的的問題,并且探討其解決方法。
三.線程的同步和通訊——生產者和消費者
假 設這樣一種情況,兩個線程同時維護一個隊列,如果一個線程對隊列中添加元
素,而另外一個線程從隊列中取用元素,那么我們稱添加元素的線程為生產者,稱取用
元素的線程為消費者。生產者與消費者問題看起來很簡單,但是卻是多線程應用中一個
必須解決的問題,它涉及到線程之間的同步和通訊問題。
前面說過,每個線程都有自己的資源,但是代碼區是共享的,即每個線程都可以執
行相同的函數。但是多線程環境下,可能帶來的問題就是幾個線程同時執行一個函數,
導致數據的混亂,產生不可預料的結果,因此我們必須避免這種情況的發生。C#提供
了一個關鍵字lock,它可以把一段代碼定義為互斥段(critical section),互斥段在一
個時刻內只允許一個線程進入執行,而其他線程必須等待。在C#中,關鍵字lock 定義
如下:
lock(expression) statement_block
expression 代表你希望跟蹤的對象,通常是對象引用。一般地,如果你想保護一個類的
實例,你可以使用this;如果你希望保護一個靜態變量(如互斥代碼段在一個靜態方法
內部),一般使用類名就可以了。而statement_block 就是互斥段的代碼,這段代碼在
一個時刻內只可能被一個線程執行。
下面是一個使用lock 關鍵字的典型例子,我將在注釋里向大家說明lock 關鍵字的
用法和用途:
//lock.cs
using System;
using System.Threading;
internal class Account
{
int balance;
Random r = new Random();
internal Account(int initial)
{
balance = initial;
}
internal int Withdraw(int amount)
{
if (balance < 0)
{
file://如果balance 小于0 則拋出異常
throw new Exception("Negative Balance");
}
//下面的代碼保證在當前線程修改balance 的值完成之前
//不會有其他線程也執行這段代碼來修改balance 的值
//因此,balance 的值是不可能小于0 的
lock (this)
{
Console.WriteLine("Current Thread:"+Thread.CurrentThread.Name);
file://如果沒有lock 關鍵字的保護,那么可能在執行完if 的條件判斷之后
file://另外一個線程卻執行了balance=balance-amount 修改了balance 的值
file://而這個修改對這個線程是不可見的,所以可能導致這時if 的條件已經不成
立了
file://但是,這個線程卻繼續執行balance=balance-amount,所以導致balance
可能小于0
if (balance >= amount)
{
Thread.Sleep(5);
balance = balance - amount;
return amount;
}
else
{
return 0; // transaction rejected
}
}
}
internal void DoTransactions()
{
for (int i = 0; i < 100; i++)
Withdraw(r.Next(-50, 100));
}
}
internal class Test
{
static internal Thread[] threads = new Thread[10];
public static void Main()
{
Account acc = new Account (0);
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(new ThreadStart(acc.DoTransactions));
threads[i] = t;
}
for (int i = 0; i < 10; i++)
threads[i].Name=i.ToString();
for (int i = 0; i < 10; i++)
threads[i].Start();
Console.ReadLine();
}
}
而多線程公用一個對象時,也會出現和公用代碼類似的問題,這種問題就不應該使
用lock 關鍵字了,這里需要用到System.Threading 中的一個類Monitor,我們可以稱
之為監視器,Monitor 提供了使線程共享資源的方案。
Monitor 類可以鎖定一個對象,一個線程只有得到這把鎖才可以對該對象進行操作。
對象鎖機制保證了在可能引起混亂的情況下一個時刻只有一個線程可以訪問這個對象。
Monitor 必須和一個具體的對象相關聯,但是由于它是一個靜態的類,所以不能使用它
來定義對象,而且它的所有方法都是靜態的,不能使用對象來引用。下面代碼說明了使
用Monitor 鎖定一個對象的情形:
......
Queue oQueue=new Queue();
......
Monitor.Enter(oQueue);
......//現在oQueue 對象只能被當前線程操縱了
Monitor.Exit(oQueue);//釋放鎖
如上所示,當一個線程調用Monitor.Enter()方法鎖定一個對象時,這個對象就歸它
所有了,其它線程想要訪問這個對象,只有等待它使用Monitor.Exit()方法釋放鎖。為了
保證線程最終都能釋放鎖,你可以把Monitor.Exit()方法寫在try-catch-finally 結構中的
finally 代碼塊里。對于任何一個被Monitor 鎖定的對象,內存中都保存著與它相關的一
些信息,其一是現在持有鎖的線程的引用,其二是一個預備隊列,隊列中保存了已經準
備好獲取鎖的線程,其三是一個等待隊列,隊列中保存著當前正在等待這個對象狀態改
變的隊列的引用。當擁有對象鎖的線程準備釋放鎖時,它使用Monitor.Pulse()方法通知
等待隊列中的第一個線程,于是該線程被轉移到預備隊列中,當對象鎖被釋放時,在預
備隊列中的線程可以立即獲得對象鎖。
下面是一個展示如何使用lock 關鍵字和Monitor 類來實現線程的同步和通訊的例
子,也是一個典型的生產者與消費者問題。這個例程中,生產者線程和消費者線程是交
替進行的,生產者寫入一個數,消費者立即讀取并且顯示,我將在注釋中介紹該程序的
精要所在。用到的系統命名空間如下:
using System;
using System.Threading;
首先,我們定義一個被操作的對象的類Cell,在這個類里,有兩個方法:ReadFromCell()
和WriteToCell。消費者線程將調用ReadFromCell()讀取cellContents 的內容并且顯示
出來,生產者進程將調用WriteToCell()方法向cellContents 寫入數據。
public class Cell
{
int cellContents; // Cell 對象里邊的內容
bool readerFlag = false; // 狀態標志,為true 時可以讀取,為false 則正在寫入
public int ReadFromCell( )
{
lock(this) // Lock 關鍵字保證了什么,請大家看前面對lock 的介紹
{
if (!readerFlag)//如果現在不可讀取
{
try
{
file://等待WriteToCell 方法中調用Monitor.Pulse()方法
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Consume: {0}",cellContents);
readerFlag = false; file://重置readerFlag 標志,表示消費行為已經完成
Monitor.Pulse(this); file://通知WriteToCell()方法(該方法在另外一個線程中執
行,等待中)
}
return cellContents;
}
public void WriteToCell(int n)
{
lock(this)
{
if (readerFlag)
{
try
{
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
file://當同步方法(指Monitor 類除Enter 之外的方法)在非同步的代碼區
被調用
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
file://當線程在等待狀態的時候中止
Console.WriteLine(e);
}
}
cellContents = n;
Console.WriteLine("Produce: {0}",cellContents);
readerFlag = true;
Monitor.Pulse(this); file://通知另外一個線程中正在等待的ReadFromCell()方

}
}
}
下面定義生產者CellProd 和消費者類CellCons,它們都只有一個方法
ThreadRun(),以便在Main()函數中提供給線程的ThreadStart 代理對象,作為線程的
入口。
public class CellProd
{
Cell cell; // 被操作的Cell 對象
int quantity = 1; // 生產者生產次數,初始化為1
public CellProd(Cell box, int request)
{
//構造函數
cell = box;
quantity = request;
}
public void ThreadRun( )
{
for(int looper=1; looper<=quantity; looper++)
cell.WriteToCell(looper); file://生產者向操作對象寫入信息
}
}
public class CellCons
{
Cell cell;
int quantity = 1;
public CellCons(Cell box, int request)
{
cell = box;
quantity = request;
}
public void ThreadRun( )
{
int valReturned;
for(int looper=1; looper<=quantity; looper++)
valReturned=cell.ReadFromCell( );//消費者從操作對象中讀取信息
}
}
然后在下面這個類MonitorSample 的Main()函數中我們要做的就是創建兩個線程分別
作為生產者和消費者,使用CellProd.ThreadRun()方法和CellCons.ThreadRun()方法
對同一個Cell 對象進行操作。
public class MonitorSample
{
public static void Main(String[] args)
{
int result = 0; file://一個標志位,如果是0 表示程序沒有出錯,如果是1 表明有錯誤發

Cell cell = new Cell( );
//下面使用cell 初始化CellProd 和CellCons 兩個類,生產和消費次數均為20 次
CellProd prod = new CellProd(cell, 20);
CellCons cons = new CellCons(cell, 20);
Thread producer = new Thread(new ThreadStart(prod.ThreadRun));
Thread consumer = new Thread(new ThreadStart(cons.ThreadRun));
//生產者線程和消費者線程都已經被創建,但是沒有開始執行
try
{
producer.Start( );
consumer.Start( );
producer.Join( );
consumer.Join( );
Console.ReadLine();
}
catch (ThreadStateException e)
{
file://當線程因為所處狀態的原因而不能執行被請求的操作
Console.WriteLine(e);
result = 1;
}
catch (ThreadInterruptedException e)
{
file://當線程在等待狀態的時候中止
Console.WriteLine(e);
result = 1;
}
//盡管Main()函數沒有返回值,但下面這條語句可以向父進程返回執行結果
Environment.ExitCode = result;
}
}
大家可以看到,在上面的例程中,同步是通過等待Monitor.Pulse()來完成的。首先生產
者生產了一個值,而同一時刻消費者處于等待狀態,直到收到生產者的“脈沖(Pulse)”通
知它生產已經完成,此后消費者進入消費狀態,而生產者開始等待消費者完成操作后將
調用Monitor.Pulese()發出的“脈沖”。它的執行結果很簡單:
Produce: 1
Consume: 1
Produce: 2
Consume: 2
Produce: 3
Consume: 3
...
...
Produce: 20
Consume: 20
事實上,這個簡單的例子已經幫助我們解決了多線程應用程序中可能出現的大問
題,只要領悟了解決線程間沖突的基本方法,很容易把它應用到比較復雜的程序中去。
四、線程池和定時器——多線程的自動管理
在多線程的程序中,經常會出現兩種情況。一種情況下,應用程序中的線程把大部
分的時間花費在等待狀態,等待某個事件發生,然后才能給予響應;而另外一種情況則
是線程平常都處于休眠狀態,只是周期性地被喚醒。在.net framework 里邊,我們使用
ThreadPool 來對付第一種情況,使用Timer 來對付第二種情況。
ThreadPool 類提供一個由系統維護的線程池——可以看作一個線程的容器,該容
器需要Windows 2000 以上版本的系統支持,因為其中某些方法調用了只有高版本的
Windows 才有的API 函數。你可以使用ThreadPool.QueueUserWorkItem()方法將線程
安放在線程池里,該方法的原型如下:
//將一個線程放進線程池,該線程的Start()方法將調用WaitCallback 代理對象代表
的函數
public static bool QueueUserWorkItem(WaitCallback);
//重載的方法如下,參數object 將傳遞給WaitCallback 所代表的方法
public static bool QueueUserWorkItem(WaitCallback, object);
要注意的是,ThreadPool 類也是一個靜態類,你不能也不必要生成它的對象,而
且一旦使用該方法在線程池中添加了一個項目,那么該項目將是沒有辦法取消的。在這
里你無需自己建立線程,只需把你要做的工作寫成函數,然后作為參數傳遞給
ThreadPool.QueueUserWorkItem()方法就行了,傳遞的方法就是依靠WaitCallback 代
理對象,而線程的建立、管理、運行等等工作都是由系統自動完成的,你無須考慮那些
復雜的細節問題,線程池的優點也就在這里體現出來了,就好像你是公司老板——只需
要安排工作,而不必親自動手。
下面的例程演示了ThreadPool 的用法。首先程序創建了一個ManualResetEvent 對象,
該對象就像一個信號燈,可以利用它的信號來通知其它線程,本例中當線程池中所有線
程工作都完成以后,ManualResetEvent 的對象將被設置為有信號,從而通知主線程繼
續運行。它有幾個重要的方法:Reset(),Set(),WaitOne()。初始化該對象時,用戶可
以指定其默認的狀態(有信號/無信號),在初始化以后,該對象將保持原來的狀態不
變直到它的Reset()或者Set()方法被調用,Reset()方法將其設置為無信號狀態,Set()
方法將其設置為有信號狀態。WaitOne()方法使當前線程掛起直到ManualResetEvent
對象處于有信號狀態,此時該線程將被激活。然后,程序將向線程池中添加工作項,這
些以函數形式提供的工作項被系統用來初始化自動建立的線程。當所有的線程都運行完
了以后,ManualResetEvent.Set()方法被調用,因為調用了
ManualResetEvent.WaitOne()方法而處在等待狀態的主線程將接收到這個信號,于是
它接著往下執行,完成后邊的工作。
using System;
using System.Collections;
using System.Threading;
//這是用來保存信息的數據結構,將作為參數被傳遞
public class SomeState
{
public int Cookie;
public SomeState(int iCookie)
{
Cookie = iCookie;
}
}
public class Alpha
{
public Hashtable HashCount;
public ManualResetEvent eventX;
public static int iCount = 0;
public static int iMaxCount = 0;
public Alpha(int MaxCount)
{
HashCount = new Hashtable(MaxCount);
iMaxCount = MaxCount;
}
file://線程池里的線程將調用Beta()方法
public void Beta(Object state)
{
//輸出當前線程的hash 編碼值和Cookie 的值
Console.WriteLine(" {0} {1} :", Thread.CurrentThread.GetHashCode(),
((SomeState)state).Cookie);
Console.WriteLine("HashCount.Count=={0},
Thread.CurrentThread.GetHashCode()=={1}", HashCount.Count,
Thread.CurrentThread.GetHashCode());
lock (HashCount)
{
file://如果當前的Hash 表中沒有當前線程的Hash 值,則添加之
if (!HashCount.ContainsKey(Thread.CurrentThread.GetHashCode()))
HashCount.Add (Thread.CurrentThread.GetHashCode(), 0);
HashCount[Thread.CurrentThread.GetHashCode()] =
((int)HashCount[Thread.CurrentThread.GetHashCode()])+1;
}
int iX = 2000;
Thread.Sleep(iX);
//Interlocked.Increment()操作是一個原子操作,具體請看下面說明
Interlocked.Increment(ref iCount);
if (iCount == iMaxCount)
{
Console.WriteLine();
Console.WriteLine("Setting eventX ");
eventX.Set();
}
}
}
public class SimplePool
{
public static int Main(string[] args)
{
Console.WriteLine("Thread Pool Sample:");
bool W2K = false;
int MaxCount = 10;//允許線程池中運行最多10 個線程
//新建ManualResetEvent 對象并且初始化為無信號狀態
ManualResetEvent eventX = new ManualResetEvent(false);
Console.WriteLine("Queuing {0} items to Thread Pool", MaxCount);
Alpha oAlpha = new Alpha(MaxCount); file://創建工作項
//注意初始化oAlpha 對象的eventX 屬性
oAlpha.eventX = eventX;
Console.WriteLine("Queue to Thread Pool 0");
try
{
file://將工作項裝入線程池
file://這里要用到Windows 2000 以上版本才有的API,所以可能出現
NotSupportException 異常
ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta),
new SomeState(0));
W2K = true;
}
catch (NotSupportedException)
{
Console.WriteLine("These API's may fail when called on a non-Windows
2000 system.");
W2K = false;
}
if (W2K)//如果當前系統支持ThreadPool 的方法.
{
for (int iItem=1;iItem < MaxCount;iItem++)
{
//插入隊列元素
Console.WriteLine("Queue to Thread Pool {0}", iItem);
ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta),new
SomeState(iItem));
}
Console.WriteLine("Waiting for Thread Pool to drain");
file://等待事件的完成,即線程調用ManualResetEvent.Set()方法
eventX.WaitOne(Timeout.Infinite,true);
file://WaitOne()方法使調用它的線程等待直到eventX.Set()方法被調用
Console.WriteLine("Thread Pool has been drained (Event fired)");
Console.WriteLine();
Console.WriteLine("Load across threads");
foreach(object o in oAlpha.HashCount.Keys)
Console.WriteLine("{0} {1}", o, oAlpha.HashCount[o]);
}
Console.ReadLine();
return 0;
}
}
程序中有些小地方應該引起我們的注意。SomeState 類是一個保存信息的數據結
構,在上面的程序中,它作為參數被傳遞給每一個線程,你很容易就能理解這個,因為
你需要把一些有用的信息封裝起來提供給線程,而這種方式是非常有效的。程序出現的
InterLocked 類也是專為多線程程序而存在的,它提供了一些有用的原子操作,所謂原
子操作就是在多線程程序中,如果這個線程調用這個操作修改一個變量,那么其他線程
就不能修改這個變量了,這跟lock 關鍵字在本質上是一樣的。
我們應該徹底地分析上面的程序,把握住線程池的本質,理解它存在的意義是什么,
這樣我們才能得心應手地使用它。下面是該程序的輸出結果:
Thread Pool Sample:
Queuing 10 items to Thread Pool
Queue to Thread Pool 0
Queue to Thread Pool 1
...
...
Queue to Thread Pool 9
Waiting for Thread Pool to drain
98 0 :
HashCount.Count==0, Thread.CurrentThread.GetHashCode()==98
100 1 :
HashCount.Count==1, Thread.CurrentThread.GetHashCode()==100
98 2 :
...
...
Setting eventX
Thread Pool has been drained (Event fired)
Load across threads
101 2
100 3
98 4
102 1
與ThreadPool 類不同,Timer 類的作用是設置一個定時器,定時執行用戶指定的
函數,而這個函數的傳遞是靠另外一個代理對象TimerCallback,它必須在創建Timer
對象時就指定,并且不能更改。定時器啟動后,系統將自動建立一個新的線程,并且在
這個線程里執行用戶指定的函數。下面的語句初始化了一個Timer 對象:
Timer timer = new Timer(timerDelegate, s,1000, 1000);
第一個參數指定了TimerCallback 代理對象;第二個參數的意義跟上面提到的
WaitCallback 代理對象的一樣,作為一個傳遞數據的對象傳遞給要調用的方法;第三個
參數是延遲時間——計時開始的時刻距現在的時間,單位是毫秒;第四個參數是定時器
的時間間隔——計時開始以后,每隔這么長的一段時間,TimerCallback 所代表的方法
將被調用一次,單位也是毫秒。這句話的意思就是將定時器的延遲時間和時間間隔都設
為1 秒鐘。
定時器的設置是可以改變的,只要調用Timer.Change()方法,這是一個參數類型重
載的方法,一般使用的原型如下:
public bool Change(long, long);
下面這段代碼將前邊設置的定時器修改了一下:
timer.Change(10000,2000);
很顯然,定時器timer 的時間間隔被重新設置為2 秒,停止計時10 秒后生效。
下面這段程序演示了Timer 類的用法。
using System;
using System.Threading;
class TimerExampleState
{
public int counter = 0;
public Timer tmr;
}
class App
{
public static void Main()
{
TimerExampleState s = new TimerExampleState();
//創建代理對象TimerCallback,該代理將被定時調用
TimerCallback timerDelegate = new TimerCallback(CheckStatus);
//創建一個時間間隔為1s 的定時器
Timer timer = new Timer(timerDelegate, s,1000, 1000);
s.tmr = timer;
//主線程停下來等待Timer 對象的終止
while(s.tmr != null)
Thread.Sleep(0);
Console.WriteLine("Timer example done.");
Console.ReadLine();
}
file://下面是被定時調用的方法
static void CheckStatus(Object state)
{
TimerExampleState s =(TimerExampleState)state;
s.counter++;
Console.WriteLine("{0} Checking Status {1}.",DateTime.Now.TimeOfDay,
s.counter);
if(s.counter == 5)
{
file://使用Change 方法改變了時間間隔
(s.tmr).Change(10000,2000);
Console.WriteLine("changed...");
}
if(s.counter == 10)
{
Console.WriteLine("disposing of timer...");
s.tmr.Dispose();
s.tmr = null;
}
}
}
程序首先創建了一個定時器,它將在創建1 秒之后開始每隔1 秒調用一次
CheckStatus()方法,當調用5 次以后,在CheckStatus()方法中修改了時間間隔為2 秒,
并且指定在10 秒后重新開始。當計數達到10 次,調用Timer.Dispose()方法刪除了timer
對象,主線程于是跳出循環,終止程序。程序執行的結果如下:
上面就是對ThreadPool 和Timer 兩個類的簡單介紹,充分利用系統提供的功能,
可以為我們省去很多時間和精力——特別是對很容易出錯的多線程程序。同時我們也可
以看到.net Framework 強大的內置對象,這些將對我們的編程帶來莫大的方便。
、互斥對象——更加靈活的同步方式
有 時候你會覺得上面介紹的方法好像不夠用,對,我們解決了代碼和資源的同步
問題,解決了多線程自動化管理和定時觸發的問題,但是如何控制多個線程相互之間的
聯系呢?例如我要到餐廳吃飯,在吃飯之前我先得等待廚師把飯菜做好,之后我開始吃
飯,吃完我還得付款,付款方式可以是現金,也可以是信用卡,付款之后我才 能離開。
分析一下這個過程,我吃飯可以看作是主線程,廚師做飯又是一個線程,服務員用信用
卡收款和收現金可以看作另外兩個線程,大家可以很清楚地看到其中 的關系——我吃
飯必須等待廚師做飯,然后等待兩個收款線程之中任意一個的完成,然后我吃飯這個線
程可以執行離開這個步驟,于是我吃飯才算結束了。事實上,現實中有著比這更復雜的
聯系,我們怎樣才能很好地控制它們而不產生沖突和重復呢?
這種情況下,我們需要用到互斥對象,即System.Threading 命名空間中的Mutex
類。大家一定坐過出租車吧,事實上我們可以把Mutex 看作一個出租車,那么乘客就是
線程了,乘客首先得等車,然后上車,最后下車,當一個乘客在車上時,其他乘客就只
有等他下車以后才可以上車。而線程與Mutex 對象的關系也正是如此,線程使用
Mutex.WaitOne()方法等待Mutex 對象被釋放,如果它等待的Mutex 對象被釋放了,它
就自動擁有這個對象,直到它調用Mutex.ReleaseMutex()方法釋放這個對象,而在此
期間,其他想要獲取這個Mutex 對象的線程都只有等待。
下面這個例子使用了Mutex 對象來同步四個線程,主線程等待四個線程的結束,而
這四個線程的運行又是與兩個Mutex 對象相關聯的。其中還用到AutoResetEvent 類的
對象,如同上面提到的ManualResetEvent 對象一樣,大家可以把它簡單地理解為一個
信號燈,使用AutoResetEvent.Set()方法可以設置它為有信號狀態,而使用
AutoResetEvent.Reset()方法把它設置為無信號狀態。這里用它的有信號狀態來表示一
個線程的結束。
// Mutex.cs
using System;
using System.Threading;
public class MutexSample
{
static Mutex gM1;
static Mutex gM2;
const int ITERS = 100;
static AutoResetEvent Event1 = new AutoResetEvent(false);
static AutoResetEvent Event2 = new AutoResetEvent(false);
static AutoResetEvent Event3 = new AutoResetEvent(false);
static AutoResetEvent Event4 = new AutoResetEvent(false);
public static void Main(String[] args)
{
Console.WriteLine("Mutex Sample ...");
//創建一個Mutex 對象,并且命名為MyMutex
gM1 = new Mutex(true,"MyMutex");
//創建一個未命名的Mutex 對象.
gM2 = new Mutex(true);
Console.WriteLine(" - Main Owns gM1 and gM2");
AutoResetEvent[] evs = new AutoResetEvent[4];
evs[0] = Event1; file://為后面的線程t1,t2,t3,t4 定義AutoResetEvent 對象
evs[1] = Event2;
evs[2] = Event3;
evs[3] = Event4;
MutexSample tm = new MutexSample( );
Thread t1 = new Thread(new ThreadStart(tm.t1Start));
Thread t2 = new Thread(new ThreadStart(tm.t2Start));
Thread t3 = new Thread(new ThreadStart(tm.t3Start));
Thread t4 = new Thread(new ThreadStart(tm.t4Start));
t1.Start( );// 使用Mutex.WaitAll()方法等待一個Mutex 數組中的對象全部被釋

t2.Start( );// 使用Mutex.WaitOne()方法等待gM1 的釋放
t3.Start( );// 使用Mutex.WaitAny()方法等待一個Mutex 數組中任意一個對象
被釋放
t4.Start( );// 使用Mutex.WaitOne()方法等待gM2 的釋放
Thread.Sleep(2000);
Console.WriteLine(" - Main releases gM1");
gM1.ReleaseMutex( ); file://線程t2,t3 結束條件滿足
Thread.Sleep(1000);
Console.WriteLine(" - Main releases gM2");
gM2.ReleaseMutex( ); file://線程t1,t4 結束條件滿足
//等待所有四個線程結束
WaitHandle.WaitAll(evs);
Console.WriteLine("... Mutex Sample");
Console.ReadLine();
}
public void t1Start( )
{
Console.WriteLine("t1Start started, Mutex.WaitAll(Mutex[])");
Mutex[] gMs = new Mutex[2];
gMs[0] = gM1;//創建一個Mutex 數組作為Mutex.WaitAll()方法的參數
gMs[1] = gM2;
Mutex.WaitAll(gMs);//等待gM1 和gM2 都被釋放
Thread.Sleep(2000);
Console.WriteLine("t1Start finished, Mutex.WaitAll(Mutex[]) satisfied");
Event1.Set( ); file://線程結束,將Event1 設置為有信號狀態
}
public void t2Start( )
{
Console.WriteLine("t2Start started, gM1.WaitOne( )");
gM1.WaitOne( );//等待gM1 的釋放
Console.WriteLine("t2Start finished, gM1.WaitOne( ) satisfied");
Event2.Set( );//線程結束,將Event2 設置為有信號狀態
}
public void t3Start( )
{
Console.WriteLine("t3Start started, Mutex.WaitAny(Mutex[])");
Mutex[] gMs = new Mutex[2];
gMs[0] = gM1;//創建一個Mutex 數組作為Mutex.WaitAny()方法的參數
gMs[1] = gM2;
Mutex.WaitAny(gMs);//等待數組中任意一個Mutex 對象被釋放
Console.WriteLine("t3Start finished, Mutex.WaitAny(Mutex[])");
Event3.Set( );//線程結束,將Event3 設置為有信號狀態
}
public void t4Start( )
{
Console.WriteLine("t4Start started, gM2.WaitOne( )");
gM2.WaitOne( );//等待gM2 被釋放
Console.WriteLine("t4Start finished, gM2.WaitOne( )");
Event4.Set( );//線程結束,將Event4 設置為有信號狀態
}
}
下面是該程序的執行結果:
從執行結果可以很清楚地看到,線程t2,t3 的運行是以gM1 的釋放為條件的,而t4
在gM2 釋放后開始執行,t1 則在gM1 和gM2 都被釋放了之后才執行。Main()函數最后,
使用WaitHandle 等待所有的AutoResetEvent 對象的信號,這些對象的信號代表相應
線程的結束。
六、小結
多線程程序設計是一個龐大的主題,而本文試圖在.net Framework 環境下,使用最
新的C#語言來描述多線程程序的概貌。希望本文能有助于大家理解線程這種概念,理
解多線程的用途,理解它的C#實現方法,理解線程將為我們帶來的好處和麻煩。C#是
一種新的語言,因此它的線程機制也有許多獨特的地方,希望大家能通過本文清楚地看
到這些,從而可以對線程進行更深入的理解和探索。
134H打造迅速響應的用戶界面
背景
最近抽時間開發了一個生成SQL腳本和執行腳本的135H小工具,基本已經完成,但由于生成腳
本和執行腳本相對而言是比較耗時的操作,所以原先的單線程模式會短暫的凍結用戶界面,
由此,為了更好的用戶體驗,針對這兩個耗時的操作引進了多線程模式。我的目標是給這兩
個操作添加對應的進度條(與用戶界面處在不同的Form),顯示目前的進度情況,及時把腳
本的執行情況反饋給用戶。下面,我就依托這個小小的背景,談一下自己的見解吧。
需要做的工作
首先,需要做的事情是,搞清楚用戶界面、進度條以及耗時的操作之間的關系,可以用UML
的序列圖表示,如下圖所示:
從圖中已經可以清晰地看到它們三者之間的關系,不過我還是要簡單的敘述一下:
1) 用戶界面構造并顯示進度條。
2) 用戶界面異步調用耗時操作。
3) 耗時操作需要及時地把執行情況反饋給用戶界面。
4) 用戶界面把目前的執行情況通知給進度條。
5) 耗時操作完成時,通知用戶界面。
6) 用戶界面銷毀進度條,提示任務執行完畢。
明確了做什么之后,下一步就可以思考有哪些方法可以幫助我們解決這個問題了。
有哪些方法
不管采取哪些方法,你都需要思考如下幾個問題:
1) 用戶界面和耗時操作如何異步調用?
2) 耗時操作如果需要參數,那么如何傳遞參數?
3) 耗時操作如何把當前的執行情況發送給用戶界面?
4) 如果耗時操作有返回值(分為每一個小的耗時操作的返回值和整個耗時操作的返回
值),那么如何把執行的結果返回給用戶界面?
5) 用戶界面如何把執行的情況反饋給進度條?
如果這些問題你都已經考慮清楚了,那么這里要解決的問題也就不再是問題了。讓我們逐個
解答吧。
用戶界面和耗時操作如何異步調用?
針對這個問題,我想解決辦法有兩種,一是自己利用多線程的相關類構建工作者線程,把耗
時操作分配給工作者線程,然后在UI 線程里開啟工作者線程。第二種方法,使用異步執行
委托,這是在UI 線程里處理此類問題所特有的方法,使用此方法免去了很多代碼,簡單好
用。
耗時操作如果需要參數,那么如何傳遞參數?
這個問題的答案就跟你上面選擇的方式有關系了,如果你采用的是自己構建多線程,并且需
要傳遞給工作者線程一些參數,你可以采取的辦法有:

編程資料 -C# 多線程

版權所有 IT知識庫 CopyRight ? 2009-2015 IT知識庫 IT610.com , All Rights Reserved. 京ICP備09083238號
广东25选5开奖结果