【问题标题】:How to implement synchronization with timers in C#如何在 C# 中实现与定时器的同步
【发布时间】:2011-11-23 09:10:24
【问题描述】:

我有一个场景,我的 C# 类有两个方法,例如 DoThis()DoThat(),它们被外部调用者以任何顺序相互独立地调用。这两个方法需要通过以下方式同步:

  • 在调用DoThis() 后,至少等待t1 秒,然后再继续执行DoThat()
  • 调用DoThat() 后,至少等待t2 秒,然后再继续执行DoThis()

所以本质上是伪代码:

static SomeCustomTimer Ta, Tb;
static TimeSpan t1, t2;

public static void DoThis()
{
    if(Tb.IsRunning())
        Tb.WaitForExpiry();

    DoStuff();
    Ta.Start(t1);
}

public static void DoThat()
{
    if(Ta.IsRunning())
        Ta.WaitForExpiry();

    DoOtherStuff();
    Tb.Start(t2);
}

DoStuff()DoOtherStuff()不是长时间运行的方法,否则不共享资源。通常不会同时调用DoThis()DoThat()。但我仍然需要防止潜在的死锁。

如何在 C# 中最好地实现 DoThis()DoThat()

编辑 我现在的场景很简单,因为没有任意数量的线程调用这些函数。为简化起见,有一个调用者线程以任意顺序调用这些函数。因此这两个方法不会被同时调用,而是调用者会以任意顺序一个接一个地调用这些方法。我无法控制调用者线程的代码,所以我想在连续调用 DoThis()、DoThat() 之间强制执行延迟。

【问题讨论】:

  • 只是想检查一下:你想“中断”另一个线程的执行一段时间——现在不管它在哪里执行? (所以线程根本没有处理器时间)?我想您可以尝试使用 Threads 来做到这一点,但我认为这根本不会防止死锁 - 如果一个线程持有锁并且您暂停它,您希望获得什么?
  • 这不会为每次调用DoThisDoThat 创建一个无限但延迟的循环吗?这就是你想要的吗?
  • @CarstenKönig 不,我不想中断线程执行。我想防止并发执行,如果另一个线程刚刚在一段时间内完成,我还想延迟启动一个线程。

标签: c# .net multithreading synchronization


【解决方案1】:

这很容易通过定时锁存器解决。闩锁是打开或关闭的同步机制。当允许打开的线程通过时。当关闭的线程无法通过时。定时闩锁是在经过一定时间后自动重新打开或重新关闭的闩锁。在这种情况下,我们需要一个“常开”的闩锁,因此行为偏向于保持打开状态。这意味着锁存器将在超时后自动重新打开,但仅当显式调用 Close 时才会关闭。多次调用Close 将重置计时器。

static NormallyOpenTimedLatch LatchThis = new NormallyOpenTimedLatch(t2);
static NormallyOpenTimedLatch LatchThat = new NormallyOpenTimedLatch(t1);

static void DoThis()
{
  LatchThis.Wait();  // Wait for it open.

  DoThisStuff();

  LatchThat.Close();
}

static void DoThat()
{
  LatchThat.Wait(); // Wait for it open.

  DoThatStuff();

  LatchThis.Close();
}

我们可以像下面这样实现我们的定时锁存器。

public class NormallyOpenTimedLatch
{
    private TimeSpan m_Timeout;
    private bool m_Open = true;
    private object m_LockObject = new object();
    private DateTime m_TimeOfLastClose = DateTime.MinValue;

    public NormallyOpenTimedLatch(TimeSpan timeout)
    {
        m_Timeout = timeout;
    }

    public void Wait()
    {
        lock (m_LockObject)
        {
            while (!m_Open)
            {
                Monitor.Wait(m_LockObject);
            }
        }
    }

    public void Open()
    {
        lock (m_LockObject)
        {
            m_Open = true;
            Monitor.PulseAll(m_LockObject);
        }
    }

    public void Close()
    {
        lock (m_LockObject)
        {
            m_TimeOfLastClose = DateTime.UtcNow;
            if (m_Open)
            {
                new Timer(OnTimerCallback, null, (long)m_Timeout.TotalMilliseconds, Timeout.Infinite);
            }
            m_Open = false;
        }
    }

    private void OnTimerCallback(object state)
    {
        lock (m_LockObject)
        {
            TimeSpan span = DateTime.UtcNow - m_TimeOfLastClose;
            if (span > m_Timeout)
            {
                Open();
            }
            else
            {
                TimeSpan interval = m_Timeout - span;
                new Timer(OnTimerCallback, null, (long)interval.TotalMilliseconds, Timeout.Infinite);
            }
        }
    }

}

【讨论】:

    【解决方案2】:

    嗯..在这种情况下你需要什么: 一个线程连续一段时间调用 DoThis。另一个可以在最后一次调用 DoThat 后至少 t2 秒或最后一次调用 DoThat 后的第一个运行 DoThat?

    我认为,如果你的目标平台是Win那么最好使用WaitableTimer(但是,它在.NET中没有实现,但你可以通过API使用它。你需要定义那些函数:

    [DllImport("kernel32.dll")]
     public static extern IntPtr CreateWaitableTimer(IntPtr lpTimerAttributes, bool bManualReset, string lpTimerName);
    
     [DllImport("kernel32.dll")]
     public static extern bool SetWaitableTimer(IntPtr hTimer, [In] ref long pDueTime,
                             int lPeriod, IntPtr pfnCompletionRoutine,
                             IntPtr lpArgToCompletionRoutine, bool fResume);
    
     [DllImport("kernel32", SetLastError = true, ExactSpelling = true)]
     public static extern Int32 WaitForSingleObject(IntPtr handle, int milliseconds);
     public static uint INFINITE = 0xFFFFFFFF;
    

    然后使用它如下:

    private IntPtr _timer = null;
    
    //Before first call of DoThis or DoThat you need to create timer:
    //_timer = CreateWaitableTimer (IntPtr.Zero, true, null);
    
    public static void DoThis()
    {
        //Waiting until timer signaled
        WaitForSingleObject (_timer, INFINITE);
    
        DoStuff();
        long dueTime = 10000 * 1000 * seconds; //dueTime is in 100 nanoseconds
        //Timer will signal once after expiration of dueTime
        SetWaitableTimer (_timer, ref dueTime, 0, IntPtr.Zero, IntPtr.Zero, false);
    }
    
    public static void DoThis()
    {
        //Waiting until timer signaled
        WaitForSingleObject (_timer, INFINITE);
    
        DoOtherStuff();
        long dueTime = 10000 * 1000 * seconds; //dueTime is in 100 nanoseconds
        //Timer will signal once after expiration of dueTime
        SetWaitableTimer (_timer, ref dueTime, 0, IntPtr.Zero, IntPtr.Zero, false);
    }
    

    在使用后你可以通过调用 CloseHandle 来销毁定时器。

    【讨论】:

    • 您可以将AutoResetEvent (Desc on MSDN) 用于相同目的。无需 P/Invoke。 AutoResetEvents WaitOne(Int32) 将一直等待,直到收到信号或时间用完。
    • @Praetor12 我没有完全清楚地表达我的问题,但看起来你确实明白了我的意图。您提出的解决方案听起来对我有用。如果我能找到一种不用 p/invoke 的方法来做到这一点,那将有很大帮助。
    • @bflat 当一个线程在 DoThat 中时,您是否需要另一个线程无法进入 DoThis,反之亦然?很多线程可以同时在 DoThat(或这样做)吗?在我们开始思考之前,必须回答这些问题:)
    • @Praetor12 > 当一个线程在 DoThat 中时,另一个线程不能在 DoThis 中进入,反之亦然?是的,我希望专门输入这些功能。如果 DoThis() 正在执行,DoThat() 的执行应该等到 DoThis() 结束加上时间 t1。 > 很多线程可以同时在DoThat(或DoThis)中吗?不可以。多个线程不能同时进入任何一个函数。
    • @bflat 还有一个问题:如果一个线程结束执行 DoThis 并且另一个线程等待 t1 时间运行 DoThat - 如果第一个线程再次调用 DoThis 你需要什么:它应该等待直到第二个线程结束 DoThat + t2 时间或执行 DoThis 并在开始位置设置 t1 时间?
    【解决方案3】:

    好的,我正在尝试使用 EventWaitHandle 来解决这个问题。寻找 cmets / 反馈。这可以可靠地工作吗?

    // Implementation of a manual event class with a DelayedSet method
    // DelayedSet will set the event after a delay period
    // TODO: Improve exception handling
    public sealed class DelayedManualEvent : EventWaitHandle
    {
        private SysTimer timer; // using SysTimer = System.Timers.Timer;
    
        public DelayedManualEvent() :
            base(true, EventResetMode.ManualReset)
        {
            timer = new SysTimer();
            timer.AutoReset = false;
            timer.Elapsed +=new ElapsedEventHandler(OnTimeout);
        }
    
        public bool DelayedSet(TimeSpan delay)
        {
            bool result = false;
            try
            {
                double timeout = delay.TotalMilliseconds;
                if (timeout > 0 && timer != null && Reset())
                {
                    timer.Interval = timeout;
                    timer.Start();
                    result = true;
                    Trace.TraceInformation("DelayedManualEvent.DelayedSet Event will be signaled in {0}ms",
                        delay);
                }
            }
            catch (Exception e)
            {
                Trace.TraceError("DelayedManualEvent.DelayedSet Exception {0}\n{1}", 
                    e.Message, e.StackTrace);
            }
            return result;
        }
    
        private void OnTimeout(object source, ElapsedEventArgs e)
        {
            if (timer != null)
            {
                timer.Stop();
                Trace.TraceInformation("DelayedManualEvent.OnTimeout Event signaled at time {0}", e.SignalTime);
            }
            try
            {
                if (!Set())
                {
                    Trace.TraceError("DelayedManualEvent.OnTimeout Event set failed");
                }
            }
            catch (Exception ex)
            {
                Trace.TraceError("DelayedManualEvent.OnTimeout Exception in signaling event\n{0}]\n{1}",
                    ex.Message, ex.StackTrace);
            }
        }
    
        protected override void Dispose(bool disposing)
        {
            if (timer != null)
            {
                timer.Dispose();
            }
            base.Dispose(disposing);
        }
    }
    

    我打算使用这个的方式:

    // Pseudocode
    static DelayedManualEvent delayedEvent = new DelayedManualEvent();
    static TimeSpan t1, t2, maxTimeout;
    
    public static void DoThis()
    {
        if(!delayedEvent.WaitOne(maxTimeout))
            return;
        DoStuff();
        delayedEvent.DelayedSet(t1);
    }
    
    public static void DoThat()
    {
        if(!delayedEvent.WaitOne(maxTimeout))
            return;
        DoOtherStuff();
        delayedEvent.DelayedSet(t2);
    }
    

    【讨论】:

    • DelayedSet 方法不是线程安全的。此外,您的主要代码现在具有与原始问题不同的行为。为什么DoThisDoThat 中有return 语句?
    • 如果您提议更新您的问题,通常编辑您的问题以包含它,而不是将其添加为答案。但是,要回答您的反馈请求,不,这在很大程度上是因为您在两种情况下都使用了相同的 DelayedManualEvent 实例。此外,您的实现一次允许任意数量的线程通过,充当闸门:要么所有线程等待,要么所有线程同时进入这两种方法。请参阅 Brian Gideon 的回答,了解您应该做什么。
    • @ChrisHannon 道歉,我在这里的第一个问题,所以我不知道习惯。下次会记住这一点。关于反馈,我认为我仍然无法正确表达我的问题 - 场景更简单。我将编辑我的问题以澄清。
    猜你喜欢
    • 2013-03-04
    • 2017-05-29
    • 1970-01-01
    • 2013-09-09
    • 1970-01-01
    • 2011-04-29
    • 1970-01-01
    • 1970-01-01
    • 2015-07-09
    相关资源
    最近更新 更多