编程资料 -C# 多线程

本文介绍了C#中实现多线程编程的实例,包括单个写入/多个阅读线程同步问题的解决方案,详细阐述了如何使用`System.Threading.ReaderWriterLock`类来确保并发访问资源时的线程安全。此外,文章还展示了如何通过`ThreadPool`和`Timer`进行多线程管理和定时任务执行,并讨论了互斥对象`Mutex`在更复杂同步场景中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

编程资料 - 多线程
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.youkuaiyun.com
# 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
代码下不了啊,能不能给我发一份
gaofan628@yahoo.com.cn
谢谢先
# 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.youkuaiyun.com
# RE: .NET 事件模型教程(二)
2005-5-11 14:37 | GAOFAN
代码下不了啊,谁有能否给我一份,感激不尽。。
gaofan628@yahoo.com.cn
# 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.youkuaiyun.com
# 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
joslat@gmail.com.
Click here

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值