Запуск задач по расписанию в .NET и C#

Недавно, на новом для себя проекте я в очередной раз столкнулся с проблемой запуска периодических заданий по расписанию и решил расписать все варианты решения этой задачи из тех, что мне встречались.

Для того, чтобы задача не выглядела такой размытой скажу, что необходимо было запускать рассылку разных писем через разные промежутки времени.

Средства .NET Framwork

Thread.Sleep

Решить эту задачу можно и не прибегая к дополнительным утилитам и сторонним библиотекам.

Первое что приходит в голову, когда надо сделать запуск рассылки через каждые, например, 20 минут - это использование Thread.Sleep. Это очень простое решение, которое отлично работает и не требует лишних телодвижений. К слову именно такой способ полгода использовался на проекте для рассылки примерно половины писем. Но у такого решения есть свои нюансы - например, если рассылка выполняется 5 минут, то интервал между первым и вторым запуском отправки писем, будет не 20, а 25 минут, а если каждый раз рассылается разное количество писем, то прогнозировать интервалы между запусками становится невозможным. Кроме того, время запуска зависит от времени старта процесса и задать определенное значение, например 12:30, становится невозможным.

while (true)
{
    //Выполняем рассылку
   Thread.Sleep(TimeSpan.FromMinutes(20));
}

Избежать плавающих интервалов можно запуская рассылку в отдельном потоке, но лучше сразу перейти к следующему способу.

Timer

Следующий способ, который почти так же прост - это использование класса Timer. Позволяет запускать задачи через фиксированные промежутки времени. Задачи запускаются в отдельном потоке, так что интервал всегда один и тот же. Запускать в определенное время по-прежнему невозможно.

Код взят со stackoverflow.com

const double interval60Minutes = 60 * 60 * 1000; // milliseconds to one hour
Timer checkForTime = new Timer(interval60Minutes);
checkForTime.Elapsed += new ElapsedEventHandler(checkForTime_Elapsed);
checkForTime.Enabled = true;

void checkForTime_Elapsed(object sender, ElapsedEventArgs e)
{
    if (timeIsReady())
    {
       SendEmail();
    }
}

Средства ОС

Windows Task Scheduler

Встроенное в Windows средство для запуска процессов по расписанию. Отличный способ, именно с его помощью рассылалась вторая половина писем - те, интервал у которых достаточно большой, чтобы висеть всё время в тред-слипе (например еженедельная рассылка). Руководства по настройке можно легко найти с помощью любимой поисковой системы (вот одно из них)

У этого способа недостатки уже другие - планировщик умеет запускать процессы, а не методы внутри них, как предыдущие два способа. Это значит, что для каждого типа рассылки нужен свой .exe, или, если запихать это все в один .exe, то свой ключ, передаваемый в аргументах запуска. Кроме того, и это уже значительно более важно, расписание запуска хранится в винде на определенной машине, и уже невозможно просто взять и скопировать бинарники с конфигом с одного сервера на другой. Еще один минус, который может проявиться при большом количестве "легких" задач - создание нового процесса на каждый запуск, гораздо тяжелее дается системе и отнимает больше системных ресурсов, чем создание нового треда.

Сторонние библиотеки

Quartz.NET

Java-библиотека, портированная на C#. По заявлению авторов подходит как для маленьких, так и для больших корпоративных систем. Для задания расписания использует формат cron, да ещё и расширенную версию, позволяя гибко задавать расписание с точностью до секунды.

Очень мощное средство, которое не просто позволяет запускать по расписанию задачи, но и умеет распределять нагрузку, обладает средствами для отказоустойчивости и поддерживает сохранение джобов в БД (через ADO.NET, но есть другие адаптеры, например, для MongoDB).

Несмотря на все плюсы, чувствуется немного чуждый Java-стиль, разобраться с использованием, не так уж просто, хотя есть документация. Некоторые действия требуют написания кода, который на первый взгляд кажется лишним, а как инжектировать в джобы зависимости с помощью IoC контейнера я так и не разобрался до конца, сделав в конце-концов простую передачу объекта-контейнера в контекст джоба, для того, чтобы при выполнении резолвить зависимости уже изнутри. Для запуска джобов использует собственный пул потоков, что тоже вносит некоторую сложность в понимании работы.

Несмотря на то, что этот фреймворк успешно использовался мною на предыдущем проекте (правда в виде - написал код запуска один раз и забыл), возится с ним не очень хотелось и было желание найти что-то подобное, но более простое, легковесное и без ненужных в данном случае функций, вроде Job Persistence.

NCron

И решение было найдено!

Да у него есть свои подводные камни и перед запуском приходится попрыгать с бубном, но в конце концов этот способ выглядит проще.

var schedulingService = new SchedulingService();
schedulingService.At("0 12 * * 1").Run<TestCronJob>();    
schedulingService.Start();

Для задания расписания используется обычный крон. Поэтому строка "0 12 * * 1" означает что джоб будет запускаться каждый понедельник в 12 часов. Более подробно можно посмотреть в википедии

Кстати, еще при использовании Quartz мы столкнулись с тем, что windows программисты плохо понимают синтаксис cron (мы частенько подходили к Linux-админу чтобы что-нибудь уточнить). Поэтому в новом проекте добавился тест с использованием библиотеки NCrontab, который показывает как интерпретируется заданное расписание. NCron и сам внутри использует NCrontab для парсинга расписания, хотя это не совсем очевидно, потому что сборка NCrontab'a интегрируется внутрь с помощью ILMerge.

Сам тест:

[Test]
public void ParseCronFormatTest()
{
	var crontabSchedule = NCrontab.CrontabSchedule.Parse("0 12 * * 1");
	foreach (var time in crontabSchedule.GetNextOccurrences(DateTime.UtcNow,DateTime.UtcNow.AddMonths(3)))
	{
		Console.WriteLine(time);
	}
}

Результат выполнения

enter image description here

Итого:


comments powered by HyperComments
Яндекс.Метрика