c# - 多个计时器/回调——防止重复并监控它们的最佳方法

标签 c# multithreading timer callback

我有一个 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 首先,我想确保当其中一个有时运行很长时,它们不会相互束缚。
例如。如果任务一有时需要 20 秒才能运行,我想防止出现重复的计时器,而前一个计时器可能仍在运行,实际上所有计时器都是如此。例如。如果 t2 的运行时间比平时稍长,则不要启动另一个。我读过一些有关 if (Monitor.TryEnter(lockObject)) 的内容,这是处理该要求的最佳方法吗?

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

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

我想知道我是否应该将这些作为单独的问题提出,但它们似乎与彼此的决定如此紧密地交织在一起。

最佳答案

如果您必须确保每个线程没有任何重叠,您可以使用Timer.Change(int, int)方法在回调开始时停止执行,然后在回调结束时恢复执行。您还可以为每个线程使用 ManualResetEvent 施展一些魔法,但这会变得困惑。

我不喜欢线程定时器,并尽可能避免使用它们。如果您可以牺牲“每个线程必须n秒后运行”,那就这样做。使用带有取消标记的任务,它将解决您的重叠问题。例如:

A.

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。考虑到上面的例子,它看起来像这样:

B.

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 上下文的每个线程中执行此操作。请再次记住,由于复杂的锁定场景会带来认知负担,维护会变得很烦人。

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

C.

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。我不知道它是否是正确使用的对象,其他人可能可以对此发表评论。创建一个 Synchronization 对象,将其传递给您的线程,让它们使用必要的数据更新它,并将其发布到您的 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());
        }

    }
}

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

关于c# - 多个计时器/回调——防止重复并监控它们的最佳方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35272914/

相关文章:

c - 使用 Queryperformancecounter 时的最大延迟和抖动是多少?

c# - 使用 yield return 返回泛型参数

java - 对多个对象的并发更新

c# - DI : Associating entities with repository

c# - 多个线程等待一个事件?

c++ - Qt在两个不同的线程中运行同一个对象的2个方法

c# - 在 C# Timer 中完成一小时后准确触发滴答事件

Java - 如何测量超时

c# - 在 C# 中使用\t 格式化未知字符串长度

c# - 用 C# 编写一个简单的 Web 服务并从 Ruby on Rails 调用它