Firing Recurring Tasks At The Same Time

怎甘沉沦 提交于 2021-02-11 11:58:53

问题


I am trying to get 2 tasks to fire at the same time at a specific point in time, then do it all over again. For example, below is a task that waits 1 minute and a second task that waits 5 minutes. The 1 minute task should fire 5 times in 5 minutes and the 5 minute task 1 time, the 1 minute task should fire 10 times in 10 minutes and the 5 minute task 2 times, on and on and on. However, I need the 1 minute task to fire at the same time as the 5 minute.

I was able to do this with System.Timers but that did not play well with the multithreading that I eventually needed. System.Thread did not have anything equivalent to System.Timers AutoReset unless I'm missing something.

What I have below is both delay timers start at the same time BUT t1 only triggers 1 time and not 5. Essentially it needs to keep going until the program is stopped not just X times.

            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();
            } 

Update I first read Johns comment below about just adding an inner loop to the Task. Below works as I was wanting. Simple fix. I know I did say I would want this to run for as long as the program runs but I was able to calculate out the max number of loops I would actually need. x < 10 is just a number I choose.

                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();
                    }
                });

As far as I can tell no gross usage of CPU or memory.


回答1:


You could have a single loop that periodically fires the tasks in a coordinated fashion:

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;
    }
}

This implementation awaits both the B task and the Task.Delay task to complete before starting a new 1-minute loop, so if the B task is extremely long-running, the schedule will slip. This is probably a desirable behavior, unless you are OK with the possibility of overlapping tasks.

In case of an exception in either the A or B task, the loop will report failure at the one minute checkpoints. This is not ideal, but making the loop perfectly responsive on errors would make the code quite complicated.


Update: Here is an advanced version that is more responsive in case of an exception. It uses a linked CancellationTokenSource, that is automatically canceled when any of the two tasks fails, which then results to the immediate cancellation of the delay task.

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);
}

The logic of this version is significantly more perplexed than the initial simple version (which makes it more error prone). The introduction of the CancellationTokenSource creates the need for disposing it, which in turn makes mandatory to ensure that all tasks will be completed on every exit point of the asynchronous method. This is the reason for using the WithCompletionAsync method to enclose all code that follows every task inside the LoopAsync method.




回答2:


I think timers or something like Vasily's suggestion would be the way to go, as these solutions are designed to handle recurring tasks more than just using threads. However, you could do this using threads, saying something like:

    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() { }

Note that this solution will drift out my a small amount if used over a very long period of time, although this shouldn't be significant unless you're dealing with very delicate margins, or a very overloaded computer. Also, this solution doesn't have contingency for the description John mentioned - it's fairly lightweight, but also quite understandable



来源:https://stackoverflow.com/questions/61995084/firing-recurring-tasks-at-the-same-time

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!