C#委托—订阅者超时的处理
订阅者其实有两种来影响发布者:
1.异常(上节已讲)
2.超时
所谓超时指方法的执行时间超过了指定值.
它和异常的区别在与:
异常会影响事件的正常触发和程序的正常运行,而超时不会,它仅仅使事件触发后需要很长的时间才能结束,在依次执行订阅者的方法这段时间内,客户端程序会被中断什么都不做。因为当执行订阅者方法时,当前线程会转向执行方法的代码,调用方法的客户端会被中断,只有当方法执行完毕并返回控制权时,才会回到客户端,继续向下执行。
例子:
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ConsoleApplication5
{
class Delegater1
{
static void Main(string[] args)
{
Publisher pub = new Publisher();
Subscriber1 sub1 = new Subscriber1();
Subscriber2 sub2 = new Subscriber2();
Subscriber3 sub3 = new Subscriber3();
pub.myEvent += new EventHandler(sub1.OnEvent);
pub.myEvent += new EventHandler(sub2.OnEvent);
pub.myEvent += new EventHandler(sub3.OnEvent);
pub.InvokeMyEvent(EventArgs.Empty); // 触发事件
Console.WriteLine(" Control back to client!"); // 返回控制权
Console.ReadLine();
}
// 触发某个事件,以列表形式返回所有方法的返回值
public static object[] FireEvent(Delegate del, params object[] args)
{
List<object> objList = new List<object>();
if (del != null)
{
Delegate[] delArray = del.GetInvocationList();
foreach (Delegate method in delArray)
{
try
{
// 使用DynamicInvoke方法触发事件
object obj = method.DynamicInvoke(args);
if (obj != null)
objList.Add(obj);
}
catch
{
}
}
}
return objList.ToArray();
}
}
public class Publisher
{
public event EventHandler myEvent;
public void InvokeMyEvent(EventArgs e)
{
Console.WriteLine("cilent begin");
EventHandler handler = myEvent;
if (handler != null)
Delegater1.FireEvent(handler,this,EventArgs.Empty);;
}
}
public class Subscriber1
{
public void OnEvent(object sender, EventArgs e)
{
Thread.Sleep(3000);
Console.WriteLine("waited for 3 seconds,subscriber1 invoked");
}
}
public class Subscriber2
{
public void OnEvent(object sender, EventArgs e)
{
Console.WriteLine("Subscriber2 immediately Invoked");
}
}
public class Subscriber3
{
public void OnEvent(object sender, EventArgs e)
{
Thread.Sleep(2000);
Console.WriteLine("waited for 2 seconds,subscriber1 invoked");
}
}
}
结果:
Client begin
Waited for 3 seconds, subscriber1 invoked
Subscriber2 immediately Invoked
Waited for 2 seconds, subscriber2 invoked
Control back to client!
程序执行过程解析:
- 程序调用InvokeMyEvent方法后,触发事件,线程由客户端交给执行目标的方法.
- 执行sub1.OnEvent方法(需要3秒)
- 执行sub2.OnEvent方法(立即)
- 执行sub3.OnEvent方法(需要2秒)
- 目标方法执行完毕,线程交给客户端
但是一般情况下,我们不希望是这样的,因为发布者和订阅者应该是完全的松耦合,发布者不关心谁订阅了它,不关心订阅者的方法有什么返回值,不关心订阅者会不会抛出异常,当然不关心订阅者需要多长时间才能完成订阅的方法,它只要在事件发生的时候告知订阅者事件已经发生并将相关参数传给订阅者就可以了。然后它就应该执行它自己后面的方法,即在上面程序中执行1后,直接执行5。而订阅者不管失败或是超时都不影响到发布者。但上面例子中发布者却不得不等待订阅者的方法执行完毕才能继续运行。
下面我们来解决这个问题,首先委托的定义会生成继承自MulticastDelegate的完整的类。其中包括Invoke()、BeginInvoke()和EndInvoke()方法。
其中:
Invoke(): 当直接调用委托的时候,就是调用了它,它会中断客户端,然后再客户端线程上执行所有订阅者方法(在完成前客户端无法执行后续代码),最后将控制权返回给客户端。
BeginInvoke()和EndInvoke()方法:在.net异步执行方法通常配对出现,并且以begin和end做方法名开头。当调用BeginInvoke()时,客户端会从线程池中抓一个闲置线程,然后交由这个线程去执行订阅者的方法,而客户端继续执行下面的代码。使用的时候需要注意以下几点:
- 在委托类型上调用BeginInvoke()时,此委托对象只能包含一个目标方法,所以对于多个订阅者注册的情况,必须使用GetInvocationList()获得所有委托对象,然后遍历它们,分别在其上调用BeginInvoke()方法。如果直接在委托上调用BeginInvoke(),会抛出异常,提示"委托只能包含一个目标方法"。
- 如果订阅者的方法抛出异常,.NET会捕捉到它,但是只有在调用EndInvoke()的时候,才会将异常重新抛出。而在本例中,我们不使用EndInvoke()(因为我们不关心订阅者的执行情况),所以我们无需处理异常,因为即使抛出异常,也是在另一个线程上,不会影响到客户端线程(客户端甚至不知道订阅者发生了异常,这有时是好事有时是坏事)。
- BeginInvoke()方法属于委托定义所生成的类,它既不属于MulticastDelegate也不属于Delegate基类,所以无法继续使用可重用的FireEvent()方法,我们需要进行一个向下转换,来获取到实际的委托类型。
异步调用委托的例子:
{
static void Main(string[] args)
{
Publisher pub = new Publisher();
Subscriber1 sub1 = new Subscriber1();
Subscriber2 sub2 = new Subscriber2();
Subscriber3 sub3 = new Subscriber3();
pub.myEvent += new EventHandler(sub1.OnEvent);
pub.myEvent += new EventHandler(sub2.OnEvent);
pub.myEvent += new EventHandler(sub3.OnEvent);
pub.InvokeMyEvent(EventArgs.Empty); // 触发事件
Console.WriteLine(" Control back to client!"); // 返回控制权
Console.ReadKey(); // 暂停客户程序,提供充分的时间供订阅者完成方法
}
}
public class Publisher
{
public event EventHandler myEvent;
public void InvokeMyEvent(EventArgs e)
{
Console.WriteLine("cilent begin");
EventHandler handler = myEvent;
if (handler != null)
{
Delegate[] delarr = handler.GetInvocationList();
foreach(Delegate del in delarr)
{
EventHandler er = (EventHandler)del;
er.BeginInvoke(null, EventArgs.Empty, null, null);
}
}
}
}
public class Subscriber1
{
public void OnEvent(object sender, EventArgs e)
{
Thread.Sleep(3000);
Console.WriteLine("waited for 3 seconds,subscriber1 invoked");
}
}
public class Subscriber2
{
public void OnEvent(object sender, EventArgs e)
{
Console.WriteLine("Subscriber2 immediately Invoked");
}
}
public class Subscriber3
{
public void OnEvent(object sender, EventArgs e)
{
Thread.Sleep(2000);
Console.WriteLine("waited for 2 seconds,subscriber1 invoked");
}
}
结果:
Client begin
Control back to client!
Subscriber2 immediately Invoked
Waited for 2 seconds, subscriber2 invoked
Waited for 3 seconds, subscriber1 invoked
注意:
我们需要在客户端程序中调用Console.ReadKey()方法来暂停客户端,以提供足够的时间来让异步方法去执行完代码,不然的话客户端的程序到此处便会运行结束,程序会退出,不会看到任何订阅者方法的输出,因为它们根本没来得及执行完毕。原因是这样的:客户端所在的线程我们通常称为主线程,而执行订阅者方法的线程来自线程池,属于后台线程(Background Thread),当主线程结束时,不论后台线程有没有结束,都会退出程序。
注意到尽管我们先注册了subscriber1,但因为它执行所用的时间较长,所以最后一个执行完,因为三个订阅者是并行执行的,所以三个订阅者执行总时间等于最长的那个时间,而不是三个执行时间之和。
如同前面所提到的,尽管subscriber2抛出了异常,我们也没有针对异常进行处理,但是客户程序并没有察觉到,程序也没有因此而中断。