多个计时器/回调 — 防止重复和监控重复的最佳方法

本文关键字:监控 方法 最佳 计时器 回调 | 更新日期: 2023-09-27 18:33:45

我有一个c#控制台,我已经把它变成了一个Windows服务,我想可靠和持续地运行它。

  1. 我想防止同一计时器再次触发重叠
  2. 我想防止不同的计时器尝试同时使用相同的资源
  3. 我希望能够监控计时器并与之交互。

它有几个方面。 每个都非常有规律地运行。 我以前读过关于TaskScheduler与Windows服务运行这种东西的文章,并选择了这种方法,因为某些东西几乎一直在运行。

  • 任务类型1
  • 任务类型2
  • 任务类型3
  • 任务类型4
我正在使用计时器回调,每个回调

都有自己的回调,类似于这个简化版本:

class Program
{
    static PollingService _service;
    static void Main()
    {
        _service = new PollingService();
        TimerCallback tc1 = _service.TaskType1;
        TimerCallback tc2 = _service.TaskType2;
        TimerCallback tc3 = _service.TaskType3A;
        TimerCallback tc4 = _service.TaskType3B;
        Timer t1 = new Timer(tc1, null, 1000, 5000);
        Timer t2 = new Timer(tc2, null, 2000, 8000);
        Timer t3 = new Timer(tc3, null, 3000, 11000);
        Timer t4 = new Timer(tc4, null, 4000, 13000);

        Console.WriteLine("Press Q to quit");
        while (Console.ReadKey(true).KeyChar != 'q')
        {
        }
    }
}
class PollingService
{
    public void TaskType1(object state)
    {
        for (int i = 1; i <= 10; i++)
        {
            Console.WriteLine($"TaskOne numbering {i}");
            Thread.Sleep(100);
        }
    }
    public void TaskType2(object state)
    {
        for (int i = 10; i <= 100; i++)
        {
            Console.WriteLine($"TaskTwo numbering {i}");
            Thread.Sleep(100);
        }
    }
    public void TaskType3A(object state)
    {
        Increment(200000000);
    }
    public void TaskType3B(object state)
    {
        Increment(40000);
    }
    private void Increment(int startNumber)
    {
        for (int i = startNumber; i <= startNumber + 1000; i++)
        {
            Console.WriteLine($"Private {startNumber} numbering {i}");
            Thread.Sleep(5);
        }
    }
}

1 首先,我想确保当一个有时跑得很长时,这些不会相互捆绑。
例如。如果任务 1 有时需要 20 秒才能运行,我想防止重复的计时器,而前一个计时器可能仍在运行,事实上所有计时器都是一样的。 例如。如果 T2 的运行时间比平时长一点,则不要启动另一个。 我已经读过一些关于if (Monitor.TryEnter(lockObject)),这是处理该要求的最佳方法吗?

2 其次,如果它们都访问相同的资源(在我的例子中是 EF 上下文(,则 t3 已经在使用它,并且 t4 尝试这样做。 有没有办法让计时器等到另一个完成?

3 最后,有没有办法监控这些计时器/回调? 我想提供一个 UI 来查看它作为 Windows 服务运行时的状态。 我的最终目标是提供一个 UI,用户可以查看任务是否正在运行,如果没有,则在一段时间内未设置为运行时按需触发它。 但同时,不要在运行时创建副本。

想知道我是否应该把这些问题作为单独的问题来问,但它们似乎与彼此的决定交织在一起。

多个计时器/回调 — 防止重复和监控重复的最佳方法

如果必须确保每个线程没有任何重叠,则可以使用 Timer.Change(int, int) 方法在回调开始时停止执行,然后在回调结束时恢复执行。你也可以为每个线程做一些魔术,用一个ManualResetEvent,但它会变得混乱。

我不喜欢线程计时器,并尽可能避免使用它们。如果你可以牺牲"每个线程必须在n秒后运行",那就去做吧。请改用带有取消令牌的任务,它将解决您的重叠问题。例如:

一个。

public class Foo
{
    private CancellationTokenSource _cts;
    //In case you care about what tasks you have.
    private List< Task > _tasks;
    public Foo()
    {
        this._cts = new CancellationTokenSource();
        this._tasks.Add(Task.Factory.StartNew(this.Method1, this._cts.Token));
        this._tasks.Add(Task.Factory.StartNew(this.Method2, this._cts.Token));
        this._tasks.Add(Task.Factory.StartNew(this.Method3, this._cts.Token));
        this._tasks.Add(Task.Factory.StartNew(this.Method4, this._cts.Token));

    }
    private void Method1(object state)
    {
        var token = (CancellationToken) state;
        while ( !token.IsCancellationRequested )
        {
            //do stuff
        }
    }
    private void Method2(object state)
    {
        var token = (CancellationToken)state;
        while (!token.IsCancellationRequested)
        {
            //do stuff
        }
    }
    private void Method3(object state)
    {
        var token = (CancellationToken)state;
        while (!token.IsCancellationRequested)
        {
            //do stuff
        }
    }
    private void Method4(object state)
    {
        var token = (CancellationToken)state;
        while (!token.IsCancellationRequested)
        {
            //do stuff
        }
    }
    public void StopExecution()
    {
        this._cts.Cancel();
    }
}

如果一次由多个线程使用,EF 上下文将引发异常。有一种方法可以同步它,使用 lock .它看起来像这样,给定上面的示例:

二.

public class Foo
{
    private object _efLock;
    public Foo()
    {
        this._efLock = new object();
    }
.
.
.
    private void MethodX(object state)
    {
        var token = (CancellationToken)state;
        while (!token.IsCancellationRequested)
        {
            lock(this._efLock)
            {
                 using(.......
            }
        }
    }
}

必须在访问 EF 上下文的每个线程中执行此操作。请记住,同样,由于复杂的lock场景带来的认知负荷,维护变得烦人。

我最近开发了一个应用程序,其中需要多个线程来访问相同的 EF 上下文。正如我上面提到的,锁定必须太多(并且有性能要求(,因此我设计了一个解决方案,其中每个线程将其对象添加到公共队列,而单独的线程除了从队列中提取数据并调用 EF 外什么都不做。这样,EF 上下文只能由一个线程访问。问题解决了。下面是给定上述示例的外观:

三.

public class Foo
{
    private struct InternalEFData
    {
        public int SomeProperty;
    }

    private CancellationTokenSource _dataCreatorCts;
    private CancellationTokenSource _efCts;
    //In case you care about what tasks you have.
    private List< Task > _tasks;
    private Task _entityFrameworkTask;
    private ConcurrentBag< InternalEFData > _efData;

    public Foo()
    {
        this._efData = new ConcurrentBag< InternalEFData >();
        this._dataCreatorCts = new CancellationTokenSource();
        this._efCts = new CancellationTokenSource();
        this._entityFrameworkTask = Task.Factory.StartNew(this.ProcessEFData, this._efCts.Token);
        this._tasks.Add(Task.Factory.StartNew(this.Method1, this._dataCreatorCts.Token));
        this._tasks.Add(Task.Factory.StartNew(this.Method2, this._dataCreatorCts.Token));
        .
        .
        .
    }
    private void ProcessEFData(object state)
    {
        var token = (CancellationToken) state;
        while ( !token.IsCancellationRequested )
        {
            InternalEFData item;
            if (this._efData.TryTake(out item))
            {
                using ( var efContext = new MyDbContext() )
                {
                    //Do processing.    
                }
            }
        }
    }
    private void Method1(object state)
    {
        var token = (CancellationToken) state;
        while ( !token.IsCancellationRequested )
        {
            //Get data from whatever source
            this._efData.Add(new InternalEFData());
        }
    }
    private void Method2(object state)
    {
        var token = (CancellationToken) state;
        while ( !token.IsCancellationRequested )
        {
            //Get data from whatever source
            this._efData.Add(new InternalEFData());
        }
    }

    public void StopExecution()
    {
        this._dataCreatorCts.Cancel();
        this._efCts.Cancel();
    }
}

当涉及到从执行线程读取数据时,我通常使用 SynchronizationContext .我不知道它是否是正确的对象,其他人可能会对此发表评论。创建一个同步对象,将其传递给线程,并让线程使用必要的数据对其进行更新,并将其发布到 UI/控制台线程:

D.

public struct SyncObject
{
    public int SomeField;
}
public delegate void SyncHandler(SyncObject s);
public class Synchronizer
{
    public event SyncHandler OnSynchronization;
    private SynchronizationContext _context;
    public Synchronizer()
    {
        this._context = new SynchronizationContext();
    }
    public void PostUpdate(SyncObject o)
    {
        var handleNullRefs = this.OnSynchronization;
        if ( handleNullRefs != null )
        {
            this._context.Post(state => handleNullRefs((SyncObject)state), o);
        }
    }
}
public class Foo
{
    private Synchronizer _sync;
    public Foo(Synchronizer s)
    {
        this._sync = s;
    }
    private void Method1(object state)
    {
        var token = (CancellationToken) state;
        while ( !token.IsCancellationRequested )
        {
            //do things
            this._sync.PostUpdate(new SyncObject());
        }
    }
}

再说一次,这就是我的做法,我不知道这是否是正确的方法。

  1. 基本上,是的,或者AutoResetEvent
  2. 您可以停止它,等待资源释放,然后重新启动它
  3. 保留与计时器关联的状态列表,并从计时器更新这些状态(设置为启动时运行,设置为完成后等待,或类似这些行(

1:可能最好的办法是不在计时器中做任何事情,而是统计一个任务 - 如果有的话,当设置或未设置标志时。寻找联锁(类(了解如何在不锁定的情况下实现它。

2:监视器。但说真的,为什么它们共享一个 EF 连接?

3:当然。创建性能计数器。监视它们。该 API 在窗口中存在了很多年。