C# Retry重试操作解决方案(附源码)
一、前言
(1)对于Thread的Abort方法,如果线程当前正在执行的是一段非托管代码,那么CLR就不会抛出ThreadAbortException,只有当代码继续回到CLR中时,才会引发ThreadAbortException。当然,即便是在CLR环境中ThreadAbortException也不会立即引发。
(2)对于BackgroundWorker的CancelAsync方法,需要设置WorkerSupportsCancellation属性为True,在执行方法内部检测CancellationPending标识,用户负责退出。
(3)对于CancellationTokenSource,场景主要为对任务设置一个预期执行时间,对超时的任务自动取消。达到时间间隔后自动触发Cancel方法,IsCancellationRequested被设置为True,用户同样需要在方法内部检测IsCancellationRequested属性。
本文在基于上述基础上,对于方法的Retry(重新执行)操作,执行时间可能比较久,容易导致主线程阻塞,因此主要以BackgroundWorker来执行相关操作。RetryProvider类图以及相关操作示例图如下:
二、RetryProvider的具体实现
RetryProvider主要用于对方法的重新执行操作,在后台线程BackgroundWorker对执行方法进行相应的控制和处理,主要提供以下2种方法:
(1)public void StartAsync(Action target, int waitTimeout = 0, int retryCount = 1)
(2)public void StartAsyncFunc(Func<bool> target, int waitTimeout = 0, int retryCount = 1)
第一种方法主要针对于无返回参数的方法,第二种方法主要针对于返回bool参数的方法,当然你也可以扩展该类,支持相关的其他参数的方法。方法具体如下所示:
public void StartAsync(Action target, int waitTimeout = 0, int retryCount = 1) { StartAsyncRetry(target, waitTimeout, retryCount); } public void StartAsyncFunc(Func<bool> target, int waitTimeout = 0, int retryCount = 1) { StartAsyncRetry(target, waitTimeout, retryCount); } private void StartAsyncRetry(object target, int waitTimeout, int retryCount) { if (target == null) { throw new ArgumentNullException("target"); } if (this._backgroupThread == null) { this._backgroupThread = new BackgroundWorker(); this._backgroupThread.WorkerSupportsCancellation = true; this._backgroupThread.WorkerReportsProgress = true; this._backgroupThread.DoWork += OnBackgroupThreadDoWork; this._backgroupThread.ProgressChanged += OnBackgroupThreadProgressChanged; } if (this._backgroupThread.IsBusy) { return; } this.WaitTimeout = waitTimeout; this.RetryCount = retryCount; this._backgroupThread.RunWorkerAsync(target); }
在后台线程中执行的方法,主要根据RetryCount和BackgroundWorker的CancellationPending属性进行相应的控制和处理,同时根据方法重试次数来报告相关进度,代码如下:
private void Start(object target) { if (target == null) { return; } int retryCount = this.RetryCount; lock (_asyncLock) { this._backgroupThread.ReportProgress(5); while (!this._backgroupThread.CancellationPending) { if (this.PerRetryBegin != null) { this.PerRetryBegin(this.RetryCount - retryCount); } try { if (target.GetType() == typeof(Action)) { (target as Action).Invoke(); InvokeCompletedEvent(true); return; } else { if ((target as Func<bool>).Invoke()) { InvokeCompletedEvent(true); return; } else { throw new InvalidOperationException("Execute Failed."); } } } catch (Exception ex) { if (this.PerRetryFailedCompleted != null) { this.PerRetryFailedCompleted(ex); } this._backgroupThread.ReportProgress((this.RetryCount - retryCount + 1) * 100 / this.RetryCount); } finally { if (this.PerRetryEnd != null) { this.PerRetryEnd(this.RetryCount - retryCount); } } if (this.RetryCount > 0) { retryCount--; if (retryCount == 0) { InvokeCompletedEvent(); return; } } Thread.Sleep(this.WaitTimeout); } if (this._backgroupThread.CancellationPending) { if (this.Cancelled != null) { this.Cancelled(); } } } }
目前只提供Action和Func<bool>参数的重试方法,因此只需要检测下参数类型(如target.GetType() == typeof(Action))就可以进行相应的操作。如果传入的参数是Func<bool>类型的,目前的处理方式为:检测方法是否执行成功,如果返回true,即if ((target as Func<bool>).Invoke()),则退出重试操作后台线程,不在执行相应重试操作,否则抛出异常继续执行操作。
Thread.Sleep(this.WaitTimeout)主要为阻塞当前线程,等待设置的Timeout时间,然后继续执行相关方法。
三、相关应用示例
(1)启动重试方法,设置相应参数以及相关事件。
private void btnStart_Click(object sender, EventArgs e) { int waitTimeout = 0; int.TryParse(this.nupWaitTimeout.Value.ToString(), out waitTimeout); int retryCount = 0; int.TryParse(this.nupRetryCount.Value.ToString(), out retryCount); Start(waitTimeout, retryCount); } private void Start(int waitTimeout, int retryCount) { if (this._retryProvider == null) { this._retryProvider = new RetryProvider(); this._retryProvider.PerRetryFailedCompleted += _retryProvider_PerRetryFailedCompleted; this._retryProvider.Cancelled += _retryProvider_Cancelled; this._retryProvider.PerRetryBegin += _retryProvider_PerRetryBegin; this._retryProvider.PerRetryEnd += _retryProvider_PerRetryEnd; this._retryProvider.ProgressChanged += _retryProvider_ProgressChanged; this._retryProvider.Completed += _retryProvider_Completed; } if (this._retryProvider.IsBusy) { return; } this.btnStart.Enabled = false; if (this.listBoxRecord.Items.Count > 0) { AddMsg(null); } this._retryProvider.StartAsyncFunc(ThrowExceptionMethod, waitTimeout * 1000, retryCount); }
相应的事件如下:
public delegate void CompletedEventHandler(bool success); //RetryProvider完成委托
public event CompletedEventHandler Completed; //RetryProvider完成事件
public delegate void CancelledEventHandler(); //RetryProvider取消委托
public event CancelledEventHandler Cancelled; //RetryProvider取消事件
public delegate void PerRetryBeginEventHandler(int retryIndex); //RetryProvider每一次重试开始操作委托,参数retryIndex重试次数,从0开始
public event PerRetryBeginEventHandler PerRetryBegin; //RetryProvider每一次重试开始操作事件
public delegate void PerRetryEndEventHandler(int retryIndex); //RetryProvider每一次重试结束操作委托,参数retryIndex重试次数,从0开始
public event PerRetryEndEventHandler PerRetryEnd; //RetryProvider每一次重试结束操作事件
public delegate void PerRetryFailedEventHandler(Exception ex); //RetryProvider每一次重试失败委托,参数Exception为失败的异常信息
public event PerRetryFailedEventHandler PerRetryFailedCompleted; //RetryProvider每一次重试失败事件
public delegate void ProgressChangedEventHandler(int percent); //RetryProvider进度更改委托
public event ProgressChangedEventHandler ProgressChanged; //RetryProvider进度更改事件
(2)取消重试操作:
private void btnCancel_Click(object sender, EventArgs e) { if (this._retryProvider != null) { this._retryProvider.Cancel(); } }
该方法将导致后台线程执行CancelAsync操作,在重试操作中,如果检测到后台线程的CancellationPending为true,则会触发Cancelled事件,退出后台线程。
六、总结
RetryProvider的主要是针对重试操作的封装。考虑重试操作的操作时间可能较长,因此采用BackgroundWorker来进行相应的处理和控制。这种主要用于执行操作可控制的情况,对于不可控的,比如调用第三方程序进行相应操作(如调用MATLAB进行命令操作),等待时间和执行时间不确定,最佳方法为用Timer定时触发进度条循环滚动,后台线程执行相应操作,线程执行完以后再设置相应内容。
源码下载地址:重试解决方案文件