Чи працюють таймери C # в окремому потоці?


97

Чи System.Timers.Timer працює в окремому потоці, ніж у потоку, який його створив?

Скажімо, у мене є клас із таймером, який спрацьовує кожні 5 секунд. Коли спрацьовує таймер, у минулому методі якийсь об’єкт модифікується. Скажімо, модифікація цього об’єкта займає багато часу, наприклад 10 секунд. Чи можливо, що в цьому сценарії я зіткнусь із колізійними зіткненнями?


Це може призвести до проблем. Зауважте, що загалом потоки пулу потоків не призначені для тривалих процесів.
Greg D

Я стикався з цим, тестуючи за допомогою служби Windows. У мене спрацювало вимкнення таймера як першої інструкції у події OnTimer, виконання моїх завдань, а потім увімкнення таймера в кінці. Це деякий час надійно працювало у виробничих умовах.
Стів

Відповіді:


60

Для System.Timers.Timer :

Дивіться відповідь Брайана Гідеона нижче

Для System.Threading.Timer :

Документація MSDN про таймери стверджує:

Клас System.Threading.Timer робить зворотні виклики в потоці ThreadPool і взагалі не використовує модель події.

Тож справді таймер закінчується іншим потоком.


21
Правда, але це вже зовсім інший клас. OP запитав про клас System.Timers.Timer.
Brian Gideon

1
О, ти маєш рацію. msdn.microsoft.com/en-us/library/system.timers.timer.aspx говорить "Подія Elapsed викликається в потоці ThreadPool." Я припускаю той самий висновок.
Йорен

6
Ну так, але це не зовсім так просто. Дивіться мою відповідь.
Brian Gideon

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: Кожен може це зробити.
Brian Gideon

а якщо у мене є список складного типу (особа) і я хочу провести час всередині кожної людини? Мені потрібно, щоб це працювало в одному потоці (для всіх осіб), тому що якщо він викликає метод від першої особи, другий повинен чекати, поки перший закінчить минулу подію. Чи можу я це зробити?
Леандро Де Мелло Фагундес

4
У кожного ... System.Timers.Timerє два режими роботи. Він може працювати на випадково призначеному потоці пулу потоків АБО він може працювати на будь-якому потоці, що розміщує ISynchronizeInvokeекземпляр. Я не знаю, як це зробити ясніше. System.Threading.Timerмає мало (якщо взагалі щось) спільного з початковим запитанням.
Брайан Гедеон,

@LeandroDeMelloFagundes Чи не можете ви використовувати lockдля цього?
Озкан

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
Додавання a timer.Stop()на початку методу Elapsed події, а потім a timer.Start()в кінці методу Elapsed event, не дозволить зіткнутися події 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 (або елемента керування у формі Windows) забезпечить що код у вашому обробнику подій Elapsed працює в тому самому потоці, у якому був створений екземпляр SynchronizingObject. "

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


Це хороший приклад. Я не знав дотепер, і мені потрібно встановити деякі замки тут і там. Ідеально.
Олару Мірча,

А що, якщо я хочу, щоб таймер запускав подію 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.