防止服务多实例化 - 最佳方法是什么?

22

那么,您认为如何防止多个线程同时运行 C# Windows 服务的最佳方法(该服务使用带有 OnElapsed 事件的定时器)?

使用 lock() 还是 mutex

我似乎无法理解 mutex 的概念,但是对于我的情况,使用 lock() 看起来很好用。

我应该花时间学习如何使用 mutex 吗?


当你说“一个服务的多个实例”时,你是指多个进程(即某人启动程序的两个副本),还是指在同一个应用程序中? - user111013
好的,OnElapsed事件将启动一个新线程来执行实际工作的void函数。有时,这项工作完成的时间可能比计时器运行的间隔时间更长。这就是我的意思。 - Francis Ducharme
1
请不要在标题前加上"C# - "等内容,这是标签的作用。 - John Saunders
6个回答

67

将你的计时器设为一次性的,并在已过时间的事件处理程序中重新初始化它。例如,如果你正在使用System.Timers.Timer,则可以像这样初始化:

myTimer.Elapsed = timer1Elapsed;
myTimer.Interval = 1000; // every second
myTimer.AutoReset = false; // makes it fire only once
myTimer.Enabled = true;

还有你的经过时间事件处理程序:

void timerElapsed(object source, ElapsedEventArgs e)
{
    // do whatever needs to be done
    myTimer.Start(); // re-enables the timer
}
这样做的缺点是计时器不会在每秒钟的间隔上触发,而是在上一次处理完成后一秒钟触发。

你能用 System.Threading.Timer 来做这个吗?(没有设置 AutoReset = false 的可能性) - flagg19
2
@flagg19:如果您使用System.Threading.Timer,则会将最后一个参数初始化为Timeout.Infinite。并且在回调中,您将调用带有最后一个参数等于Timeout.InfiniteChange函数。 - Jim Mischel
2
你可以在计时器开始时使用秒表来测量经过的时间。然后根据经过的时间进行延迟,以获得一个相当平均的计时器。如果你的任务所需的时间比计时器之间的期望长度更长,那么你的延迟将为0。如果它花费了半秒钟,那么你将延迟半秒钟,以此类推。 - KingOfHypocrites

11
不要使用计时器来生成线程。只开启一个线程。当线程完成工作循环后,计算下一次循环开始前剩余的时间。如果这段时间是0或负数,立即回到循环开头启动一个新的循环;如果是正数,就在循环回到开头之前休眠这段时间。
通常可以通过计算开始时间和结束时间之间无符号整数差的有符号整数结果来获得已花费的时间。将此结果从所需间隔中减去即可得到新的剩余时间。
不需要额外的计时器线程,也没有两个线程同时运行的可能性,设计更为简化,无需持续创建/启动/终止/销毁操作,无需进行 mallocs、new()、栈分配/释放、垃圾回收。
使用计时器、互斥锁、信号量、锁等其他设计过于复杂。如果不产生多余的线程,何必费心用同步机制来阻止它们呢?
有时,使用计时器而不是 sleep() 循环是一个非常糟糕的想法。这似乎就是其中一种情况。
public void doWorkEvery(int interval)
{
    while (true)
    {
        uint startTicks;
        int workTicks, remainingTicks;
        startTicks = (uint)Environment.TickCount;
        DoSomeWork();
        workTicks=(int)((uint)Environment.TickCount-startTicks);
        remainingTicks = interval - workTicks;
        if (remainingTicks>0) Thread.Sleep(remainingTicks);
    }
}

1
非常有趣。建议:将此更改为始终调用Sleep。因此,最后一行变成了Thread.Sleep(Math.Max(remainingTicks, MINIMUM_TICKS));,而不是if..Sleep..。原因1:始终让出CPU,以防有另一个线程具有相同的优先级。可以将MINIMUM_TICKS设置为0以获得简短的让出。原因2:如果该任务重复花费太长时间,则避免消耗太多CPU。在这种情况下,选择MINIMUM_TICKS作为我们希望保证该任务在每个周期中都不占用任何CPU的时间量。 - ToolmakerSteve
注意:不需要将其转换为uintint数学会以这样的方式“包装”,只有在开始和结束时间之间的差异超过int.MaxValue时才需要使用它(在这种情况下,您应该改用ulongGetTickCount64)。 实际上,使用uint使减法变得相当奇怪:即使结果可能是负值,您也正在取两个无符号值的差异。这是荒谬的。它之所以起作用,仅因为您将其重新转换为int。最好一开始就全部使用int。无论哪种方式,都必须允许“溢出”。 - ToolmakerSteve

6

你可以使用Monitor.TryEnter()代替lock,如果另一个计时器线程已经在执行回调,则会返回:

class Program
{
    static void Main(string[] args)
    {
        Timer t = new Timer(TimerCallback, null,0,2000);
        Console.ReadKey();
    }

    static object timerLock = new object();

    static void TimerCallback(object state)
    {
        int tid = Thread.CurrentThread.ManagedThreadId;
        bool lockTaken = false;
        try
        {
            lockTaken = Monitor.TryEnter(timerLock);
            if (lockTaken)
            {
                Console.WriteLine("[{0:D02}]: Task started", tid);
                Thread.Sleep(3000); // Do the work 
                Console.WriteLine("[{0:D02}]: Task finished", tid);
            }
            else
            {
                Console.WriteLine("[{0:D02}]: Task is already running", tid);
            }
        }
        finally
        {
            if (lockTaken) Monitor.Exit(timerLock);
        }
    }
}

这是一个很好的解决方案,当你想要完全抛弃“额外”的执行时。唯一的潜在缺点是它增加了可能在执行之间经过的最长时间(与其他解决方案相比)。这是好是坏取决于当执行超过计时器周期时你想要发生什么。如果你希望在这种情况下快速启动下一个执行,我建议参考Martin Jame's answer - ToolmakerSteve
或者参考Tony Valenti的回答,使用IntervalStartTime.ElapsedStart - ToolmakerSteve

3
我知道你想做什么。你有一个定时器,它会定期执行回调函数(定时器的定义),这个回调函数会做一些工作。这些工作可能比定时器周期更长(例如,定时器周期为500毫秒,而某次回调的执行可能需要超过500毫秒)。这意味着你的回调函数需要支持重入。
如果你无法支持重入(有各种原因可能导致这种情况),我过去所做的是在回调函数开始时关闭定时器,然后在结束时重新启动定时器。例如:
private void timer_Elapsed(object source, ElapsedEventArgs e)
{
    timer.Enabled = false;
    //... do work
    timer.Enabled = true;
}

如果您实际上想要一个“线程”在另一个之后立即执行,我不建议使用计时器;我建议使用 Task 对象。例如:

Task.Factory.StartNew(()=>{
    // do some work
})
.ContinueWith(t=>{
    // do some more work without running at the same time as the previous
});

2
如果你只想防止同一进程/应用程序域中的两个线程并发执行,那么lock语句可能适合你。
但请注意,当其他线程等待访问关键部分时,lock会将它们锁定在那里。它们不会被中止、重定向或其他任何操作;它们只是坐在那里,等待原始线程完成对lock块的执行,以便它们可以运行。
使用mutex可以给您更多的控制权,包括能够使第二个及后续线程完全停止而不是锁定,并且可以锁定跨进程的线程。

1
我认为其中一些方法非常棒,但有点复杂。
我创建了一个包装类,可以防止计时器重叠,并允许您选择“每个间隔调用一次ELAPSED”或“调用之间应该有一个间隔延迟”。
如果您改进了此代码,请在此处发布更新!
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AllCommander.Diagnostics {
    public class SafeTimer : IDisposable {

        public enum IntervalStartTime {
            ElapsedStart,
            ElapsedFinish
        }


        private System.Timers.Timer InternalTimer;

        public bool AutoReset { get; set; }

        public bool Enabled {
            get {
                return InternalTimer.Enabled;
            }
            set {
                if (value) {
                    Start();
                } else {
                    Stop();
                }
            }
        }


        private double __Interval;
        public double Interval {
            get {
                return __Interval;
            }
            set {
                __Interval = value;
                InternalTimer.Interval = value;
            }
        }

        /// <summary>
        /// Does the internal start ticking at the END of Elapsed or at the Beginning? 
        /// </summary>
        public IntervalStartTime IntervalStartsAt { get; set; }

        public event System.Timers.ElapsedEventHandler Elapsed;


        public SafeTimer() {
            InternalTimer = new System.Timers.Timer();
            InternalTimer.AutoReset = false;
            InternalTimer.Elapsed += InternalTimer_Elapsed;

            AutoReset = true;
            Enabled = false;
            Interval = 1000;
            IntervalStartsAt = IntervalStartTime.ElapsedStart;
        }


        void InternalTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) {

            if (Elapsed != null) {
                Elapsed(sender, e);
            }

            var ElapsedTime = DateTime.Now - e.SignalTime;


            if (AutoReset == true) {
                //Our default interval will be INTERVAL ms after Elapsed finished.
                var NewInterval = Interval;
                if (IntervalStartsAt == IntervalStartTime.ElapsedStart) {
                    //If ElapsedStart is set to TRUE, do some fancy math to determine the new interval.
                    //If Interval - Elapsed is Positive, then that amount of time is remaining for the interval
                    //If it is zero or negative, we're behind schedule and should start immediately.
                    NewInterval = Math.Max(1, Interval - ElapsedTime.TotalMilliseconds);
                }

                InternalTimer.Interval = NewInterval;

                InternalTimer.Start();
            }

        }


        public void Start() {
            Start(true);
        }
        public void Start(bool Immediately) {
            var TimerInterval = (Immediately ? 1 : Interval);
            InternalTimer.Interval = TimerInterval;
            InternalTimer.Start();
        }

        public void Stop() {
            InternalTimer.Stop();
        }


        #region Dispose Code
        //Copied from https://lostechies.com/chrispatterson/2012/11/29/idisposable-done-right/
        bool _disposed;
        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        ~SafeTimer() {
            Dispose(false);
        }

        protected virtual void Dispose(bool disposing) {
            if (!_disposed) {
                if (disposing) {
                    InternalTimer.Dispose();
                }

                // release any unmanaged objects
                // set the object references to null
                _disposed = true;
            }
        }
        #endregion

    }
}

点赞 - 我喜欢:1)如果执行超过时间间隔,重新启动1毫秒后的选项。2)能够在两种方法之间进行选择。3)将其作为包装类。4)使用e.SignalTime,以便时间测量准确。顺便说一句,我会将处理程序命名为OnElapsedElapsedHandler,而不是Elapsed(对我来说听起来像是时间测量)。 - ToolmakerSteve

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接