博客园  :: 首页  :: 联系 :: 管理

1、Retry机制

C#和C++, Java一样拥有异常处理机制,我相信很多朋友和我一样,第一次接触异常的时候,都非常希望异常能够像内核捕获内存page fault异常一样类似的拥有Retry(重试)的机制,很可惜这些语言中都没有给我们提供Retry机制。

当然原因有很多,比如如果重试,那么到底是重试哪部分代码呢?是try{}catch{}中的代码?还是发生异常那一行的代码?重试多少次?重试之间是不是要再等一会儿?如果再失败怎么处理?而且如果不同层有Retry,那么会出现Retry次数以乘法形式递增(底层Retry 5次,高层Retry 3次,那么实际Retry次数将达到15次)。

在应用环境中的Retry不比内核,处理后只需要重新执行产生异常的那一条指令,应用环境的要求要更复杂。

可是在实际应用中,特别是面对网络应用时候,有可能会有大量的类似于TimeOut(超时)、或者外部资源被临时占用、暂时性的设备较忙的异常产生,只要再重试几次就会正常。如果此时将raise异常,并且扔向更高层,显然是不太明智的。于是就开始有人用有限次循环的办法来模拟Retry。典型的代码如下:


    public static void TraditionalSolution()
    
{
        
int retryTimes = 10;
        
        
for (int i = 0; i < retryTimes; ++i)
        
{
            
try
            
{
                
//    do something here, 
                
//    such as call WebClient to fetch a webpage.
                
//    the code might throw an exception.
                break;
            }

            
catch (Exception)
            
{
                
if (i == retryTimes - 1)
                
{
                    
throw;
                }

                
else
                
{
                    System.Threading.Thread.Sleep(TimeSpan.FromSeconds(
5));
                }

            }

        }

    }

使用这样的代码可以成功的实现Retry,如果中间的代码发生异常,循环体会捕捉到,如果没有到达retryTimes的要求,那么继续重复执行中间的过程。每次retry前都会先等待5秒钟。如果到达retryTimes后依旧还发生异常,那就没办法,throw到上层。这段代码很典型,在C++和Java中也可以使用类似的方式进行处理。

这段代码的缺点就是,这层循环、等待、判断循环终结抛出异常的代码总要附着在你需要retry的代码块外面的事情周围。如果程序中有不少需要retry的地方,这部分代码反复出现就显得很臃肿了。而且,如果这段代码有一点小小的错误,或者有更多的需求加入进去,要同步项目中其它地方retry代码,这就很不合理了。下面我们就利用C#的匿名方法(anonymous methods)匿名委托(anonymous delegate)来美化我们的代码。

首先,我们先声明一个无参数委托。

public delegate void NoArgumentHandler();

然后,我们写一个专门处理Retry机制的函数。

 

    public static void Retry(int retryTimes, TimeSpan interval, bool throwIfFail, NoArgumentHandler function)
    
{
        
if (function != null)
        
{
            
for (int i = 0; i < retryTimes; ++i)
            
{
                
try
                
{
                    function();
                    
break;
                }

                
catch (Exception)
                
{
                    
if (i == retryTimes - 1)
                    
{
                        
if (throwIfFail)
                        
{
                            
throw;
                        }

                        
else
                        
{
                            
break;
                        }

                    }

                    
else
                    
{
                        
if (interval != null)
                        
{
                            System.Threading.Thread.Sleep(interval);
                        }

                    }

                }

            }

        }

    }

 

这段代码实现了前面代码的Retry机制,把需要执行的部分,换成了NoArgumentHandler的函数参数。使用者可以定义function()执行需要retry的次数,retry间隔的等待时间,当retryTimes跑完后,依旧未能成功地话,是否抛出这个异常。

最后,我们应用这个Retry()函数。大家都看到了delegate,那么我们需要对每一个retry块都定义一个函数么?不需要了,C# 2.0早就提供了anonymous methods(匿名方法)来处理。我们看一下,新的Retry调用代码:

 

    public static void NewApproach()
    
{
        
//    ...
        Retry(10, TimeSpan.FromSeconds(10), truedelegate
        
{
            
//    do something here, 
            
//    such as call WebClient to fetch a webpage.
            
//    the code might throw an exception.
        }
);
        
//    ...
    }

 

这样我们的Retry代码简洁多了。对现有代码增加Retry机制,只需要加上一层括号就可以了,而且Retry行为可以很方便的在参数上调整。
我们可以进一步注意到delegate后面连()也省略掉了,也就是说delegate没有函数调用签名,我们这里使用的是匿名委托(anonymous delegate),让C#编译器去自己推导正确的delegate类型。在这个case里面,由于Retry()对应位置只有NoArgumentHandler一个delegate类型,因此,将delegate{}自动视为NoArgumentHandler类型。

如果有什么进一步需求的话,可以自己改进Retry()函数,定制为自己的Retry()。举个例子,可以为Retry()再增加一个delegate函数参数,用以进行异常发生后的处理。

注意:Retry机制很方便用,但是不要过分使用Retry。特别是在不同层使用Retry的时候更要细心。特别要注意乘法效应导致Retry次数和时间成倍增长的情况,否则反而不美。


2、WinForm 的线程安全的访问

用C#写Windows程序少不了要写WinForm程序。很多时候,我们还需要写多线程的WinForm应用。最典型的就是为了不因为核心代码执行影响用户对应用程序的响应,当执行一个比较耗时的操作时,为了方式用户界面死掉的情况,常需要建立一个背景线程去运行耗时的代码,并且实时将结果表现在当前窗口上。

在多线程访问WinForm的时候,我们会注意到,WinForm的那些Control不是线程安全的。因此不建议直接访问它们,否则会导致竞争冒险,甚至可能出现死锁。比如在线程中直接使用下面的代码就是不推荐的:

textBox1.Text = “OK”;

微软建议的办法,使建立一个SetTextBox(string text)的函数,里面写上上面这句。然后判断this.InvokeRequired,如果需要,就调用this.Invoke(SetTextBox, “OK”);,否则直接调用textBox1.Text = “OK”

总结一下会发现,这个建议的方法不合理,因为同样是textBox1.Text = “OK”; 在不同的位置上被写了两次。两次?惊醒。凡是同一个东西,在不同的位置出现了两次,我们就需要惊醒了。很多安全问题都是由于这种两个不同位置表达一个意思而造成。如果修改了一个地方,而忘了修改另一个地方怎么办?如果是数据的话,还会出现,用户代码到底会用哪个数据作为基准?虽然程序员在“尽量”保证两个位置一致,但是历史已经无数次告诉我们这种“尽量”非常不可靠。那么我们如何解决这个问题呢?这次匿名方法和匿名委托又一次显身手了。

首先,我们定义一个线程安全的访问Control的函数 DoThreadSafe():

        private static void DoThreadSafe(Control control, MethodInvoker function)
        
{
            
if (function != null)
            
{
                
if (control.InvokeRequired)
                
{
                    control.Invoke(function);
                }

                
else
                
{
                    function();
                }

            }

        }

这里的代码实现了微软推荐的采用InvokeRequired判断,然后通过Invoke()调用具体操作的逻辑,但是通过入口的MethodInvoker function函数参数避免了同一个东西被写两次的情况。这里MethodInvoker是System.Windows.Forms名字空间下的一个delegate,和上面的NoArgumentHandler定义一样:

delegate void MethodInvoker();

有了这个小小的帮助函数,我们写线程安全的 WinForm 操作就很简单了,比如,这回我们需要设置进度条的Value:

 

        private void SetProgressBar(int value)
        
{
            DoThreadSafe(progressBar1, 
delegate
            
{
                progressBar1.Value 
= value;
            }
);
        }

 

凡是需要访问 WinForm 空间,我们都包裹上这么一行代码,就能够保证对WinForm所作的操作时线程安全的了,很方便。

如果进一步注意,我们会发现,虽然我们的delegate是无参数传递的,但是,在上面的调用代码里面,prograssBar1.Value = value,这个value是delegate外的函数地参数。神奇吧?这是合法的,虽然名义上,这是一个匿名方法,已经不属于当前scope了,但是依旧可以访问当前scope里面的变量,这就给我们很大的便利,我们可以充分利用这一点,而不再需要定义各种各样的有参数的delegate来完成对不同控件所需要的线程安全的操作。呵呵,否则,按照微软的建议,那几乎是要对WinForm上每一个元素都做一个SetXxxxxText(string text), GetXxxxxText()函数了。

这个小小的DoThreadSafe(),大大降低了对那些Delegate的需求,并且利用匿名方法(anonymous methods)大大减少了声明函数的工作量。(当然实际上编译器在生成执行代码的时候会帮你自动产生对应的函数,不信你就用Reflector来看看。)

3、总结

匿名方法可以降低另写method的工作量,而且匿名方法可以访问调用者同scope的变量,利用这点我们可以大大简化委托的声明,和降低传参的复杂度。

好好的利用匿名方法和匿名委托,会让你的代码看起来更加优雅,优雅的代码也会降低错误发生的可能性。