Last active
April 16, 2020 04:54
Throttle actions by number per period
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using HotelInfoUpdater.Common; | |
using System; | |
using System.Collections.Concurrent; | |
using System.Collections.Generic; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using System.Threading.Tasks.Dataflow; | |
namespace ThrottleTest | |
{ | |
class Program | |
{ | |
static async Task Main(string[] args) | |
{ | |
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; | |
var ts = new Throttle2(TimeSpan.FromSeconds(5), 2); | |
var tsks = new List<Task>(); | |
for (var i = 0; i < 100; i++) | |
{ | |
//Task<Task> | |
var tsk1 = ts.Execute(() => AA(i)); | |
//参数绑定 | |
var tsk2 = ts.Execute((o) => AA((int)o), i); | |
//Task<Task<T>> | |
var tsk3 = ts.Execute(() => BB(i)); | |
//参数绑定 | |
var tsk4 = ts.Execute((o) => BB((int)o), i); | |
//Task<T> | |
var tsk5 = ts.Execute(() => CC(i)); | |
//参数绑定 | |
var tsk6 = ts.Execute((o) => CC((int)o), i); | |
tsks.Add(tsk1); | |
tsks.Add(tsk2); | |
tsks.Add(tsk3); | |
tsks.Add(tsk4); | |
tsks.Add(tsk5); | |
tsks.Add(tsk6); | |
} | |
await Task.WhenAll(tsks); | |
Console.Read(); | |
} | |
private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) | |
{ | |
Console.WriteLine(e.Exception.Message); | |
e.SetObserved(); | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <param name="i"></param> | |
/// <returns></returns> | |
private static async Task AA(int i) | |
{ | |
//await Task.Delay(TimeSpan.FromSeconds(6)); | |
await Task.Delay(TimeSpan.FromSeconds(1)); | |
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}\tAA:{i}"); | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <param name="i"></param> | |
/// <returns></returns> | |
private static async Task<int> BB(int i) | |
{ | |
await Task.Delay(TimeSpan.FromSeconds(1)); | |
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}\tBB:{i}"); | |
return i; | |
} | |
private static int CC(int i) | |
{ | |
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}\tCC:{i}"); | |
return i; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// | |
/// </summary> | |
public class Throttle2 : IDisposable | |
{ | |
#region 参数 | |
/// <summary> | |
/// 每个周期内,允许多少次调用 | |
/// </summary> | |
public int MaxCountPerPeriod { get; } | |
/// <summary> | |
/// 周期 | |
/// </summary> | |
public TimeSpan Period { get; } | |
#endregion | |
#region 核心 | |
/// <summary> | |
/// 用于限速的阻止队列, 如果空不足,插入操作就会等待. | |
/// </summary> | |
private readonly BlockingCollection<int> block; | |
/// <summary> | |
/// 真正的任务队列 | |
/// </summary> | |
private readonly ConcurrentQueue<Task> tsks = new ConcurrentQueue<Task>(); | |
/// <summary> | |
/// 用于 周期性的 重置计数 | |
/// </summary> | |
private readonly System.Timers.Timer timer; | |
#endregion | |
#region 数据 | |
/// <summary> | |
/// 总执行条数 | |
/// </summary> | |
private long _totalExecuted = 0; | |
/// <summary> | |
/// 总执行条数 | |
/// </summary> | |
public long TotalExecuted => this._totalExecuted; | |
/// <summary> | |
/// 总共压入队列数 | |
/// </summary> | |
private long _totalEnqueued = 0; | |
/// <summary> | |
/// 总共压入队列数 | |
/// </summary> | |
public long TotalEnqueued => this._totalEnqueued; | |
/// <summary> | |
/// 当前周期内, 可用空间 (阴止队列可插入数: block.BoundedCapacity - block.Count) | |
/// </summary> | |
public int FreeSpace => this.MaxCountPerPeriod - this.block?.Count ?? 0; | |
/// <summary> | |
/// 当前计数 | |
/// </summary> | |
public int CurrentCount => this._currentCount; | |
/// <summary> | |
/// 当前任务数 | |
/// </summary> | |
public int TaskCount => this.tsks.Count; | |
/// <summary> | |
/// 周期数 | |
/// </summary> | |
public long PeriodNumber { get; private set; } | |
#endregion | |
/// <summary> | |
/// 计数判断 | |
/// </summary> | |
private int _currentCount = 0; | |
/// <summary> | |
/// | |
/// </summary> | |
private bool inProcess; | |
/// <summary> | |
/// | |
/// </summary> | |
private readonly object lockObj = new object(); | |
/// <summary> | |
/// | |
/// </summary> | |
/// <param name="period"></param> | |
/// <param name="maxCountPerPeriod"></param> | |
public Throttle2(TimeSpan period, int maxCountPerPeriod) | |
{ | |
if (period <= TimeSpan.Zero) | |
throw new ArgumentException($"{nameof(period)} 无效"); | |
if (maxCountPerPeriod <= 0) | |
throw new ArgumentException($"{nameof(maxCountPerPeriod)} 无效"); | |
this.MaxCountPerPeriod = maxCountPerPeriod; | |
this.Period = period; | |
this.block = new BlockingCollection<int>(maxCountPerPeriod); | |
this.timer = new System.Timers.Timer(period.TotalMilliseconds) | |
{ | |
AutoReset = true | |
}; | |
this.timer.Elapsed += Timer_Elapsed; | |
this.timer.Start(); | |
//this.TryProcessQueue(); | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <param name="sender"></param> | |
/// <param name="e"></param> | |
private void Timer_Elapsed(object sender, ElapsedEventArgs e) | |
{ | |
//var n = this._currentCount; | |
this._currentCount = 0; | |
this.PeriodNumber++; | |
//this.OnPeriodElapsed?.Invoke(this, new PeriodElapsedEventArgs() | |
//{ | |
// Statistic = new ThrottleStatistic() | |
// { | |
// CurrentExecutedCount = n, | |
// CurrentFreeSpace = this.FreeSpace, | |
// CurrentTaskCount = this.tsks.Count, | |
// TotalEnqueued = this.TotalEnqueued, | |
// TotalExecuted = this.TotalExecuted, | |
// PeriodNumber = this.PeriodNumber | |
// } | |
//}); | |
this.TryProcessQueue(); | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <param name="task"></param> | |
private void Enqueue(Task task) | |
{ | |
var _tsk = task; | |
// 这里对应的是 Throttle.Execute(Action) | |
if (task is Task<Task> t) | |
{ | |
//如果 task 是 task 的嵌套, 需要先解包装 | |
_tsk = t.Unwrap(); | |
} | |
//Throttle.Execute(Func<T>) 怎么判断 ?? | |
else if (task is IUnwrap tt) | |
{ | |
_tsk = tt.GetUnwrapped(); | |
} | |
_tsk.ContinueWith(tt => | |
{ | |
//当任务执行完时, 才能阻止队列的一个空间出来,供下一个任务进入 | |
this.block.TryTake(out _); | |
Interlocked.Increment(ref this._totalExecuted); | |
//Console.WriteLine($"{DateTime.Now}..................Release"); | |
}, TaskContinuationOptions.AttachedToParent); | |
//占用一个空间, 如果空间占满, 会无限期等待,直至有空间释放出来 | |
this.block.Add(0); | |
//Console.WriteLine($"{DateTime.Now}..................Add"); | |
//占用一个空间后, 才能将任务插入队列 | |
this.tsks.Enqueue(task); | |
Interlocked.Increment(ref this._totalEnqueued); | |
//尝试执行任务.因为初始状态下, 任务队列是空的, while 循环已退出. | |
this.TryProcessQueue(); | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
private void TryProcessQueue() | |
{ | |
if (this.inProcess) | |
return; | |
lock (this.lockObj) | |
{ | |
if (this.inProcess) | |
return; | |
this.inProcess = true; | |
//Console.WriteLine($"{DateTime.Now}..................TryProcess {this.FreeSpace}"); | |
try | |
{ | |
ProcessQueue(); | |
} | |
finally | |
{ | |
inProcess = false; | |
} | |
} | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
private void ProcessQueue() | |
{ | |
//当 当前计数 小于周期内最大允许的任务数 | |
//且 任务队列中有任务可以取出来 | |
while ((this._currentCount < this.MaxCountPerPeriod) | |
&& tsks.TryDequeue(out Task tsk)) | |
{ | |
//Console.WriteLine($"{DateTime.Now}..................Dequeue"); | |
Interlocked.Increment(ref this._currentCount); | |
//执行任务 | |
tsk.Start(); | |
} | |
} | |
#region execute | |
#region Func<T> / Func<object, T> | |
/// <summary> | |
/// | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <param name="func"></param> | |
/// <returns></returns> | |
public Task<T> Execute<T>(Func<T> func, CancellationToken cancellation = default, TaskCreationOptions creationOptions = TaskCreationOptions.None) | |
{ | |
var t = new Task<T>(func, cancellation, creationOptions); | |
this.Enqueue(t); | |
return t; | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <param name="func"></param> | |
/// <returns></returns> | |
public Task<T> Execute<T>(Func<object, T> func, object state, CancellationToken cancellation = default, TaskCreationOptions creationOptions = TaskCreationOptions.None) | |
{ | |
var t = new Task<T>(func, state, cancellation, creationOptions); | |
this.Enqueue(t); | |
return t; | |
} | |
#endregion | |
#region Func<Task<T>> / Func<object, Task<T>> | |
/// <summary> | |
/// | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <param name="func"></param> | |
/// <returns></returns> | |
public Task<T> Execute<T>(Func<Task<T>> func, CancellationToken cancellation = default, TaskCreationOptions creationOptions = TaskCreationOptions.None) | |
{ | |
var t = new WrapFuncTask<T>(func, cancellation, creationOptions); | |
this.Enqueue(t); | |
return t.Result; | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <param name="func"></param> | |
/// <returns></returns> | |
public Task<T> Execute<T>(Func<object, Task<T>> func, object state, CancellationToken cancellation = default, TaskCreationOptions creationOptions = TaskCreationOptions.None) | |
{ | |
var t = new WrapFuncTask<T>(func, state, cancellation, creationOptions); | |
this.Enqueue(t); | |
return t.Result; | |
} | |
#endregion | |
#region Action | |
/// <summary> | |
/// | |
/// </summary> | |
/// <param name=""></param> | |
/// <param name="act"></param> | |
/// <returns></returns> | |
public Task Execute(Action act, CancellationToken cancellation = default, TaskCreationOptions creationOptions = TaskCreationOptions.None) | |
{ | |
var t = new Task(act, cancellation, creationOptions); | |
this.Enqueue(t); | |
return t; | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <param name=""></param> | |
/// <param name="act"></param> | |
/// <returns></returns> | |
public Task Execute<T>(Action<object> act, object state, CancellationToken cancellation = default, TaskCreationOptions creationOptions = TaskCreationOptions.None) | |
{ | |
var t = new Task(act, state, cancellation, creationOptions); | |
this.Enqueue(t); | |
return t; | |
} | |
#endregion | |
#endregion | |
#region | |
/// <summary> | |
/// | |
/// </summary> | |
/// <returns></returns> | |
public ThrottleStatistic GetStatistic() | |
{ | |
return new ThrottleStatistic() | |
{ | |
CurrentExecutedCount = this.CurrentCount, | |
CurrentFreeSpace = this.FreeSpace, | |
CurrentTaskCount = this.tsks.Count, | |
TotalEnqueued = this.TotalEnqueued, | |
TotalExecuted = this.TotalExecuted, | |
PeriodNumber = this.PeriodNumber | |
}; | |
} | |
#endregion | |
#region dispose | |
/// <summary> | |
/// | |
/// </summary> | |
public void Dispose() | |
{ | |
this.Dispose(true); | |
GC.SuppressFinalize(this); | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
~Throttle2() | |
{ | |
this.Dispose(false); | |
} | |
private bool isDisposed = false; | |
/// <summary> | |
/// | |
/// </summary> | |
/// <param name="flag"></param> | |
private void Dispose(bool flag) | |
{ | |
if (!isDisposed) | |
{ | |
if (flag) | |
{ | |
if (this.timer != null) | |
{ | |
this.timer.Dispose(); | |
} | |
if (this.block != null) | |
{ | |
this.block.Dispose(); | |
} | |
} | |
isDisposed = true; | |
} | |
} | |
#endregion | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// | |
/// </summary> | |
public class PeriodElapsedEventArgs : EventArgs | |
{ | |
public ThrottleStatistic Statistic { get; set; } | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
public class ThrottleStatistic | |
{ | |
/// <summary> | |
/// 总共压入队列数 | |
/// </summary> | |
public long TotalEnqueued { get; set; } | |
/// <summary> | |
/// 总执行条数 | |
/// </summary> | |
public long TotalExecuted { get; set; } | |
/// <summary> | |
/// 当前周期内执行次数 | |
/// </summary> | |
public int CurrentExecutedCount { get; set; } | |
/// <summary> | |
/// 当前周期内, 可用空间 (阴止队列可插入数: block.BoundedCapacity - block.Count) | |
/// </summary> | |
public int CurrentFreeSpace { get; set; } | |
/// <summary> | |
/// 当前周期内, 未执行任务数 | |
/// </summary> | |
public int CurrentTaskCount { get; set; } | |
/// <summary> | |
/// 周期数 | |
/// </summary> | |
public long PeriodNumber { get; set; } | |
/// <summary> | |
/// | |
/// </summary> | |
/// <returns></returns> | |
public override string ToString() | |
{ | |
return $"TotalEnqueued:{TotalEnqueued}, TotalExecuted:{TotalExecuted}, CurrentExecutedCount:{CurrentExecutedCount}, CurrentFreeSpace:{CurrentFreeSpace}, CurrentTaskCount:{CurrentTaskCount}, PeriodNumber:{PeriodNumber}"; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Text; | |
using System.Threading; | |
using System.Threading.Tasks; | |
namespace HotelInfoUpdater.Common | |
{ | |
/// <summary> | |
/// | |
/// </summary> | |
public interface IUnwrap | |
{ | |
/// <summary> | |
/// | |
/// </summary> | |
/// <returns></returns> | |
Task GetUnwrapped(); | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
public class WrapFuncTask<T> : Task<Task<T>>, IUnwrap | |
{ | |
/// <summary> | |
/// | |
/// </summary> | |
/// <param name="function"></param> | |
/// <param name="cancellationToken"></param> | |
/// <param name="creationOptions"></param> | |
public WrapFuncTask(Func<Task<T>> function, CancellationToken cancellationToken, TaskCreationOptions creationOptions) | |
: base(function, cancellationToken, creationOptions) | |
{ | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <param name="function"></param> | |
/// <param name="state"></param> | |
/// <param name="cancellationToken"></param> | |
/// <param name="creationOptions"></param> | |
public WrapFuncTask(Func<object, Task<T>> function, object state, CancellationToken cancellationToken, TaskCreationOptions creationOptions) | |
: base(function, state, cancellationToken, creationOptions) | |
{ | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <returns></returns> | |
public Task GetUnwrapped() | |
{ | |
return this.Unwrap(); | |
} | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
public class WW<T> : Task<T>, IUnwrap where T : Task | |
{ | |
/// <summary> | |
/// | |
/// </summary> | |
/// <param name="function"></param> | |
/// <param name="cancellationToken"></param> | |
/// <param name="creationOptions"></param> | |
public WW(Func<T> function, CancellationToken cancellationToken, TaskCreationOptions creationOptions) | |
: base(function, cancellationToken, creationOptions) | |
{ | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <returns></returns> | |
public Task GetUnwrapped() | |
{ | |
return (this as Task<Task>).Unwrap(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
如何周期性的对任务节流.
Limit action execution max count per period.
工作中, 有很多地方需要限制一段时间内, 对某个方法的调用.
比如某某API 限制你每分钟只能请求 600次, 怎么办呢?? 一次请求的耗时, 要看对方的响应速度, 服务器的网速, 等多方面的综合因素. 所以不好估算每秒,每分钟我能调用多少次, 只能一次次的去估算, 把并发数调低, 请求间隔调长. 调低, 调长, 会拉长执行间; 调高,调短 会被限制频率, 很是头疼...
疫情过后, 一地鸡毛, 别人复工, 我们轮流在家休假. 上天终于给我安排了时间面对这个问题了.
合理安排任务
是周期性的把一批任务压进内存, 还是视任务队列的执行情况, 在决定要压进多少新任务呢??
假设每个任务要执行2秒, 而每1秒可以执行100次, 那么完成这100个任务就需要2秒.
如果不考虑任务的执行情况, 在第二秒的时候, 任务队列里就会有200个任务在处理.
就好比高铁, 每一站下多少人, 就可以在卖几张票, 而不是不管下了多少人, 都按整列车的座位数重新售票.
前一秒的还没有执行完, 后一秒的又进来了, 这是要累死牛的节凑, 人都不干, 更何况机器, 如果真这样, 内存/CPU 迟早要完玩...
处理并发
秉承最短时间内,做最多的工作的原则, 我们可以认为: 一个周期内, 最大可执行的次数, 就是周期内的最大并发数.
如果想限制最大任务并行数, 可以用
SemaphoreSlim
或Semaphore
.但是如果想限制一段时间内, 最大任务执行次数, 用
Semaphore
就不好办了, 因为不能确定每个任务啥时间运行完.那要怎么限制并发呢??
BlockingCollection<T>
这个是什么不需要我解释, 我们用它来模拟那辆高铁的座位: 位置是有限的, 下去几个人就可以上几个人, 多了上不了,买票请排队. 但是这个对象不能用来存放 Task, 因为哪个 Task 先完成, 哪个 Task 后完成不是可以安排的, 只有当任务完成后, 才能释放出来一个空位, 就如同高铁上的座位只有等乘客下车才能释放出来, 而不是一上车,说我到哪哪下之后就可以释放出来. 所以这个
BlockingCollection<T>
只用来存占位符, 真正的任务队列我们另请高明.本着先进先出的原则, 我们用
ConcurrentQueue<T>
来存放任务队列.有票才能上车
上车的时候, 需要在阻止队列中插入一个占位符, 占位符插入成后, 才能把任务添加到任务队列中.
上车之前, 还要对任务做一下扩展: 下车的时候, 把你占用的位置释放出来
帽子戏法
如果一个任务是
Task<Task>
或Task<Task<T>>
, 你一定要对它Unwrap
, 否则一眨眼, 你的任务就执行完了, 下车回头一看, 才发现: 我艹, 我包呢?? 你的包(子任务)还在等待着开往春天的地铁呢.但是
task is Task<Task<>>
(语法错误) 还是task is Task<object>
(类型不匹配) 呢?? 怎么都不对, 这个时候才发现 C# 太TMD 的不地道了, 这么简单的车, 还需要我在兜一圈.差不多了, 开车
先加个开车提醒:
开车了....