1
\$\begingroup\$

I have following code:

public class Worker : BackgroundService
{

    public Worker()
    {
    }

    protected async override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var task1 = Task.Run(() =>
                MethodOneAsync(stoppingToken)
            ).ContinueWith(_ => Debug.WriteLine($"MethodOneAsync finished @ {DateTime.Now:dd.MM.yyyy HH:mm:ss.ffff}"));
            var task2 = Task.Run(() =>
                MethodTwoAsync(stoppingToken)
            ).ContinueWith(_ => Debug.WriteLine($"MethodTwoAsync finished @ {DateTime.Now:dd.MM.yyyy HH:mm:ss.ffff}"));

            await Task.WhenAll(task1, task2);

            await Task.Delay(10000);
        }
    }

    private async Task<bool> MethodOneAsync(CancellationToken stoppingToken)
    {
        Debug.WriteLine($"MethodOneAsync started @ {DateTime.Now:dd.MM.yyyy HH:mm:ss.ffff}");
        await Task.Delay(1000); // emulating some long work

        return true;
    }

    private async Task<bool> MethodTwoAsync(CancellationToken stoppingToken)
    {
        Debug.WriteLine($"MethodTwoAsync started @ {DateTime.Now:dd.MM.yyyy HH:mm:ss.ffff}");
        await Task.Delay(1000); // emulating some long work

        return true;
    }
}

What I want to achieve is:

  • to create background worker that will call MethodOneAsync and MethodTwoAsync on separate threads (start both tasks simultaneously)
  • log time each task started and finished
  • wait for them both to finish
  • then wait another 10 seconds
  • repeat all over again

The code I wrote does its job as expected. Since I am still trying to get a grasp of .NET Core tasks, I would like to know is this the right way to do it?

\$\endgroup\$
5
  • 1
    \$\begingroup\$ Are MethodOneAsync and MethodTwoAsync CPU-bound or IO-bound? \$\endgroup\$ Commented May 13, 2021 at 8:45
  • \$\begingroup\$ They get a list of objects from SQL, map them to MongoDB objects, and update MongoDB with new objects. So, mostly IO-bound. \$\endgroup\$
    – leroy
    Commented May 13, 2021 at 8:47
  • 1
    \$\begingroup\$ Then you don't need Task.Run calls in the ExecuteAsync. You could simply call await Task.WhenAll(MethodOneAsync(stoppingToken), MethodTwoAsync(stoppingToken)); \$\endgroup\$ Commented May 13, 2021 at 8:50
  • \$\begingroup\$ @PeterCsala can you please, in short, tell why? \$\endgroup\$
    – leroy
    Commented May 13, 2021 at 9:01
  • 1
    \$\begingroup\$ Task.Run was designed for CPU-bound work, so it pulls a thread from the thread pool to run the method and returns a Task which represents the completion. Async non-blocking I/O operation does not need a dedicated thread. \$\endgroup\$ Commented May 13, 2021 at 9:21

1 Answer 1

1
\$\begingroup\$
  • Async method out-of-the-box is using multiple threads for continuations. (Everything after await is continuation). Exception of this rule if existing SynchronizationContext. Don't Task.Run asynchronous I/O-bound operation, do it only for synchronous CPU-bound one. Asynchronous programming, SynchrinizationContext & TaskScheduler.
  • Mixing .ContinueWith and await is not recommended as it doing the same but using different coding pattern. Mixing patterns with no reason makes the code more complicated.

If I were you, the code could look like this:

public class Worker : BackgroundService
{
    protected async override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // all methods will throw OperationCanceledException if cancellation was happened,
        // then checking if it was canceled is a kind of redundancy here.
        while (true) 
        {
            var task1 = MethodOneAsync(stoppingToken);
            var task2 = MethodTwoAsync(stoppingToken);
            await Task.WhenAll(task1, task2, Task.Delay(10000, stoppingToken)); // inlining Task.Delay here changes the logic: 10 seconds counted between starts of two iterations not between finish of previous and start of next.
        }
    }

    private async Task MethodOneAsync(CancellationToken stoppingToken)
    {
        Debug.WriteLine($"MethodOneAsync started @ {DateTime.Now:dd.MM.yyyy HH:mm:ss.ffff}");
        await Task.Delay(1000, stoppingToken);
        Debug.WriteLine($"MethodOneAsync finished @ {DateTime.Now:dd.MM.yyyy HH:mm:ss.ffff}");
    }

    private async Task MethodTwoAsync(CancellationToken stoppingToken)
    {
        Debug.WriteLine($"MethodTwoAsync started @ {DateTime.Now:dd.MM.yyyy HH:mm:ss.ffff}");
        await Task.Delay(1000, stoppingToken);
        Debug.WriteLine($"MethodTwoAsync finished @ {DateTime.Now:dd.MM.yyyy HH:mm:ss.ffff}");
    }
}

About Worker class purpose. It looks like Scheduler not Worker because it repeats some work continuously. Consider to change its name.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.