如何在C#中使用定时器实现同步

4
我有一个场景,我的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()之间的延迟。


只是想确认:您是否想要在某段时间内“中断”其他线程的执行 - 无论其在执行中的位置如何?(因此,该线程基本上根本不会获得任何处理器时间)?我想您可以尝试使用线程来实现这一点,但我认为这根本无法防止死锁 - 如果一个线程持有锁并且您暂停它,那么您希望获得什么? - Random Dev
这不会为每次调用DoThisDoThat创建一个无限但延迟的循环吗?这是你想要的吗? - Enigmativity
@CarstenKönig 不,我不想中断线程执行。我想防止并发执行,并且如果另一个线程在一段时间窗口内刚刚完成,我也想延迟启动一个线程。 - amo
3个回答

5

使用定时闩门可以很容易地解决这个问题。 闩门是一种同步机制,可以打开或关闭。当打开时,线程可以通过;当关闭时,线程无法通过。 定时闩门会在经过一定时间后自动重新打开或关闭。 在这种情况下,我们想要一个“通常打开”的闩门,以便行为偏向于保持打开状态。 这意味着,在超时后,闩门将自动重新打开,但仅在显式调用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);
            }
        }
    }

}

1
嗯.. 在这种情况下你需要什么: 一个线程在连续一段时间内调用DoThis。另一个线程可以在上一次调用DoThat后至少t2秒运行DoThat或在上一次调用DoThat之后第一次运行?
我认为,如果你的目标平台是Windows,最好使用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销毁计时器。


2
你可以使用AutoResetEventMSDN上的描述) 来实现同样的目的。不需要使用P/Invoke。AutoResetEvents 的 WaitOne(Int32) 方法将会等待,直到它被发信号或者超时。 - juhan_h
1
@bflat 在一个线程执行DoThat时,另一个线程是否不能进入DoThis,反之亦然?许多线程能同时执行DoThat(或DoThis)吗?必须在我们开始思考之前回答这些问题 :) - Tadeusz
1
@bflat 还有一个问题:如果一个线程已经执行完了DoThis函数,而另一个线程正在等待t1时间来运行DoThat函数 - 在第一个线程再次调用DoThis时,应该等待第二个线程完成DoThat + t2时间还是直接执行DoThis并将t1时间设置为起始位置? - Tadeusz
@Praetor12 在这种情况下,如果线程已经等待了t1的时间,我将只是让第二次调用失败。 - amo
不要使用未经管理的WinAPI函数进行P/Invoke,而是使用已经在.Net Framework中提供的任意选项(例如System.Timers.Timer、AutoResetEvent、Monitor等)。请参考Brian Gideon的答案,以获得更合理的解决方案。 - Chris Hannon
显示剩余2条评论

0

好的,我正在尝试使用EventWaitHandle来解决这个问题。寻求评论/反馈。这种方法可靠吗?

// 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 语句? - Brian Gideon
如果您要提出对问题的更新,通常应该编辑您的问题以包含它,而不是将其作为答案添加。然而,回答您的反馈请求,不,这不起作用,主要是由于您在两种情况下都使用了相同的DelayedManualEvent实例。此外,您的实现允许任意数量的线程通过,充当闸门:所有线程都等待或所有线程同时进入两个方法。请参见Brian Gideon的答案,了解您应该做什么。 - Chris Hannon
@ChrisHannon 抱歉,这是我第一次在这里提问,所以我不知道有定制的规则。下次会记住的。 关于反馈,我认为我还没有能够正确表达我的问题 - 场景更简单。我将编辑我的问题以澄清。 - amo

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