1

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.

TomG
  • 1,019
  • 1
  • 8
  • 6
  • 1
    try something like [Hangfire](https://www.hangfire.io/) – vasily.sib May 25 '20 at 03:05
  • 1
    I've re-opened your question because [Servy's duplicate](https://stackoverflow.com/questions/17197699/awaiting-multiple-tasks-with-different-results) didn't match up with what you're asking. What happens if the 5 minute task takes longer than 5 minutes? If it should be "once every 5 minutes so far as is possible" then a simple loop inside the task should suffice. – ProgrammingLlama May 25 '20 at 03:25

2 Answers2

3

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.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
1

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

Jack
  • 871
  • 1
  • 9
  • 17
  • what if `TaskA` takes more then a minute to complete? – vasily.sib May 25 '20 at 03:47
  • what I'm trying to say is that your comments _"start taskA **every** minute"_ and _"start taskB **every** five minutes"_ are just not true. You should change word _"every"_ to _"after"_ – vasily.sib May 25 '20 at 04:07
  • @vasily.sib Indeed. That's the clarification I was trying to get from OP too. I do think this is a good solution if it works for OP's case though. – ProgrammingLlama May 25 '20 at 04:09
  • Because TaskA and TaskB are running on their own threads in this example, they will start every minute, even if the last thread for TaskA() or TaskB() is running. So those two //comments are actually true. Run the code through yourself and see. You could make a solution based on this answer where you save the thread, and check against the state to see if what to do next. The problem though is what to do in which case. Do you want to skip entirely if the task is still running? Or something else? I don't know, and I won't code for each case based on a guess. – Jack May 25 '20 at 05:26
  • @Jack While I agree with your answer as being an acceptable solution and probably enough for OP's needs, I do disagree with the statement that _"they will start every minute, even if the last thread for TaskA() or TaskB() is running. So those two //comments are actually true"_. Imagine the "workload" part of Task A takes 40 seconds. You then wait 1 minute before executing it again. That means that you're executing the workload every 100 seconds, not every 60 seconds. At the moment it seems expected that the workload will take an insignificantly small amount of time, which might not be the case – ProgrammingLlama May 25 '20 at 06:02
  • No, not really, because new Thread(()=> TaskA()).Start(); calls TaskA() asynchronously. https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.start There will be a very slight delay while the thread is waiting to be picked again up after Thread.Sleep() has finished, and a miniscule overhead with spawning a new thread. But these should be fractions of a nanosecond in each instance. – Jack May 25 '20 at 11:13