Таймеры C # проходят в отдельном потоке?

98

Проходит ли System.Timers.Timer в отдельном потоке, а не в потоке, который его создал?

Допустим, у меня есть класс с таймером, который срабатывает каждые 5 секунд. Когда срабатывает таймер, в прошедшем методе изменяется какой-то объект. Допустим, изменение этого объекта занимает много времени, например 10 секунд. Возможно ли, что я столкнусь с конфликтами потоков в этом сценарии?

user113164
источник
Это могло привести к проблемам. Обратите внимание, что, как правило, потоки пула потоков не предназначены для длительных процессов.
Greg D
Я столкнулся с этим, тестируя с помощью службы Windows. У меня сработало отключение таймера в качестве первой инструкции в событии OnTimer, выполнение моих задач, а затем включение таймера в конце. В течение некоторого времени это надежно работало в производственной среде.
Стив

Ответы:

60

Для System.Timers.Timer :

См . Ответ Брайана Гидеона ниже

Для System.Threading.Timer :

В документации MSDN по таймерам говорится:

Класс System.Threading.Timer выполняет обратные вызовы в потоке ThreadPool и вообще не использует модель событий.

Так что действительно таймер истекает в другом потоке.

Джорен
источник
21
Верно, но это совсем другой класс. ОП спросил о классе System.Timers.Timer.
Брайан Гидеон
1
О, ты прав. msdn.microsoft.com/en-us/library/system.timers.timer.aspx сообщает: «Событие Elapsed возникает в потоке ThreadPool». Полагаю, такой же вывод отсюда.
Joren
6
Ну да, но не все так просто. Смотрите мой ответ.
Брайан Гидеон,
192

Это зависит. System.Timers.TimerИмеет два режима работы.

Если SynchronizingObjectустановлен в ISynchronizeInvokeэкземпляр, Elapsedсобытие будет выполняться в потоке, в котором размещается синхронизирующий объект. Обычно эти ISynchronizeInvokeслучаи не что иное , как обычный старый Controlи Formэкземпляры , что мы все знакомы. В этом случае Elapsedсобытие вызывается в потоке пользовательского интерфейса и ведет себя аналогично System.Windows.Forms.Timer. В противном случае это действительно зависит от конкретного ISynchronizeInvokeэкземпляра, который использовался.

Если SynchronizingObjectимеет значение null, то Elapsedсобытие вызывается в ThreadPoolпотоке и ведет себя аналогично System.Threading.Timer. Фактически, он фактически использует System.Threading.Timerскрытую информацию и выполняет операцию маршалинга после получения обратного вызова таймера, если это необходимо.

Брайан Гидеон
источник
4
Если вы хотите, чтобы обратный вызов таймера выполнялся в новом потоке, следует ли использовать System.Threading.Timerили System.Timers.Timer?
CJ7
1
@ cj7: Любой из них может это сделать.
Брайан Гидеон
а если у меня есть список сложного типа (человек) и я хочу провести время внутри каждого человека? Мне нужно, чтобы это работало в одном потоке (все люди), потому что, если он вызывает метод от первого лица, второй человек должен ждать, пока первый не завершит прошедшее событие. Могу ли я сделать это?
Леандро Де Мелло Фагундес,
4
У каждого ... System.Timers.Timerесть два режима работы. Он может работать в случайно назначенном потоке пула потоков ИЛИ в любом потоке, в котором размещен ISynchronizeInvokeэкземпляр. Я не знаю, как прояснить это. System.Threading.Timerимеет мало общего (если вообще есть) с исходным вопросом.
Брайан Гидеон
@LeandroDeMelloFagundes Разве вы не можете использовать lockдля этого?
Ozkan
24

Каждое прошедшее событие будет запускаться в том же потоке, если предыдущее Elapsed еще не запущено.

Так что он обрабатывает столкновение за вас

попробуйте поместить это в консоль

static void Main(string[] args)
{
    Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
    var timer = new Timer(1000);
    timer.Elapsed += timer_Elapsed;
    timer.Start();
    Console.ReadLine();
}

static void timer_Elapsed(object sender, ElapsedEventArgs e)
{
    Thread.Sleep(2000);
    Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
}

вы получите что-то вроде этого

10
6
12
6
12

где 10 - вызывающий поток, а 6 и 12 запускаются из истекшего события bg. Если удалить Thread.Sleep (2000); вы получите что-то вроде этого

10
6
6
6
6

Поскольку коллизий нет.

Но это по-прежнему оставляет вам проблему. если вы запускаете событие каждые 5 секунд, а редактирование занимает 10 секунд, вам нужна блокировка, чтобы пропустить некоторые изменения.

Саймон
источник
8
Добавление timer.Stop()в начало метода события Elapsed, а затемtimer.Start() в конец метода события Elapsed предотвратит столкновение события Elapsed.
Metro Smurf
4
Вам не нужно ставить timer.Stop (), вам просто нужно определить timer.AutoReset = false; затем вы делаете timer.Start () после обработки события. Я думаю, это лучший способ избежать столкновений.
João Antunes
17

Для System.Timers.Timer в отдельном потоке, если SynchronizingObject не установлен.

    static System.Timers.Timer DummyTimer = null;

    static void Main(string[] args)
    {
        try
        {

            Console.WriteLine("Main Thread Id: " + System.Threading.Thread.CurrentThread.ManagedThreadId);

            DummyTimer = new System.Timers.Timer(1000 * 5); // 5 sec interval
            DummyTimer.Enabled = true;
            DummyTimer.Elapsed += new System.Timers.ElapsedEventHandler(OnDummyTimerFired);
            DummyTimer.AutoReset = true;

            DummyTimer.Start();

            Console.WriteLine("Hit any key to exit");
            Console.ReadLine();
        }
        catch (Exception Ex)
        {
            Console.WriteLine(Ex.Message);
        }

        return;
    }

    static void OnDummyTimerFired(object Sender, System.Timers.ElapsedEventArgs e)
    {
        Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId);
        return;
    }

Вы увидите, сработал ли DummyTimer с интервалом в 5 секунд:

Main Thread Id: 9
   12
   12
   12
   12
   12
   ... 

Итак, как видно, OnDummyTimerFired выполняется в потоке Workers.

Нет, дальнейшее усложнение - если вы уменьшите интервал до 10 мс,

Main Thread Id: 9
   11
   13
   12
   22
   17
   ... 

Это связано с тем, что если предыдущее выполнение OnDummyTimerFired не было выполнено при следующем запуске тика, .NET создаст новый поток для выполнения этой работы.

Еще больше усложняя ситуацию: «Класс System.Timers.Timer обеспечивает простой способ решения этой дилеммы - он предоставляет общедоступное свойство SynchronizingObject. Установка этого свойства в экземпляр Windows Form (или элемент управления в Windows Form) гарантирует что код в вашем обработчике событий Elapsed выполняется в том же потоке, в котором был создан экземпляр SynchronizingObject ".

http://msdn.microsoft.com/en-us/magazine/cc164015.aspx#S2

Тампон.Jat
источник
Это хороший пример. Я не знал до сих пор, и мне нужно установить несколько замков здесь и там. Отлично.
Олару Мирча
А что, если я хочу, чтобы таймер запускал событие Elapsed в основном потоке консольного приложения?
jacktric
13

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

static void timer_Elapsed(object sender, ElapsedEventArgs e)    
{     
   try
   {
      timer.Stop(); 
      Thread.Sleep(2000);        
      Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);    
   }
   finally
   {
     timer.Start();
   }
}
Раджан
источник
+1 Простой и понятный ответ, но это не всегда сработает. Скорее следует использовать свойство lock или SynchronizingObject таймера.
RollerCosta