在.net中进行线程化的计时器

时间:2010-08-16 18:47:35

标签: c# .net multithreading timer

我想在新线程中使用计时器tick / elapsed事件。好像我不能使用Windows计时器。但是如果我使用Timers.Timer,它会为每个已发生的事件从线程池创建工作线程。有没有办法让这些事件发生在同一个线程中?

更新

谢谢大家回答这个问题。

我的意图背后可能听起来有点疯狂。我要你纠正我。当我试图解决这个问题时,这是我的想法(作为新手)。我正在尝试每2秒执行一次任务。当我使用Timers.Timer时,它每2秒创建一个线程,我认为这是一个开销。

我的主线程和我的其他线程需要大量处理器时间来执行任务。因此,如果我可以避免创建这些线程,我会保存处理器的任何微秒,以便在每次计时器过去时为我的主线程和其他线程创建一个线程。

我进行了快速测试并比较了几个解决方案。每种情况下间隔1000毫秒。 100蜱。

解决方案1:等待/休眠的无限循环{00:01:42.0068344}

解决方案2:使用Brian的Synchronizer {00:01:42.4068573}

解决方案3:Timers.Timer,因为它是{00:01:42.4018571}

这应该告诉我2.0068344,2.4068573,2.4018571是在背景中浪费其他东西的时间,而不是100个刻度的1000毫秒的时间间隔。这应该意味着当solution1满足您的需求时,它是性能最佳的解决方案?

这也应该意味着虽然Brian的解决方案同步到一个线程,但它实际上是在后台创建线程。

请确认或纠正我。

5 个答案:

答案 0 :(得分:6)

是。您可以使System.Timers.Timer在同一个线程中执行Elapsed事件处理程序。为此,您需要将SynchronizingObject属性设置为一个对象,该对象将执行封送到您选择的线程上。但是,重要的是要意识到您不能只是将执行注入随机线程。接收线程必须专门设计为允许编组方法调用的执行。

public static void Main(string[] args)
{
    var timer = new System.Timers.Timer();
    timer.Interval = 1000;
    timer.Elapsed += 
      (s, a) => 
      { 
        Console.WriteLine("{1} {0}", 
          DateTime.Now, Thread.CurrentThread.ManagedThreadId); 
      };
    timer.SynchronizingObject = new Synchronizer();
    timer.Start();
    Console.ReadLine();
}

以下是Synchronizer类的代码。此代码使用.NET BCL 4.0中的BlockingCollection类。如果您没有使用4.0,那么您可以将其替换为Stephen Toub的BlockingQueue

public class Synchronizer : ISynchronizeInvoke
{
    private Thread m_Thread;
    private BlockingCollection<Message> m_Queue = new BlockingCollection<Message>();

    public Synchronizer()
    {
        m_Thread = new Thread(Run);
        m_Thread.IsBackground = true;
        m_Thread.Start();
    }

    private void Run()
    {
        while (true)
        {
            Message message = m_Queue.Take();
            message.Return = message.Method.DynamicInvoke(message.Args);
            message.Finished.Set();
        }
    }

    public IAsyncResult BeginInvoke(Delegate method, object[] args)
    {
        Message message = new Message();
        message.Method = method;
        message.Args = args;
        m_Queue.Add(message);
        return message;
    }

    public object EndInvoke(IAsyncResult result)
    {
        Message message = result as Message;
        if (message != null)
        {
            message.Finished.WaitOne();
            return message.Return;
        }
        throw new ArgumentException("result");
    }

    public object Invoke(Delegate method, object[] args)
    {
        Message message = new Message();
        message.Method = method;
        message.Args = args;
        m_Queue.Add(message);
        message.Finished.WaitOne();
        return message.Return;
    }

    public bool InvokeRequired
    {
        get { return Thread.CurrentThread != m_Thread; }
    }

    private class Message : IAsyncResult
    {
        public Delegate Method = null;
        public object[] Args = null;
        public object Return = null;
        public object State = null;
        public ManualResetEvent Finished = new ManualResetEvent(false);

        public object AsyncState
        {
            get { return State; }
        }

        public WaitHandle AsyncWaitHandle
        {
            get { return Finished; }
        }

        public bool CompletedSynchronously
        {
            get { return false; }
        }

        public bool IsCompleted
        {
            get { return Finished.WaitOne(0); }
        }
    }
}

<强>更新

您需要了解System.Timers.Timer的工作原理。它实际上在幕后使用System.Threading.Timer并在ThreadPool线程上执行事件处理程序。所以它根本不是真正创建任何线程。来自线程池的相同内容会一次又一次地被回收。没有线程创建开销。这是线程池的重点。

现在,我在这里提供的解决方案仍然使用幕后的ThreadPool,但不是让工作项直接执行Elapsed事件处理程序,而是执行Synchronizer.BeginInvoke方法。将事件处理程序编组到Synchronizer创建的线程上。

因此,我在这里提供的解决方案会更慢,这应该是不可忽视的。就个人而言,我会坚持使用解决方案#1,因为它更容易。在这一点上,我不会担心很多关于性能的问题。

答案 1 :(得分:3)

  

有没有办法让这些事件在同一个线程中发生?

不,这也没有多大意义 一个事件不能只是“闯入”一个线程,线程必须合作。

你可以做的是创建一个反复循环并等待信号量的线程。然后使用任何类型的Timer来触发该信号量。

答案 2 :(得分:2)

我认为提供另一个答案是有益的。原因是因为我的另一个答案太复杂了。如果您想要做的就是确保在同一个线程上定期执行任务,那么在SynchronzingObject实例上设置System.Timers.Timer属性实际上是不必要的。在一个无限循环中开始一个旋转的新线程要容易得多,这个循环执行最初放在Elapsed事件中的内容。

public static void Main(string[] args)
{
    var stop = new ManualResetEvent(false);
    var thread = new Thread(
        () =>
        {
            while (stop.WaitOne(YOUR_INTERVAL_GOES_HERE))
            {
                // The code that was in the Elapsed event handler goes here instead.
            }
        });
    Console.WriteLine("Press ENTER to stop...");
    Console.ReadLine();
    stop.Set();
    thread.Join();
}

答案 3 :(得分:1)

如果您希望在特定线程上执行定时事件,则至少有几个选项:

  • 启动你希望“计时器”运行的线程并让它在计时器持续时间内休眠,然后完成你想要的工作。
  • 让线程等待由计时器(as Henk Holterman suggested)发出信号的事件。这似乎是一种更复杂的方法来实现上述目标,但如果您希望线程能够通过计时器以外的方式唤醒,则可能有意义。

答案 4 :(得分:0)

我建议您创建一个线程来监视由计时器的已用事件处理程序提供的阻塞队列。然后,事件处理程序使用哪个线程并不重要,并且由于它所做的只是将某些内容放入队列中,因此该线程的持续时间将是短暂的。

有关良好的阻塞队列实现,请参阅Marc Gravell的回答:Creating a blocking Queue<T> in .NET?