【问题标题】:Firing Recurring Tasks At The Same Time同时触发重复任务
【发布时间】:2020-09-11 15:48:56
【问题描述】:

我试图让 2 个任务在特定时间点同时触发,然后重新执行。例如,下面是等待 1 分钟的任务和等待 5 分钟的第二个任务。 1 分钟任务应在 5 分钟内触发 5 次,5 分钟任务应在 1 次内触发,1 分钟任务应在 10 分钟内触发 10 次,5 分钟任务应在 2 次内触发,如此循环。但是,我需要 1 分钟的任务与 5 分钟的任务同时触发。

我可以使用 System.Timers 来做到这一点,但这与我最终需要的多线程处理效果不佳。 System.Thread 没有任何与 System.Timers AutoReset 等效的东西,除非我遗漏了什么。

我下面的内容是两个延迟计时器同时启动,但 t1 只触发 1 次而不是 5 次。本质上它需要继续运行,直到程序停止 X 次为止。

            int i = 0;
            while (i < 1)
            {

                Task t1 = Task.Run(async delegate
                {
                    await Task.Delay(TimeSpan.FromMinutes(1));
                    TaskWorkers.OneMinuteTasks();
                });
                //t1.Wait();

                Task t2 = Task.Run(async delegate
                {
                    await Task.Delay(TimeSpan.FromMinutes(5));
                    TaskWorkers.FiveMinuteTasks();
                });
                t2.Wait();
            } 

更新 我首先阅读了下面的 Johns 评论,内容是为任务添加一个内部循环。下面按我的意愿工作。简单的修复。我知道我确实说过只要程序运行我就希望它运行,但我能够计算出我实际需要的最大循环数。 x

                Task t1 = Task.Run(async delegate
                    {
                        for(int x = 0; x < 10; x++)
                        {
                            await Task.Delay(TimeSpan.FromMinutes(1));
                            TaskWorkers.OneMinuteTasks();
                        }
                    });

                Task t2 = Task.Run(async delegate
                {
                    for (int x = 0; x < 10; x++)
                    {
                        await Task.Delay(TimeSpan.FromMinutes(5));
                        TaskWorkers.FiveMinuteTasks();
                    }
                });

据我所知,没有 CPU 或内存的总使用量。

【问题讨论】:

  • 试试Hangfire
  • 我重新打开了您的问题,因为 Servy's duplicate 与您的要求不符。如果 5 分钟的任务花费的时间超过 5 分钟会怎样?如果它应该是“尽可能每 5 分钟一次”,那么任务内部的一个简单循环就足够了。

标签: c# multithreading asynchronous task


【解决方案1】:

您可以有一个循环以协调的方式定期触发任务:

async Task LoopAsync(CancellationToken token)
{
    while (true)
    {
        Task a = DoAsync_A(); // Every 5 minutes
        for (int i = 0; i < 5; i++)
        {
            var delayTask = Task.Delay(TimeSpan.FromMinutes(1), token);
            Task b = DoAsync_B(); // Every 1 minute
            await Task.WhenAll(b, delayTask);
            if (a.IsCompleted) await a;
        }
        await a;
    }
}

此实现在开始一个新的 1 分钟循环之前等待 B 任务和 Task.Delay 任务完成,因此如果 B 任务运行时间过长,则计划将滑倒。这可能是一种可取的行为,除非您可以接受重叠任务的可能性。

如果 A 或 B 任务发生异常,循环将在一分钟检查点报告失败。这并不理想,但让循环完美响应错误会使代码变得相当复杂。


更新:这是一个高级版本,在出现异常时响应更快。它使用链接的CancellationTokenSource,当两个任务中的任何一个失败时自动取消,然后立即取消延迟任务。

async Task LoopAsync(CancellationToken token)
{
    using (var linked = CancellationTokenSource.CreateLinkedTokenSource(token))
    {
        while (true)
        {
            Task a = DoAsync_A(); // Every 5 minutes
            await WithCompletionAsync(a, async () =>
            {
                OnErrorCancel(a, linked);
                for (int i = 0; i < 5; i++)
                {
                    var delayTask = Task.Delay(TimeSpan.FromMinutes(1),
                        linked.Token);
                    await WithCompletionAsync(delayTask, async () =>
                    {
                        Task b = DoAsync_B(); // Every 1 minute
                        OnErrorCancel(b, linked);
                        await b;
                        if (a.IsCompleted) await a;
                    });
                }
            });
        }
    }
}

async void OnErrorCancel(Task task, CancellationTokenSource cts)
{
    try
    {
        await task.ConfigureAwait(false);
    }
    catch
    {
        cts.Cancel();
        //try { cts.Cancel(); } catch { } // Safer alternative
    }
}

async Task WithCompletionAsync(Task task, Func<Task> body)
{
    try
    {
        await body().ConfigureAwait(false);
    }
    catch (OperationCanceledException)
    {
        await task.ConfigureAwait(false);
        throw; // The task isn't faulted. Propagate the exception of the body.
    }
    catch
    {
        try
        {
            await task.ConfigureAwait(false);
        }
        catch { } // Suppress the task's exception
        throw; // Propagate the exception of the body
    }
    await task.ConfigureAwait(false);
}

这个版本的逻辑比最初的简单版本要复杂得多(这使得它更容易出错)。 CancellationTokenSource 的引入产生了处理它的需要,这反过来又强制确保所有任务都将在异步方法的每个退出点完成。这就是使用WithCompletionAsync 方法将每个任务之后的所有代码封装在LoopAsync 方法内的原因。

【讨论】:

    【解决方案2】:

    我认为计时器或类似 Vasily 的建议将是可行的方法,因为这些解决方案旨在处理重复性任务,而不仅仅是使用线程。但是,您可以使用线程来执行此操作,例如:

        void TriggerTimers()
        {
            new Thread(() =>
            {
                while (true)
                {
                    new Thread(()=> TaskA()).Start();
                    Thread.Sleep(60 * 1000); //start taskA every minute
                }
    
            }).Start();
    
            new Thread(() =>
            {
                while (true)
                {
                    new Thread(() => TaskB()).Start();
                    Thread.Sleep(5 * 60 * 1000); //start taskB every five minutes
                }
    
            }).Start();
        }
    
        void TaskA() { }
    
        void TaskB() { }
    

    请注意,如果在很长一段时间内使用此解决方案,我会产生少量偏差,尽管除非您处理非常精细的边距或非常过载的计算机,否则这应该不会很重要。此外,这个解决方案对 John 提到的描述没有偶然性 - 它相当轻量级,但也很容易理解

    【讨论】:

    • 如果TaskA 需要超过一分钟才能完成怎么办?
    • 我想说的是你的 cmets "start taskA every minute""start taskB every 五分钟” 是不正确的。您应该将单词 "every" 更改为 "after"
    • @vasily.sib 确实如此。这也是我试图从 OP 那里得到的澄清。如果它适用于 OP 的情况,我确实认为这是一个很好的解决方案。
    • 因为在本例中 TaskA 和 TaskB 在各自的线程上运行,所以它们将每分钟启动一次,即使 TaskA() 或 TaskB() 的最后一个线程正在运行。所以这两个 //cmets 实际上是正确的。通过自己运行代码并查看。您可以根据保存线程的答案制定解决方案,并检查状态以查看下一步该做什么。但问题是在这种情况下该怎么做。如果任务仍在运行,您想完全跳过吗?或者是其他东西?我不知道,我不会根据猜测为每个案例编码。
    • @Jack 虽然我同意你的回答是一个可接受的解决方案,并且可能足以满足 OP 的需求,但我不同意 “他们将每分钟开始,即使最后一个线程因为 TaskA() 或 TaskB() 正在运行。所以这两个 //cmets 实际上是真的"。想象一下,任务 A 的“工作量”部分需要 40 秒。然后等待 1 分钟再执行它。这意味着您每 100 秒执行一次工作负载,而不是每 60 秒执行一次。目前似乎预计工作量将花费极少的时间,但情况可能并非如此
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-14
    • 1970-01-01
    • 1970-01-01
    • 2021-05-06
    相关资源
    最近更新 更多