是否有基于任务的替代 System.Threading.Timer?

我是新来的。Net 4.0的任务,我无法找到我认为是基于任务的定时器的替代品或实现,例如定期任务。有这种东西吗?

更新 我想出了一个解决我需要的解决方案,就是将“ Timer”功能封装在一个 Task 中,其中包含子任务,所有这些子任务都利用了 CancelationToken,并返回 Task,以便能够参与进一步的 Task 步骤。

public static Task StartPeriodicTask(Action action, int intervalInMilliseconds, int delayInMilliseconds, CancellationToken cancelToken)
{
Action wrapperAction = () =>
{
if (cancelToken.IsCancellationRequested) { return; }


action();
};


Action mainAction = () =>
{
TaskCreationOptions attachedToParent = TaskCreationOptions.AttachedToParent;


if (cancelToken.IsCancellationRequested) { return; }


if (delayInMilliseconds > 0)
Thread.Sleep(delayInMilliseconds);


while (true)
{
if (cancelToken.IsCancellationRequested) { break; }


Task.Factory.StartNew(wrapperAction, cancelToken, attachedToParent, TaskScheduler.Current);


if (cancelToken.IsCancellationRequested || intervalInMilliseconds == Timeout.Infinite) { break; }


Thread.Sleep(intervalInMilliseconds);
}
};


return Task.Factory.StartNew(mainAction, cancelToken);
}
77399 次浏览

It's not exactly in System.Threading.Tasks, but Observable.Timer (or simpler Observable.Interval) from Reactive Extensions library is probably what you're looking for.

UPDATE I am marking the answer below as the "answer" since this is old enough now that we should be using the async/await pattern. No need to downvote this anymore. LOL


As Amy answered, there is no Tasked based periodic/timer implementation. However, based upon my original UPDATE, we have evolved this into something quite useful and production tested. Thought I would share:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;


namespace ConsoleApplication7
{
class Program
{
static void Main(string[] args)
{
Task perdiodicTask = PeriodicTaskFactory.Start(() =>
{
Console.WriteLine(DateTime.Now);
}, intervalInMilliseconds: 2000, // fire every two seconds...
maxIterations: 10);           // for a total of 10 iterations...


perdiodicTask.ContinueWith(_ =>
{
Console.WriteLine("Finished!");
}).Wait();
}
}


/// <summary>
/// Factory class to create a periodic Task to simulate a <see cref="System.Threading.Timer"/> using <see cref="Task">Tasks.</see>
/// </summary>
public static class PeriodicTaskFactory
{
/// <summary>
/// Starts the periodic task.
/// </summary>
/// <param name="action">The action.</param>
/// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
/// <param name="delayInMilliseconds">The delay in milliseconds, i.e. how long it waits to kick off the timer.</param>
/// <param name="duration">The duration.
/// <example>If the duration is set to 10 seconds, the maximum time this task is allowed to run is 10 seconds.</example></param>
/// <param name="maxIterations">The max iterations.</param>
/// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
/// is included in the total duration of the Task.</param>
/// <param name="cancelToken">The cancel token.</param>
/// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create the task for executing the <see cref="Action"/>.</param>
/// <returns>A <see cref="Task"/></returns>
/// <remarks>
/// Exceptions that occur in the <paramref name="action"/> need to be handled in the action itself. These exceptions will not be
/// bubbled up to the periodic task.
/// </remarks>
public static Task Start(Action action,
int intervalInMilliseconds = Timeout.Infinite,
int delayInMilliseconds = 0,
int duration = Timeout.Infinite,
int maxIterations = -1,
bool synchronous = false,
CancellationToken cancelToken = new CancellationToken(),
TaskCreationOptions periodicTaskCreationOptions = TaskCreationOptions.None)
{
Stopwatch stopWatch = new Stopwatch();
Action wrapperAction = () =>
{
CheckIfCancelled(cancelToken);
action();
};


Action mainAction = () =>
{
MainPeriodicTaskAction(intervalInMilliseconds, delayInMilliseconds, duration, maxIterations, cancelToken, stopWatch, synchronous, wrapperAction, periodicTaskCreationOptions);
};


return Task.Factory.StartNew(mainAction, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}


/// <summary>
/// Mains the periodic task action.
/// </summary>
/// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
/// <param name="delayInMilliseconds">The delay in milliseconds.</param>
/// <param name="duration">The duration.</param>
/// <param name="maxIterations">The max iterations.</param>
/// <param name="cancelToken">The cancel token.</param>
/// <param name="stopWatch">The stop watch.</param>
/// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
/// is included in the total duration of the Task.</param>
/// <param name="wrapperAction">The wrapper action.</param>
/// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create a sub task for executing the <see cref="Action"/>.</param>
private static void MainPeriodicTaskAction(int intervalInMilliseconds,
int delayInMilliseconds,
int duration,
int maxIterations,
CancellationToken cancelToken,
Stopwatch stopWatch,
bool synchronous,
Action wrapperAction,
TaskCreationOptions periodicTaskCreationOptions)
{
TaskCreationOptions subTaskCreationOptions = TaskCreationOptions.AttachedToParent | periodicTaskCreationOptions;


CheckIfCancelled(cancelToken);


if (delayInMilliseconds > 0)
{
Thread.Sleep(delayInMilliseconds);
}


if (maxIterations == 0) { return; }


int iteration = 0;


////////////////////////////////////////////////////////////////////////////
// using a ManualResetEventSlim as it is more efficient in small intervals.
// In the case where longer intervals are used, it will automatically use
// a standard WaitHandle....
// see http://msdn.microsoft.com/en-us/library/vstudio/5hbefs30(v=vs.100).aspx
using (ManualResetEventSlim periodResetEvent = new ManualResetEventSlim(false))
{
////////////////////////////////////////////////////////////
// Main periodic logic. Basically loop through this block
// executing the action
while (true)
{
CheckIfCancelled(cancelToken);


Task subTask = Task.Factory.StartNew(wrapperAction, cancelToken, subTaskCreationOptions, TaskScheduler.Current);


if (synchronous)
{
stopWatch.Start();
try
{
subTask.Wait(cancelToken);
}
catch { /* do not let an errant subtask to kill the periodic task...*/ }
stopWatch.Stop();
}


// use the same Timeout setting as the System.Threading.Timer, infinite timeout will execute only one iteration.
if (intervalInMilliseconds == Timeout.Infinite) { break; }


iteration++;


if (maxIterations > 0 && iteration >= maxIterations) { break; }


try
{
stopWatch.Start();
periodResetEvent.Wait(intervalInMilliseconds, cancelToken);
stopWatch.Stop();
}
finally
{
periodResetEvent.Reset();
}


CheckIfCancelled(cancelToken);


if (duration > 0 && stopWatch.ElapsedMilliseconds >= duration) { break; }
}
}
}


/// <summary>
/// Checks if cancelled.
/// </summary>
/// <param name="cancelToken">The cancel token.</param>
private static void CheckIfCancelled(CancellationToken cancellationToken)
{
if (cancellationToken == null)
throw new ArgumentNullException("cancellationToken");


cancellationToken.ThrowIfCancellationRequested();
}
}
}

Output:

2/18/2013 4:17:13 PM
2/18/2013 4:17:15 PM
2/18/2013 4:17:17 PM
2/18/2013 4:17:19 PM
2/18/2013 4:17:21 PM
2/18/2013 4:17:23 PM
2/18/2013 4:17:25 PM
2/18/2013 4:17:27 PM
2/18/2013 4:17:29 PM
2/18/2013 4:17:31 PM
Finished!
Press any key to continue . . .

It depends on 4.5, but this works.

public class PeriodicTask
{
public static async Task Run(Action action, TimeSpan period, CancellationToken cancellationToken)
{
while(!cancellationToken.IsCancellationRequested)
{
await Task.Delay(period, cancellationToken);


if (!cancellationToken.IsCancellationRequested)
action();
}
}


public static Task Run(Action action, TimeSpan period)
{
return Run(action, period, CancellationToken.None);
}
}

Obviously you could add a generic version that takes arguments as well. This is actually similar to other suggested approaches since under the hood Task.Delay is using a timer expiration as a task completion source.

Until now I used a LongRunning TPL task for cyclic CPU bound background work instead of the threading timer, because:

  • the TPL task supports cancellation
  • the threading timer could start another thread while the programm is shutting down causing possible problems with disposed resources
  • chance for overrun: the threading timer could start another thread while the previous is still being processed due to unexpected long work (I know, it can be prevented by stopping and restarting the timer)

However, the TPL solution always claims a dedicated thread which is not necessary while waiting for the next action (which is most of the time). I would like to use the proposed solution of Jeff to perform CPU bound cyclic work on the background because it only needs a threadpool thread when there is work to do which is better for scalability (especially when the interval period is big).

To achieve that, I would suggest 4 adaptions:

  1. Add ConfigureAwait(false) to the Task.Delay() to execute the doWork action on a thread pool thread, otherwise doWork will be performed on the calling thread which is not the idea of parallelism
  2. Stick to the cancellation pattern by throwing a TaskCanceledException (still required ?)
  3. Forward the CancellationToken to doWork to enable it to cancel the task
  4. Add a parameter of type object to supply task state information (like a TPL task)

About point 2 I'm not sure, does async await still requires the TaskCanceledExecption or is it just best practice?

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
{
do
{
await Task.Delay(period, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
doWork(taskState, cancellationToken);
}
while (true);
}

Please give your comments to the proposed solution...

Update 2016-8-30

The above solution doesn't immediately call doWork() but starts with await Task.Delay().ConfigureAwait(false) to achieve the thread switch for doWork(). The solution below overcomes this problem by wrapping the first doWork() call in a Task.Run() and await it.

Below is the improved async\await replacement for Threading.Timer that performs cancelable cyclic work and is scalable (compared to the TPL solution) because it doesn’t occupy any thread while waiting for the next action.

Note that in contrary with the Timer, the waiting time (period) is constant and not the cycle time; the cycle time is the sum of the waiting time and the duration of doWork() which can vary.

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
{
await Task.Run(() => doWork(taskState, cancellationToken), cancellationToken).ConfigureAwait(false);
do
{
await Task.Delay(period, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
doWork(taskState, cancellationToken);
}
while (true);
}

I ran into a similar problem and wrote a TaskTimer class that returns a seris of tasks that complete on timer: https://github.com/ikriv/tasktimer/.

using (var timer = new TaskTimer(1000).Start())
{
// Call DoStuff() every second
foreach (var task in timer)
{
await task;
DoStuff();
}
}
static class Helper
{
public async static Task ExecuteInterval(Action execute, int millisecond, IWorker worker)
{
while (worker.Worked)
{
execute();


await Task.Delay(millisecond);
}
}
}




interface IWorker
{
bool Worked { get; }
}

Simple...

I needed to trigger the recurring asynchronous tasks from a synchronous method.

public static class PeriodicTask
{
public static async Task Run(
Func<Task> action,
TimeSpan period,
CancellationToken cancellationToken = default(CancellationToken))
{
while (!cancellationToken.IsCancellationRequested)
{


Stopwatch stopwatch = Stopwatch.StartNew();


if (!cancellationToken.IsCancellationRequested)
await action();


stopwatch.Stop();


await Task.Delay(period - stopwatch.Elapsed, cancellationToken);
}
}
}

This is an adaption of Jeff's answer. It is changed to take in a Func<Task> It also makes sure that the period is how often it is run by deducting the task's run time from the period for the next delay.

class Program
{
static void Main(string[] args)
{
PeriodicTask
.Run(GetSomething, TimeSpan.FromSeconds(3))
.GetAwaiter()
.GetResult();
}


static async Task GetSomething()
{
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine($"Hi {DateTime.UtcNow}");
}
}