我有一个类似的课程:
public sealed class Contract
{
public bool isExpired { get; set; }
public DateTime ExpirationDate { get; set; }
public void MarkAsExpired()
{
this.isExpired = true;
// Other stuff..
}
}
我想要做的是:达到ExpirationDate
后,应调用MarkAsExpired
。如果程序关闭,并且当它重新打开时,ExpirationDate
已经过去,同样应该发生。如果isExpired
为真,则根本不会发生任何事情。
合同是可修改的,并且经常添加新合同。我确切的预期数量是未知的。
我已经知道这可以通过DateTime
对象的可推广扩展方法来完成:
var contracts = new List<Contract>();
foreach (var contract in contracts.Where(contract => !contract.isExpired))
{
contract.ExpirationDate.ExecuteWhenPassed(() => contract.MarkAsExpired());
}
问题在于编写扩展方法本身。我已经创建了一个可行的解决方案:
static void ExecuteWhenPassed(this DateTime date, Action action)
{
// Check if date has already passed
if (date <= DateTime.Now)
{
action();
}
else
{
// Timer will fire when $date is reached
var timer = new Timer { Interval = date.Subtract(DateTime.Now).TotalMilliseconds, AutoReset = false };
timer.Elapsed += (s, e) => action();
timer.Start();
}
}
它完美无缺。但是,我关注它的效率。具体来说,我担心为每个Contract
实例创建一个单独的计时器所涉及的开销,其中可能有数百个尚未到期。
开销是否显着?如果是这样,那么处理这个问题的更有效方法是什么?
只要处理过期问题得到解决,我就会采用完全不同的方法。
答案 0 :(得分:4)
我建议使用Microsoft的Reactive Framework(NuGet“System.Reactive”)。它变得非常容易。
以下是代码:
List<Contract> contracts = new List<Contract>();
/* populate `contracts` here before `query` */
IObservable<Contract> query =
from i in Observable.Interval(TimeSpan.FromMinutes(1.0))
from c in contracts
where !c.isExpired
where c.ExpirationDate <= DateTime.Now
select c;
IDisposable subscription =
query
.Subscribe(c => c.MarkAsExpired());
这样做是设置一个可观察的计时器(Observable.Interval(TimeSpan.FromMinutes(1.0))
),每分钟触发一次值。然后,每分钟,它都会将合同列表过滤到那些尚未过期且过期数据早于现在的合同列表。
然后在subscription
中,只需获取这些值的流,并将其标记为已过期。
如果您想停止处理,只需致电subscription.Dispose()
即可。这是一段非常简单的代码。
如果您在Windows窗体上运行此代码,则可以执行.ObserveOn(instanceOfFormOrControl)
以返回UI线程。在WPF上,它是.ObserveOnDispatcher()
。该代码就在.Subscribe(...)
之前。
答案 1 :(得分:1)
每个合约不需要使用一个计时器,你可以用一个计时器来完成。找到下一个要过期的项目并创建计时器。当计时器触发时 - 使该项目到期,然后配置计时器并重复执行程序。好处包括:项目大约在ExpirationTime
(没有&#34;每分钟\秒;&#34;轮询)中规定的时间到期,最多一个计时器,如果没有项目到期 - 没有计时器。示例代码(而不是使用单独的MarkAsExpired
方法 - 您可以在IsExpired
属性设置器中执行逻辑:
public interface IExpirable {
bool IsExpired { get; set; }
DateTime ExpirationTime { get; }
}
public class ExpirationManager {
private readonly List<IExpirable> _items = new List<IExpirable>();
public ExpirationManager(IEnumerable<IExpirable> items) {
_items.AddRange(items);
Trigger();
}
public void Add(IExpirable item) {
lock (_items)
_items.Add(item);
// reset current timer and repeat
Trigger();
}
public void Remove(IExpirable item) {
lock (_items)
_items.Remove(item);
// reset current timer and repeat
Trigger();
}
private Timer _timer;
private void Trigger() {
// reset first
if (_timer != null) {
_timer.Dispose();
_timer = null;
}
IExpirable next;
lock (_items) {
next = _items.Where(c => !c.IsExpired).OrderBy(c => c.ExpirationTime).FirstOrDefault();
}
if (next == null)
return; // no more items to expire
var dueTime = next.ExpirationTime - DateTime.Now;
if (dueTime < TimeSpan.Zero) {
// already expired, process here
next.IsExpired = true;
// and repeat
Trigger();
}
else {
_timer = new Timer(TimerTick, next, dueTime, Timeout.InfiniteTimeSpan);
}
}
private void TimerTick(object state) {
((IExpirable)state).IsExpired = true;
// repeat
Trigger();
}
}
答案 2 :(得分:0)
以下答案假定按时间排序的活动合约集合。
static List<Contract> ActiveContracts = new List<Contract>();
// ...
ActiveContracts.AddRange(contracts.Where(x => !x.isExpired));
ActiveContracts.Sort((e1, e2) => e1.ExpirationDate.CompareTo(e2.ExpirationDate));
这个假设主要是针对性能的,我的方法也可以在没有它的情况下工作,只是在每次更新中过滤和排序合同时效率可能会受到影响。实际选择可能取决于合同添加到列表或合同日期的频率变化。
使用可在需要时执行下一次更新事件的计时器 - 首先在开始后更新(1ms),后续更新将取决于可用的到期日期
// trigger initially in 1ms
System.Timers.Timer timer = new System.Timers.Timer(1);
timer.Elapsed += timer_Elapsed;
timer.AutoReset = false;
timer.Start();
在已用事件中,更新过去过期日期的所有合同,并根据将来最近的到期日期设置新间隔
static void timer_Elapsed(object sender, ElapsedEventArgs e)
{
var t = sender as System.Timers.Timer;
while (ActiveContracts.Any())
{
var item = ActiveContracts[0];
if (item.ExpirationDate > DateTime.Now)
{
break;
}
item.MarkAsExpired();
ActiveContracts.RemoveAt(0);
}
if (ActiveContracts.Any())
{
// specially when debugging, the time difference can lead to a negative new interval - don't assign directly
var nextInterval = (ActiveContracts[0].ExpirationDate - DateTime.Now).TotalMilliseconds;
t.Interval = nextInterval > 0 ? nextInterval : 1;
t.Start();
}
else
{
t.Stop();
}
}
如果您希望合同列表在不同时间间隔内更改,则可能需要定义上限间隔(if (nextInterval > XXX) nextInterval = XXX;
)。如果您希望空列表重新填写新合同,请将t.Stop();
替换为t.Interval = XXX;
答案 3 :(得分:0)
首先让我说,无论效率概念如何,您的解决方案都是优雅和完美的,但您是对的,拥有更多线程并不一定意味着实现高效的系统。就像你现在一样,当你使用计时器时,你实际上是在创建一个背景线程,它必须勾选时间并在适当的时刻激发你想要的动作。现在根据证据,在实际情况下,你将拥有数百个。问题是资源将在所有这些线程之间共享,如果您的cod是服务器端的,这可能是一个严重的问题。 所以,如果我遇到同样的问题,我会选择其他解决方案。我会接受一个合理的宽容。此容差不是固定量,但每次触发操作时都会计算。 此解决方案不如您的解决方案准确,但它仍然可以让服务器保存更多资源。 使用此方法,您只有一个后台线程可以使指定的项目到期。
让我们看一下代码:
var timer = new Timer {Interval = ComputeAvgTime(), AutoReset = true};
timer.Elapsed += timer_Elapsed;
timer.Start();
ComputeAvgTime
计算优化量。
static double ComputeAvgTime()
{
//For all items wich are not expired yet.
var contractExpTime = new List<DateTime>();
var timeStamp = DateTime.Now;
//Sample Data
contractExpTime.Add(timeStamp.AddMilliseconds(2000));
contractExpTime.Add(timeStamp.AddMilliseconds(3000));
contractExpTime.Add(timeStamp.AddMilliseconds(5000));
//
var total = contractExpTime.Where(item => item > timeStamp).Aggregate<DateTime, double>(0, (current, item) => item.Subtract(timeStamp).TotalMilliseconds + current);
var avg = total / contractExpTime.Count(item => item > timeStamp);
return avg;
}
最后设置项目
static void timer_Elapsed(object sender, ElapsedEventArgs e)
{
//Fetch all items which are not expired yet.
// Set all Expired.
// Save all items.
}