单纯使用
Timer
是无法解决问题的(因为它会按照固定时间间隔触发事件,无论之前分派的进程是否已完成)。被分派的进程所需执行的时间变化很大 - 有时可能只需一秒钟,而有时可能需要数小时。如果进程仍在处理上次启动时的任务,则不希望重新启动进程。
有人能提供一些有效的 C# 示例代码吗?
我认为在这种情况下,可以使用 System.ComponentModel.BackgroundWorker
类,然后每次想要分派(或不分派)新线程时,简单地检查它的 IsBusy
属性。 代码非常简单; 这是一个例子:
class MyClass
{
private BackgroundWorker worker;
public MyClass()
{
worker = new BackgroundWorker();
worker.DoWork += worker_DoWork;
Timer timer = new Timer(1000);
timer.Elapsed += timer_Elapsed;
timer.Start();
}
void timer_Elapsed(object sender, ElapsedEventArgs e)
{
if(!worker.IsBusy)
worker.RunWorkerAsync();
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
//whatever You want the background thread to do...
}
}
在这个例子中,我使用了 System.Timers.Timer
,但我认为它也适用于其他定时器。BackgroundWorker
类还支持进度报告和取消操作,并使用基于事件驱动的通信模型与调度线程进行通信,因此您不必担心易变的变量等问题...。
编辑
这里是更详细的示例,包括取消和进度报告:
class MyClass
{
private BackgroundWorker worker;
public MyClass()
{
worker = new BackgroundWorker()
{
WorkerSupportsCancellation = true,
WorkerReportsProgress = true
};
worker.DoWork += worker_DoWork;
worker.ProgressChanged += worker_ProgressChanged;
worker.RunWorkerCompleted += worker_RunWorkerCompleted;
Timer timer = new Timer(1000);
timer.Elapsed += timer_Elapsed;
timer.Start();
}
void timer_Elapsed(object sender, ElapsedEventArgs e)
{
if(!worker.IsBusy)
worker.RunWorkerAsync();
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker w = (BackgroundWorker)sender;
while(/*condition*/)
{
//check if cancellation was requested
if(w.CancellationPending)
{
//take any necessary action upon cancelling (rollback, etc.)
//notify the RunWorkerCompleted event handler
//that the operation was cancelled
e.Cancel = true;
return;
}
//report progress; this method has an overload which can also take
//custom object (usually representing state) as an argument
w.ReportProgress(/*percentage*/);
//do whatever You want the background thread to do...
}
}
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
//display the progress using e.ProgressPercentage and/or e.UserState
}
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if(e.Cancelled)
{
//do something
}
else
{
//do something else
}
}
}
然后,为了取消进一步的执行,只需调用worker.CancelAsync()
。请注意,这是完全由用户处理的取消机制(它不支持线程中止或任何类似的功能)。
BackgroundWorker
,从未遇到任何问题。 - Grx70BackgroundWorker
来实现它。 - Grx70worker_DoWork()
方法已经具有BackgroundWorker
实例。这是通过sender
参数传递的对象引用。您可以在作者刚刚添加的编辑代码示例中看到这一点。如果您只是要使用它的状态,我认为CancellationToken
并没有比普通的volatile bool
字段更有用。当处理Task
对象的取消时,它更加有用。 - Peter Dunihowhile
- 这只是一个示例,您也可以使用for
,foreach
或任何其他类型的迭代。 - Grx70你可以通过维护一个易变的布尔型变量来实现你所要求的:
private volatile bool _executing;
private void TimerElapsed(object state)
{
if (_executing)
return;
_executing = true;
try
{
// do the real work here
}
catch (Exception e)
{
// handle your error
}
finally
{
_executing = false;
}
}
volatile
来防止并发的方法。最好的做法是使用lock
或Interlock
,但是由于时间间隔(几分钟),这种方法足够安全。 - Paul Turnerpublic void TimerElapsed(object sender, EventArgs e)
{
_timer.Stop();
//Do Work
_timer.Start();
}
您可以使用System.Threading.Timer
,并在处理数据/方法之前将Timeout
设置为Infinite
,然后当它完成时重新启动Timer
以准备下一次调用。
private System.Threading.Timer _timerThread;
private int _period = 2000;
public MainWindow()
{
InitializeComponent();
_timerThread = new System.Threading.Timer((o) =>
{
// Stop the timer;
_timerThread.Change(-1, -1);
// Process your data
ProcessData();
// start timer again (BeginTime, Interval)
_timerThread.Change(_period, _period);
}, null, 0, _period);
}
private void ProcessData()
{
// do stuff;
}
ProcessData();
会在单独的线程中执行(而不是UI线程)吗? - Ajendra Prasadasync
/await
实现单个方法中的定时机制,c) 引用的实现相当复杂,有些混淆了与此特定问题相关的关键点。Task
和async
/await
来实现周期性行为,因为它可以简化代码。尤其是async
/await
特性在处理回调实现细节时非常有价值,将自然、线性逻辑保留在单个方法中。但是没有一个答案展示这种简洁性。因此,在这个理由的推动下,我要添加另一个答案到这个问题...
对我来说,首先要考虑的是“需要的确切行为是什么?” 这里的问题以一个基本前提开始:即定时器启动的周期任务不应在并发情况下运行,即使该任务比定时器的周期时间更长。但是有多种方式可以满足这个前提条件,包括:
根据评论,我有印象#3选项最接近OP的原始请求,虽然#1选项也可能有效。但对其他人来说,#2和#4选项可能更可取。
在下面的代码示例中,我使用了五种不同的方法来实现这些选项(其中两种实现了选项#3,但方式略有不同)。当然,人们会根据自己的需求选择适当的实现方式。您可能不需要在一个程序中使用所有五种方法! :)class Program
{
const int timerSeconds = 5, actionMinSeconds = 1, actionMaxSeconds = 7;
static Random _rnd = new Random();
static void Main(string[] args)
{
Console.WriteLine("Press any key to interrupt timer and exit...");
Console.WriteLine();
CancellationTokenSource cancelSource = new CancellationTokenSource();
new Thread(() => CancelOnInput(cancelSource)).Start();
Console.WriteLine(
"Starting at {0:HH:mm:ss.f}, timer interval is {1} seconds",
DateTime.Now, timerSeconds);
Console.WriteLine();
Console.WriteLine();
// NOTE: the call to Wait() is for the purpose of this
// specific demonstration in a console program. One does
// not normally use a blocking wait like this for asynchronous
// operations.
// Specify the specific implementation to test by providing the method
// name as the second argument.
RunTimer(cancelSource.Token, M1).Wait();
}
static async Task RunTimer(
CancellationToken cancelToken, Func<Action, TimeSpan, Task> timerMethod)
{
Console.WriteLine("Testing method {0}()", timerMethod.Method.Name);
Console.WriteLine();
try
{
await timerMethod(() =>
{
cancelToken.ThrowIfCancellationRequested();
DummyAction();
}, TimeSpan.FromSeconds(timerSeconds));
}
catch (OperationCanceledException)
{
Console.WriteLine();
Console.WriteLine("Operation cancelled");
}
}
static void CancelOnInput(CancellationTokenSource cancelSource)
{
Console.ReadKey();
cancelSource.Cancel();
}
static void DummyAction()
{
int duration = _rnd.Next(actionMinSeconds, actionMaxSeconds + 1);
Console.WriteLine("dummy action: {0} seconds", duration);
Console.Write(" start: {0:HH:mm:ss.f}", DateTime.Now);
Thread.Sleep(TimeSpan.FromSeconds(duration));
Console.WriteLine(" - end: {0:HH:mm:ss.f}", DateTime.Now);
}
static async Task M1(Action taskAction, TimeSpan timer)
{
// Most basic: always wait specified duration between
// each execution of taskAction
while (true)
{
await Task.Delay(timer);
await Task.Run(() => taskAction());
}
}
static async Task M2(Action taskAction, TimeSpan timer)
{
// Simple: wait for specified interval, minus the duration of
// the execution of taskAction. Run taskAction immediately if
// the previous execution too longer than timer.
TimeSpan remainingDelay = timer;
while (true)
{
if (remainingDelay > TimeSpan.Zero)
{
await Task.Delay(remainingDelay);
}
Stopwatch sw = Stopwatch.StartNew();
await Task.Run(() => taskAction());
remainingDelay = timer - sw.Elapsed;
}
}
static async Task M3a(Action taskAction, TimeSpan timer)
{
// More complicated: only start action on time intervals that
// are multiples of the specified timer interval. If execution
// of taskAction takes longer than the specified timer interval,
// wait until next multiple.
// NOTE: this implementation may drift over time relative to the
// initial start time, as it considers only the time for the executed
// action and there is a small amount of overhead in the loop. See
// M3b() for an implementation that always executes on multiples of
// the interval relative to the original start time.
TimeSpan remainingDelay = timer;
while (true)
{
await Task.Delay(remainingDelay);
Stopwatch sw = Stopwatch.StartNew();
await Task.Run(() => taskAction());
long remainder = sw.Elapsed.Ticks % timer.Ticks;
remainingDelay = TimeSpan.FromTicks(timer.Ticks - remainder);
}
}
static async Task M3b(Action taskAction, TimeSpan timer)
{
// More complicated: only start action on time intervals that
// are multiples of the specified timer interval. If execution
// of taskAction takes longer than the specified timer interval,
// wait until next multiple.
// NOTE: this implementation computes the intervals based on the
// original start time of the loop, and thus will not drift over
// time (not counting any drift that exists in the computer's clock
// itself).
TimeSpan remainingDelay = timer;
Stopwatch swTotal = Stopwatch.StartNew();
while (true)
{
await Task.Delay(remainingDelay);
await Task.Run(() => taskAction());
long remainder = swTotal.Elapsed.Ticks % timer.Ticks;
remainingDelay = TimeSpan.FromTicks(timer.Ticks - remainder);
}
}
static async Task M4(Action taskAction, TimeSpan timer)
{
// More complicated: this implementation is very different from
// the others, in that while each execution of the task action
// is serialized, they are effectively queued. In all of the others,
// if the task is executing when a timer tick would have happened,
// the execution for that tick is simply ignored. But here, each time
// the timer would have ticked, the task action will be executed.
//
// If the task action takes longer than the timer for an extended
// period of time, it will repeatedly execute. If and when it
// "catches up" (which it can do only if it then eventually
// executes more quickly than the timer period for some number
// of iterations), it reverts to the "execute on a fixed
// interval" behavior.
TimeSpan nextTick = timer;
Stopwatch swTotal = Stopwatch.StartNew();
while (true)
{
TimeSpan remainingDelay = nextTick - swTotal.Elapsed;
if (remainingDelay > TimeSpan.Zero)
{
await Task.Delay(remainingDelay);
}
await Task.Run(() => taskAction());
nextTick += timer;
}
}
}
最后一点说明:我在跟踪一个重复问题时找到了这个Q&A。在那个问题中,与此处不同,OP明确指出他们正在使用System.Windows.Forms.Timer
类。当然,这个类主要用于其具有在UI线程中引发Tick
事件的好特性。
现在,无论是这里还是那个问题都涉及到在后台线程中实际执行任务,因此该计时器类的UI线程亲和性行为在这些情况下并没有特别的用处。这里的代码是实现匹配“启动后台任务”范例的,但它很容易被更改,使得taskAction
委托直接被调用,而不是在Task
中运行并等待。使用async
/await
的好处,除了上面提到的结构优势之外,还在于它保留了从System.Windows.Forms.Timer
类中所需的线程亲和性行为。
async
/await
的实现方式更好。在我看来,它是一个很好的、现代化的替代方案,可以取代老旧而可靠的System.Windows.Forms.Timer
。 :) - Peter DunihoCancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
Task task = PeriodicTaskFactory.Start(() =>
{
Console.WriteLine(DateTime.Now);
Thread.Sleep(5000);
}, intervalInMilliseconds: 1000, synchronous: true, cancelToken: cancellationTokenSource.Token);
Console.WriteLine("Press any key to stop iterations...");
Console.ReadKey(true);
cancellationTokenSource.Cancel();
Console.WriteLine("Waiting for the task to complete...");
Task.WaitAny(task);
synchronous: true
实现的。Press any key to stop iterations...
9/6/2013 1:01:52 PM
9/6/2013 1:01:58 PM
9/6/2013 1:02:04 PM
9/6/2013 1:02:10 PM
9/6/2013 1:02:16 PM
Waiting for the task to complete...
Press any key to continue . . .
更新
如果您想要使用PeriodicTaskFactory的“跳过事件”行为,只需不使用同步选项并实现类似Bob在这里https://dev59.com/n2cs5IYBdhLWcg3wynDD#18665948所做的Monitor.TryEnter。
Task task = PeriodicTaskFactory.Start(() =>
{
if (!Monitor.TryEnter(_locker)) { return; } // Don't let multiple threads in here at the same time.
try
{
Console.WriteLine(DateTime.Now);
Thread.Sleep(5000);
}
finally
{
Monitor.Exit(_locker);
}
}, intervalInMilliseconds: 1000, synchronous: false, cancelToken: cancellationTokenSource.Token);
< p> PeriodicTaskFactory 的好处在于返回一个可用于所有TPL API的任务,例如 Task.Wait ,延续等。
在任务完成之前,您可以停止计时器,然后在任务完成后重新启动它,这可以使您的任务按照固定时间间隔周期性执行。
public void myTimer_Elapsed(object sender, EventArgs e)
{
myTimer.Stop();
// Do something you want here.
myTimer.Start();
}
有至少20种不同的方法可以完成这个任务,从使用定时器和信号量,到使用易失性变量,再到使用TPL,或者使用像Quartz这样的开源调度工具等等。
创建线程是一个昂贵的操作,所以为什么不只创建一个并让它在后台运行呢?由于它会在大部分时间处于空闲状态,因此对系统没有实际影响。周期性地唤醒它来执行任务,然后在一段时间后让它进入睡眠状态。无论任务需要多长时间,完成后都要等待“waitForWork”时间段后才能开始下一个任务。
//wait 5 seconds for testing purposes
static TimeSpan waitForWork = new TimeSpan(0, 0, 0, 5, 0);
static ManualResetEventSlim shutdownEvent = new ManualResetEventSlim(false);
static void Main(string[] args)
{
System.Threading.Thread thread = new Thread(DoWork);
thread.Name = "My Worker Thread, Dude";
thread.Start();
Console.ReadLine();
shutdownEvent.Set();
thread.Join();
}
public static void DoWork()
{
do
{
//wait for work timeout or shudown event notification
shutdownEvent.Wait(waitForWork);
//if shutting down, exit the thread
if(shutdownEvent.IsSet)
return;
//TODO: Do Work here
} while (true);
}
class Program
{
static System.Threading.Timer timer;
static bool workAvailable = false;
static int timeInMs = 5000;
static object o = new object();
static void Main(string[] args)
{
timer = new Timer((o) =>
{
try
{
if (workAvailable)
{
// do the work, whatever is required.
// if another thread is started use Thread.Join to wait for the thread to finish
}
}
catch (Exception)
{
// handle
}
finally
{
// only set the initial time, do not set the recurring time
timer.Change(timeInMs, Timeout.Infinite);
}
});
// only set the initial time, do not set the recurring time
timer.Change(timeInMs, Timeout.Infinite);
}
Monitor.TryEnter()
?如果在上一个线程完成之前再次调用 OnTimerElapsed()
,它将被丢弃,并且另一次尝试将不会再发生,直到计时器再次触发。private static readonly object _locker = new object();
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
if (!Monitor.TryEnter(_locker)) { return; } // Don't let multiple threads in here at the same time.
try
{
// do stuff
}
finally
{
Monitor.Exit(_locker);
}
}
System.Timers.Timer
,Windows.Forms.Timer
,System.Threading.Timer
? - Peter Ritchie