可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I'm new to .Net 4.0's Tasks and I wasn't able to find what I thought would be a Task based replacement or implementation of a Timer, e.g. a periodic Task. Is there such a thing?
Update
I came up with what I think is a solution to my needs which is to wrap the "Timer" functionality inside a Task with child Tasks all taking advantage of the CancellationToken and returns the Task to be able to participate in further Task steps.
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);
}
回答1:
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.
回答2:
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 . . .
回答3:
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.
回答4:
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:
- 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
- Stick to the cancellation pattern by throwing a TaskCanceledException (still required ?)
- Forward the CancellationToken to
doWork
to enable it to cancel the task
- 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);
}
回答5:
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}");
}
}
回答6:
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();
}
}
回答7:
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...