订阅者方法超时的处理
前言
订阅者除了可以通过异常的方式来影响发布者以外,还能通过另一种方式:超时.一般所说的超时,指的是方法的执行超过了某个指定的事件,而这里将含义扩展了一下,凡是方法执行的时间比较长,就可以认为它是超时了.这个”比较长”是一个比较模糊的概念,2秒,3秒,5秒都可以视为超时.超时和异常的区别就是超时不会影响事件正确的触发和程序的正常运行,却会导致事件触发后需要很长时间才能结束.在依次执行订阅者的方法这段期间内,客户端程序会被中断,什么也不能做.因为当执行订阅者方法时(通过委托,相当于依次调用所有注册了的方法),当前线程会转去执行方法中的代码,调用方法的客户端会被中断,只有当方法执行完毕并返回时,控制权才会回到客户端,从而继续执行下面的代码.案例如下:
namespace 订阅者方法超时的处理1
{
class Program
{
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.DoSomething();
Console.WriteLine("Control back to client!");
}
//触发某个事件,以列表形式返回所有方法的返回值
internal 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 (Exception)
{
throw;
}
}
}
return objList.ToArray();
}
}
public class Publisher
{
public event EventHandler MyEvent;
public void DoSomething()
{
Console.WriteLine("DoSomething invoked");
Program.FireEvent(MyEvent,this,EventArgs.Empty);
}
}
public class Subscriber1
{
public void OnEvent(object sender, EventArgs e)
{
Thread.Sleep(TimeSpan.FromSeconds(3));
Console.WriteLine("Waited for 3seconds,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(TimeSpan.FromSeconds(2));
Console.WriteLine("Waited for 2 deconds,subscriber2 invoked!");
}
}
}
在这段代码中,使用Thread.Sleep()静态方法模拟了方法超时的情况.其中Subscriber1.OnEvent()需要3秒完成,Subscriber2.OnEvent()立即执行,Subscriber3.OnEvent需要两秒完成.这段代码完全可以正常输出,也没有异常情况.
但是这段代码在调用方法DoSomething()打印了”DoSomething invoked”之后,触发了事件,随后必须等订阅者的三个方法全部执行完毕,也就是大概五秒的时间,才能继续执行下面的语句,也就是打印”Control back to client”.但是前面说过,在大多数情况下,尤其是远程调用的时候,发布者和订阅者应该是完全松耦合,发布者不关心谁谁订阅了它,不关心订阅者的方法有啥返回值,不关心订阅者会不会抛出异常,当然也不关心订阅者需要多长时间才能弄完成订阅的方法,它只要在事件发生的那一瞬间告诉订阅者事件已经发生并将相关参数传给订阅者就OK了.然后它就应该继续执行他后面的动作,在本例中就是打印”Control back to client!”.而订阅者不管失败或超时都不应该影响到发布者,但在上例中,发布者却不得不等待订阅者的方法执行完毕才能继续运行.这是不科学的!
让我们看看如何解决呢?委托的定义会生成继承自MulticastDelegate的完整的类,其中包含Invoke(),BeginInvoke()和EndInvoke()方法.当直接调用委托时,实际上是调用了Invoke()方法,它会中断调用他的客户端,然后在客户端线程上执行所有订阅者的方法(客户端无法继续执行后面代码),最后将控制权返回客户端.注意BeginInvoke(),EndInvoke()方法没在.NET中,异步执行的方法通常都会配对出现,并且以Begin和End作为方法的开头(最常见的可能就是Stream类的BeginRead()和EndRead()方法了).它们用于方法的异步执行,即在调用BeginInvoke()之后,客户端从线程池中抓取一个闲置线程,然后交由这个县城去执行订阅者的方法,而客户端线程则可以继续执行后面的代码.
BeginInvoke()接受”动态”的参数个数和类型,为啥说是”动态”的呢?因为他的参数是在编译时根据委托的定义动态生成的,其中前面参数的个数和类型与委托定义中接受的参数个数和类型相同,最后两个参数分别是AsyncCallback和object类型.这些东西读一遍就可以了,网上有很多.现在,仅需要知道对这两个参数传入null就可以了.还有几点需要注意:
1.在委托类型上调用BeginInvoke()时,此委托对象只能包含一个目标方法,所以对于多个订阅者注册的情况,必须使用GetInvocationList()获得所有委托对象,然后遍历他们,分别在其上调用BeginInvoke()方法.如果直接在委托上调用BeingInvoke(),会抛出异常,提示””委托只能包含一个目标方法.
2.如果订阅者的方法抛出异常,.NET会捕捉到它,但是只有在调用EndInvoke()的时候才会将异常重新抛出.在本例中,我们不是用EndInvoke(),所以无须处理异常,因为即使抛出异常,也是在另一个线程上,不会影响到客户端线程.
3.BeginInvoke()方法属于委托定义所生成的类,它既不属于MulticastDelegate,也不属于Delegate基类,所以无法继续使用可重用的FireEvent()方法,需要进行一个向下的转换来获取实际的委托类型.
现在修改一下上面的程序,使用异步调用来解决订阅者的方法执行超时的情况:
namespace 订阅者方法超时的处理1
{
class Program
{
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.DoSomething();
Console.WriteLine("Control back to client!");
Console.WriteLine("Press any thing to exit...");
Console.ReadKey();
}
//触发某个事件,以列表形式返回所有方法的返回值
internal 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 (Exception)
{
throw;
}
}
}
return objList.ToArray();
}
}
public class Publisher
{
public event EventHandler MyEvent;
public void DoSomething()
{
Console.WriteLine("DoSomething invoked");
if (MyEvent!=null)
{
Delegate[] delArray = MyEvent.GetInvocationList();
foreach (Delegate del in delArray)
{
EventHandler method = (EventHandler)del;
method.BeginInvoke(null, EventArgs.Empty, null, null);
}
}
}
}
public class Subscriber1
{
public void OnEvent(object sender, EventArgs e)
{
Thread.Sleep(TimeSpan.FromSeconds(3));//模拟耗时3秒才能完成方法
Console.WriteLine("Waited for 3seconds,subscriber1 invoked!");
}
}
public class Subscriber2
{
public void OnEvent(object sender,EventArgs e)
{
throw new Exception("Subscriber2 failed");//及时抛出异常也不会影响到客户端
//Console.WriteLine("Subscriber2 immediately Invoked!");
}
}
public class Subscriber3
{
public void OnEvent(object sender, EventArgs e)
{
Thread.Sleep(TimeSpan.FromSeconds(2));//模拟耗时两秒才能完成方法
Console.WriteLine("Waited for 2 deconds,subscriber2 invoked!");
}
}
}
这样貌似可以了,需要注意代码输出中的几个变化:
(1)在客户端程序中调用Console.ReadKey()方法来暂停客户端,以提供足够的时间来让异步方法执行完代码,不然客户端的程序到此就运行结束了,你可以试试把这句代码去了看看会发生什么.如果去了这行代码的话,不会有任何订阅者方法的输出,因为它们根本没来得及执行完毕.原因是这样的:客户端所在的线程通常称为主线程,而执行订阅这方法的线程来自线程池,属于后台线程(BackgroundThread),当主线程结束时,不管后台线程有没有执行完毕,都会退出程序.
(2)打印完”Press any thing to exit..”之后,两个订阅者的方法以两秒,一秒的间隔显示出来,且尽管先注册了subscriber1,但是却先执行subscriber3,这是因为执行它需要的时间短.除此之外,注意到这两个方法时并行执行的,所以执行它们的总时间是最长的方法所需要的时间,也就是3秒,而不是他们的累加5秒.
(3)如同前面提过的,尽管subscriber2抛出了异常,我们也没有针对异常进行处理,但是客户程序并没有察觉到,程序也没有因此而中断.
总结
还有一天就开学了,我瞄了一眼这学期的课本,貌似有java web,UML,还有编译原理,其中感觉编译原理是我还接触过的,有机会的话,写点博客加深一下理解.