林德熙 小伙伴希望保存一个文件,并且希望如果出错了也要不断地重试。然而我认为如果一直错误则应该对外抛出异常让调用者知道为什么会一直错误。

这似乎是一个矛盾的要求。然而最终我想到了一个办法:让重试一直进行下去,谁需要关心异常谁就去 catch 异常,不需要关心异常的模块则跟着一直重试直到成功。

我们通过编写一个自己的 Awaiter 来实现,本文将说明其思路和最终实现的代码。


Awaiter 系列文章

入门篇:

实战篇:

遇到了什么问题

有一个任务,可能会出错,然而重试有可能可以解决。典型的例子是写入文件,你可能因为其他进程占用的问题而导致无法写入,然而一段时间之后重试是可以解决的。

现在,不同业务对这同一个操作有不同的需求:

  • 有的业务不关心写入结果到底如何
  • 有的业务由于时间有限,只能接受几次的重试
  • 有的业务关心写入过程中的异常
  • 而有的业务非常闲,只要一直写入就行了,最终成功告诉我就好

可是,我们如何在一个任务中同时对所有不同的业务需求进行不同种类的响应呢?

思路

我的思路是:

  1. 当有业务发起请求之后,就开启一个不断重试的任务;
  2. 针对这个请求的业务,返回一个专为此业务定制的可等待对象;
  3. 如果在重试完成之前,还有新的业务请求发起,那么则返回一个专为此新业务定制的可等待对象;
  4. 一旦重试任务成功完成,那么所有的可等待对象强制返回成功;
  5. 而如果重试中有的可等待对象已经等待结束但任务依旧没有成功,则在可等待对象中引发任务重试过程中发生过的异常。

这样,任务不断重试。而且,无论多少个业务请求到来,都只是加入到循环中的一部分来,不会开启新的循环任务。每个业务的等待时长和异常处理都是自己的可等待对象中处理的,不影响循环任务的继续执行。

关于源代码说明

本文所述的所有源代码可以在 https://gist.github.com/walterlv/d2aecd02dfad74279713112d44bcd358 查看和下载到最新版本。

期望如何使用这个新的 Awaiter

public class WalterlvDemo
{
// 记录一个可以重试的循环。
private readonly PartialAwaitableRetry _loop; public WalterlvDemo()
{
// 初始化一个可以重试的循环,循环内部执行的方法是 TryCoreAsync。
_loop = new PartialAwaitableRetry(TryCoreAsync);
} // 如果外界期望使用这个类试一下,那么就调用此方法。默认尝试 10 次,但也可以指定为 -1 尝试无数次。
public ContinuousPartOperation TryAsync(int tryCount = 10)
{
// 加入循环中,然后返回一个可以异步等待 10 次循环的对象。
return _loop.JoinAsync(tryCount);
} // 此方法就是循环的内部执行的方法。
private async Task<OperationResult> TryCoreAsync(PartialRetryContext context)
{
// 每 1 秒执行一次循环重试,当然你也可以尝试指数退避。
await Task.Delay(1000).ConfigureAwait(false); // 执行真正需要重试而且可能出现异常的方法。
await DoSomethingIOAsync().ConfigureAwait(false); // 如果执行成功,那么就返回 true。当然,上面的代码如果出现了异常,也是可以被捕获到的。
return true;
} // 这就是那个有可能会出错,然后出错了需要不断重试的方法。
private async Task DoSomethingIOAsync()
{
// 省略实际执行的代码。
}
}

写一个可以不断循环的循环,并允许不同业务加入等待

上面的代码中,我们使用到了两个新的类型:用于循环执行某个委托的 PartialAwaitableRetry,以及用于表示单次执行结果的 OperationResult

以下只贴出此代码的关键部分,全部源码请至本文末尾查看或下载。

public class PartialAwaitableRetry
{
// 省略构造函数和部分字段,请至本文文末查看完整代码。
private readonly List<CountLimitOperationToken> _tokens = new List<CountLimitOperationToken>(); public ContinuousPartOperation JoinAsync(int countLimit)
{
var token = new CountLimitOperationToken(countLimit); // 省略线程安全代码,请至本文文末查看完整代码。
_tokens.Add(token);
if (!_isLooping)
{
Loop();
} return token.Operation;
} private async void Loop()
{
while(true)
{
await _loopItem.Invoke(context).ConfigureAwait(false);
// 省略线程安全处理和异常处理,请至本文文末查看完整代码。
foreach (var token in _tokens)
{
token.Pass(1);
}
}
}
}

维护一个 CountLimitOperationToken 的集合,然后在每次循环的时候更新集合中的所有项。这样,通过 JsonAsync 创建的每一个可等待对象就能更新其状态 —— 将异常传入或者将执行的次数传入。

由于我们在创建可等待对象 CountLimitOperationToken 的时候,传入了等待循环的次数,所以我么可以在 CountLimitOperationToken 内部实现每次更新循环执行次数和异常的时候,更新其等待状态。如果次数已到,那么就通知异步等待完成。

关于 OperationResult 类,是个简单的运算符重载,用于表示单次循环中的成功与否的状态和异常情况。可以在本文文末查看其代码。

写一个可等待对象,针对不同业务返回不同的可等待对象实例

我写了三个不同的类来完成这个可等待对象:

  • CountLimitOperationToken

    • 上面的代码中我们使用到了这个类型,目的是为了生成 ContinuousPartOperation 这个可等待对象。
    • 我将这个 Token 和实际的 Awaitable 分开,是为了隔离执行循环任务的代码和等待循环任务的代码,避免等待循环任务的代码可以修改等待的过程。
  • ContinuousPartOperation
    • 这个是实际的可等待对象,这个类型的实例可以直接使用 await 关键字进行异步等待,也可以使用 Wait() 方法进行同步等待。
    • 我把这个 Awaitable 和 Awaiter 分开,是为了隔离 await 关键字的 API 和编译器自动调用的方法。避免编译器的大量方法干扰使用者对这个类的使用。
  • ContinuousPartOperation.Awaiter
    • 这是实际上编译器自动调用方法的一个类,有点类似于我们为了支持 foreach 而实现的 IEnumerator。(而集合应该继承 IEnumerable

所以其实这三个类是在干同一件事情,都是为了实现一个可 await 异步等待的对象。

关于如何编写一个自己的 Awaiter,可以参考我的 Awaiter 入门篇章:

以及实战篇章:

这几个类的实际代码可以在文末查看和下载。

附全部源码

如果你看不到源码,请前往 gist.github.com


我的博客会首发于 https://walterlv.com/,而 CSDN 和博客园仅从其中摘选发布,而且一旦发布了就不再更新。

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://blog.csdn.net/wpwalter),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系

.NET 编写一个可以异步等待循环中任何一个部分的 Awaiter的更多相关文章

  1. Java循环中删除一个列表元素

    本文主要想讲述一下我对之前看到一篇文章的说法.假设跟你的想法有出入,欢迎留言.一起讨论. #3. 在循环中删除一个列表元素 考虑以下的代码.迭代过程中删除元素: ArrayList<String ...

  2. 题目一:编写一个类Computer,类中含有一个求n的阶乘的方法

    作业:编写一个类Computer,类中含有一个求n的阶乘的方法.将该类打包,并在另一包中的Java文件App.java中引入包,在主类中定义Computer类的对象,调用求n的阶乘的方法(n值由参数决 ...

  3. 编写Java程序,在硬盘中选取一个 txt 文件,读取该文档的内容后,追加一段文字“[ 来自新华社 ]”,保存到一个新的 txt 文件内

    查看本章节 查看作业目录 需求说明: 在硬盘中选取一个 txt 文件,读取该文档的内容后,追加一段文字"[ 来自新华社 ]",保存到一个新的 txt 文件内 实现思路: 创建 Sa ...

  4. JavaScript的for循环中嵌套一个点击事件为何点击一次弹出多个相同的值

    先看下面一段代码: for(var i=0; i<10; i++) { $('#ul').bind('click', function() { alert(i) }) } 对于这段代码,当点击I ...

  5. python循环中对一个列表的赋值问题

    参考:https://www.cnblogs.com/zf-blog/p/10613981.html https://www.cnblogs.com/andywenzhi/p/7453374.html ...

  6. volist/foreach下,点击循环中的一个进行操作

    第一种方法,是给点击元素绑定事件,用ajax将值传到控制器中,其中传的值,用jquery选择器选择值. 1.在html中 <foreach name="save" item= ...

  7. Hibernate的多表查询,分装到一个新的实体类中的一个方法

    不知道是否还有其他方法实现,请高人指点. 如果涉及到多张表多字段查询,并且想利用查询出来的字段在界面层构建一个新的实体类,可以使用这种方法: 如果查询出来的多字段中,有多个字段的名字都相同(如想查询出 ...

  8. 【安卓面试题】在一个Activity启动另一个Activity和在Service中启动一个Activity有什么区别

    在Activity中可以直接使用Intent启动另一个Activity 显式Intent intent = new Intent(context, activity.class) 隐式 Intent ...

  9. 【解决了一个小问题】golang中引用一个路径较长的库,导致goland中出现"module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v2"

    在项目中的go.mod文件中有这样一句: require ( github.com/xxx-devops/xx1/sdk/go v2.2.3 ) 项目的编译没有问题,但是goland中出现如下提示: ...

随机推荐

  1. 2018年全国多校算法寒假训练营练习比赛(第一场)D N阶汉诺塔变形

    https://www.nowcoder.com/acm/contest/67/D 思路: 先手动模拟一下过程,以下是模拟过程,按顺序表示第几步需要移动的盘标号 1 1 2 1 1 2 1 1 3 1 ...

  2. Python进阶--常用模块

    一.模块.包 什么是模块? 模块实质上就是一个python文件,它是用来组织代码的,意思就是说把python代码写到里面,文件名就是模块的名称,test.py test就是模块名称. 什么是包? 包, ...

  3. python-day47--mysql数据备份与恢复

    一.IDE工具介绍 掌握: #1. 测试+链接数据库 #2. 新建库 #3. 新建表,新增字段+类型+约束 #4. 设计表:外键 #5. 新建查询 #6. 备份库/表 #注意: 批量加注释:ctrl+ ...

  4. Ubuntu解压缩zip,tar,tar.gz,tar.bz2文件命令

    tar解包:tar xvf FileName.tar打包:tar cvf FileName.tar DirName .gz解压1:gunzip FileName.gz解压2:gzip -d FileN ...

  5. csp 通信网络

    http://blog.csdn.net/zyy_1998/article/details/78334496 试题编号: 201709-4 试题名称: 通信网络 时间限制: 1.0s 内存限制: 25 ...

  6. java使用HttpClient

    HttpClient常用的包有两个 org.apache.http.client以及org.apache.commons.httpclient 我常用的是org.apache.http.client. ...

  7. idea Exception in thread "http-apr-8080-exec-2" java.lang.OutOfMemoryError: PermGen space

    idea Exception in thread "http-apr-8080-exec-2" java.lang.OutOfMemoryError: PermGen space ...

  8. 063——VUE中vue-router之重定向redirct的使用

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  9. 062——VUE中vue-router之命名视图的实例

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  10. POJ 3083 Bfs+Dfs

    注意求最短路的时候用Bfs. #include<iostream> #include<stdio.h> using namespace std; int w,h,ex,ey,s ...