Выполнение кода по расписанию, и пропуск задачи
Есть код для запуска задач по расписанию:
public class TaskScheduler
{
private static TaskScheduler _instance;
private List<Timer> _timers = new List<Timer>();
private TaskScheduler() { }
public static TaskScheduler Instance => _instance ?? (_instance = new TaskScheduler());
/// <summary>
/// Create new Task
/// </summary>
/// <code>
/// TaskScheduler.Instance.ScheduleTask(
/// ()=>
/// {
///
/// }, new TimeSpan(0, 20, 0));
///
/// TaskScheduler.Instance.ScheduleTask(Action, TimeSpan);
///
/// </code>
/// <param name="task">Action</param>
/// <param name="time"></param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="OverflowException"></exception>
public void ScheduleTask(Action task, TimeSpan time)
{
ScheduleTask(task, time.Hours, time.Minutes, time.Seconds);
}
/// <summary>
/// Create new Task
/// </summary>
/// <code>
/// TaskScheduler.Instance.ScheduleTask(
/// ()=>
/// {
///
/// }, 1, 20, 0);
///
/// TaskScheduler.Instance.ScheduleTask(Action, hour, minute, second);
///
/// </code>
/// <param name="task">Action</param>
/// <param name="hour">Hour</param>
/// <param name="min">Minute</param>
/// <param name="second">Second</param>
/// <param name="intervalInHour">Interval in hours</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="OverflowException"></exception>
public void ScheduleTask(Action task, int hour = 0, int min = 0, int second = 1, double intervalInHour = 24d)
{
var now = DateTime.Now;
var firstRun = new DateTime(now.Year, now.Month, now.Day, hour, min, second);
if (now > firstRun) firstRun = firstRun.AddDays(1);
var timeToGo = firstRun - now;
if (timeToGo <= TimeSpan.Zero) timeToGo = TimeSpan.Zero;
var timer = new Timer(x =>
{
task.Invoke();
}, null, timeToGo, TimeSpan.FromHours(intervalInHour));
_timers.Add(timer);
}
}
Работает великолепно, задачи запускаются, работают.
Есть некий нюанс, есть код который копирует директорию в директорию.
public static class FolderCopper
{
public static void CopyAll(DirectoryInfo source, DirectoryInfo target, CancellationToken token)
{
if (!source.Exists) return;
if (!target.Exists) target.Create();
var po = new ParallelOptions
{
CancellationToken = token
};
Parallel.ForEach(source.GetDirectories(), po, (sourceChildDirectory) =>
{
if (po.CancellationToken.IsCancellationRequested)
{
po.CancellationToken.ThrowIfCancellationRequested();
}
CopyAll(sourceChildDirectory, new DirectoryInfo(Path.Combine(target.FullName, sourceChildDirectory.Name)), token);
});
Parallel.ForEach(source.GetFiles(), po, sourceFile =>
{
if (po.CancellationToken.IsCancellationRequested)
{
po.CancellationToken.ThrowIfCancellationRequested();
}
var file = new FileInfo(Path.Combine(target.FullName, sourceFile.Name));
switch (file.Exists)
{
case false:
sourceFile.CopyTo(file.FullName);
break;
case true when file.LastWriteTimeUtc < sourceFile.LastWriteTimeUtc:
sourceFile.CopyTo(file.FullName, true);
break;
}
});
}
}
Его необходимо запускать например раз в 20 или в 10 минут.
Вызываю таким образом:
Utils.TaskScheduler.Instance.ScheduleTask(() => {}, 0, 20, 0, 0.33); //Задача, часы, минуты, секунды, интервал в часах 20 минут - 0.33.
Временами бывает что задача выполняется дольше чем 20 минут, и он запускает 2 задачу, и тд...
Как переписать код, чтобы он не запускал ещё 1 экземпляр, если предыдущий не отработал, и просто пропустить выполнение задачи?
Ответы (3 шт):
Храните рядом с Action флаги состояния (ожидает запуска, в процессе выполнения), и перед вызовом task.Invoke(); проверяйте предыдущее состояние. Например:
public class TaskScheduler
{
...
bool State = false;
}
var timer = new Timer(x =>
{
if (State == false)
{
State = true;
task.Invoke();
State = false;
}
}, null, timeToGo, TimeSpan.FromHours(intervalInHour));
Как вариант, вы можете написать класс задачи, которая не допускает параллельных запусков, пример
public class MyTimedTask
{
private Timer _timer;
private Action _toInvoke;
private TimeSpan _interval;
public MyTimedTask(Action toInvoke, TimeSpan interval)
{
if (toInvoke == null) throw new ArgumentException(nameof(toInvoke));
this._toInvoke = toInvoke;
this._interval = interval;
}
public void Start()
{
this.Stop();
_timer = new Timer(Callback, null, _interval, Timeout.InfiniteTimeSpan);
}
private void Callback(object state)
{
try
{
Console.WriteLine();
Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking from timer start");
this._toInvoke();
Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking from timer end");
}
finally
{
// тут можно расчитать время до следующего запуска по вашему алгоритму
_timer?.Change(_interval, Timeout.InfiniteTimeSpan);
}
}
public void Stop()
{
var local = _timer;
_timer = null;
local?.Dispose();
}
}
Шедулер может выглядеть как то так
public class MyScheduler
{
private List<MyTimedTask> _tasks = new List<MyTimedTask>();
public void Schedule(Action action, TimeSpan interval)
{
var task = new MyTimedTask(action, interval);
_tasks.Add(task);
task.Start();
}
public void Stop()
{
_tasks.ForEach(t => t.Stop());
_tasks.Clear();
}
}
Проверка (тут интервал 2 секунды, а задача занимает 5)
void Main()
{
var scheduler = new MyScheduler();
scheduler.Schedule(SomeTask, TimeSpan.FromSeconds(2));
Console.ReadLine();
scheduler.Stop();
}
private void SomeTask()
{
Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking SomeTask start");
Thread.Sleep(5000);
Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking SomeTask end");
}
Результат
7:11:23 AM Invoking from timer start
7:11:23 AM Invoking SomeTask start
7:11:28 AM Invoking SomeTask end
7:11:28 AM Invoking from timer end
7:11:30 AM Invoking from timer start
7:11:30 AM Invoking SomeTask start
7:11:35 AM Invoking SomeTask end
7:11:35 AM Invoking from timer end
7:11:37 AM Invoking from timer start
7:11:37 AM Invoking SomeTask start
7:11:42 AM Invoking SomeTask end
7:11:42 AM Invoking from timer end
Для простоты я при перезапуске таймера не считаю интервал до ближайшего запуска, а просто переиспользую интервал. Таким образом интервал является интервалом между запусками работы. Вы можете любую логику туда положить на свой вкус.
Альтернатива моему первому ответу, мы может переписать код класса, который запускает задачу, на использование монитора и его попытку перезахвата. Код я тупо скопипастил с документации
public class MyTimedTask2
{
private Timer _timer;
private Action _toInvoke;
private TimeSpan _interval;
private object _locker = new Object();
public MyTimedTask2(Action toInvoke, TimeSpan interval)
{
if (toInvoke == null) throw new ArgumentException(nameof(toInvoke));
this._toInvoke = toInvoke;
this._interval = interval;
}
public void Start()
{
this.Stop();
_timer = new Timer(Callback, null, _interval, _interval);
}
private void Callback(object state)
{
var timeout = TimeSpan.FromMilliseconds(500);
Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} TryEnter attempt");
if (Monitor.TryEnter(_locker, timeout))
{
try
{
Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} TryEnter sucessed");
Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking from timer start");
this._toInvoke();
Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking from timer end");
}
finally
{
Monitor.Exit(_locker);
}
}
else
{
Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} TryEnter failed");
}
}
public void Stop()
{
var local = _timer;
_timer = null;
local?.Dispose();
}
}
осталось только поправить шедулер
public class MyScheduler
{
private List<MyTimedTask2> _tasks = new List<MyTimedTask2>();
public void Schedule(Action action, TimeSpan interval)
{
var task = new MyTimedTask2(action, interval);
_tasks.Add(task);
task.Start();
}
public void Stop()
{
_tasks.ForEach(t => t.Stop());
_tasks.Clear();
}
}
Код проверки идентичен
void Main()
{
var scheduler = new MyScheduler();
scheduler.Schedule(SomeTask, TimeSpan.FromSeconds(2));
Console.ReadLine();
scheduler.Stop();
}
private void SomeTask()
{
Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking SomeTask start");
Thread.Sleep(5000);
Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking SomeTask end");
}
В выводе можно видеть удачные и неудачные попытки захвата монитора
7:22:04 AM TryEnter attempt
7:22:04 AM TryEnter sucessed
7:22:04 AM Invoking from timer start
7:22:04 AM Invoking SomeTask start
7:22:06 AM TryEnter attempt
7:22:07 AM TryEnter failed
7:22:08 AM TryEnter attempt
7:22:09 AM TryEnter failed
7:22:09 AM Invoking SomeTask end
7:22:09 AM Invoking from timer end
7:22:10 AM TryEnter attempt
7:22:10 AM TryEnter sucessed
7:22:10 AM Invoking from timer start
7:22:10 AM Invoking SomeTask start
7:22:12 AM TryEnter attempt
7:22:13 AM TryEnter failed
7:22:14 AM TryEnter attempt
7:22:15 AM TryEnter failed
7:22:15 AM Invoking SomeTask end
7:22:15 AM Invoking from timer end
7:22:16 AM TryEnter attempt
7:22:16 AM TryEnter sucessed
7:22:16 AM Invoking from timer start
7:22:16 AM Invoking SomeTask start
7:22:18 AM TryEnter attempt
7:22:19 AM TryEnter failed
7:22:20 AM TryEnter attempt
7:22:21 AM TryEnter failed
7:22:21 AM Invoking SomeTask end
7:22:21 AM Invoking from timer end
7:22:22 AM TryEnter attempt
7:22:22 AM TryEnter sucessed
7:22:22 AM Invoking from timer start
7:22:22 AM Invoking SomeTask start
7:22:27 AM Invoking SomeTask end
7:22:27 AM Invoking from timer end
в этом случае интервал уже не зависит от времени выполнения самого кода метода SomeTask().