-1

I have a function from the one service to that will get the count of all files inside a directory. And another service will get that int number to do some stuff with it.

public int GetNumberOfAvatarsInFile()
{
    try
    {
        var path = GetAvatarsFilePath();
        var numberOfAvatars = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly).Length;
        return numberOfAvatars;
    }
    catch (Exception exception)
    {
        var message = $"Error While Getting Total Numbers Of Avatars at {DateTime.Now}\n\t" +
            $"Error: {JsonConvert.SerializeObject(exception)}";
        sentryClient.CaptureMessage(message, SentryLevel.Error);
        return 1;
    }
}

private string GetAvatarsFilePath()
{
    var webRootPath = webHostEnvironment.WebRootPath;
    var path = Path.Combine(webRootPath, "path");
    return path;
}

The other service will use this function like this

private int GetMaximumAvatarId() => avatarService.GetNumberOfAvatarsInFile();

How do I set up so that all these file getting logic and string combine will be separated to a background thread/another thread by either Task.Run or something similar?

When I try to set up the GetNumberOfAvatarsInFile() by implementing await Task.Run(async () => LOGIC+Return int); I have to return a Task rather than the int from the other service that is calling it as well, which is not desirable since those are other people code and I should not change them. Also as far as I know all the Path.Combine and Directory functions do not employ any awaiter.

Is there a way to implement this?

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Noelia
  • 89
  • 10
  • For such IO operation like this, why don't we just do async/await all the way ? then just await the task would easily take the number out ? – Gordon Khanh Ng. Aug 27 '21 at 04:48
  • you mean async await inside the function? Last I check I do not think Path.Combine or Directory has any awaiter that links with it? – Noelia Aug 27 '21 at 04:52
  • I mean that you want to wrap `GetNumberOfAvatarsInFile` in a back ground task like `Task.Run` right ? that would return a `Task`, so you can handle the outter level method as `Task` too, and mark it async, then just await your wrapper, that's done. – Gordon Khanh Ng. Aug 27 '21 at 04:55
  • The thing is that the service that calls this method needs an int not a Task – Noelia Aug 27 '21 at 06:46
  • Why do you want to move this logic into a background thread anyway? – Peter Csala Aug 27 '21 at 06:55
  • @PeterCsala normally for I/O operation that CPU can't performs on its own that may take up some time since it relies on other components I generally wants to put it in another thread/task while the main thread can await for it. Same with database manipulation like updateOne, inseertOne... – Noelia Aug 27 '21 at 06:59
  • @Noelia If your service is implemented in a synchronous then you have to perform a blocking call there. So, if async is not used [all the way down](https://stackoverflow.com/questions/41438736/async-all-the-way-down-well-whats-all-the-way-at-the-bottom) then putting some logic into a separate thread is not really benefical. – Peter Csala Aug 27 '21 at 07:04
  • 1
    @Noelia I guess Peter recommended again what i just ask since the first place `why don't we just do async/await all the way`, what i mean is make this `the service that calls this method` method return a Task too, therefore, async all the way – Gordon Khanh Ng. Aug 27 '21 at 07:46

1 Answers1

2

As mentioned in the comments, the best practice is to provide async methods to the caller and use async all the way (see this article). However there are 2 things that can already be done:

1. Make your I/O method run asynchronously in a separate thread.
2. Have callers call your method asynchronously even if the implementation is synchronous.

The implementations on client side and on service side are independent. Here is a commented example that I hope shows how to do this. Most of the code below is unnecessary and is there only to illustrate what happens when multiple callers call your method and what is executed when. You may change the Thread.Sleep() values to simulate different execution time.

I also added a side note regarding the value you return in the Exception, that does not look ok to me.

public class Program
{
    public static void Main()
    {
        // These simulate 3 callers calling your service at different times.
        var t1 = Task.Run(() => GetMaximumAvatarId(1));
        Thread.Sleep(100);
        var t2 = Task.Run(() => GetMaximumAvatarId(2));
        Thread.Sleep(2000);
        var t3 = Task.Run(() => GetMaximumAvatarId(3));

        // Example purposes.
        Task.WaitAll(t1, t2, t3);
        Console.WriteLine("MAIN: Done.");
        Console.ReadKey();
    }

    // This is a synchronous call on the client side. This could very well be implemented
    // as an asynchronous call, even if the service method is synchronous, by using a
    // Task and having the caller await for it (GetMaximumAvatarIdAsync).
    public static int GetMaximumAvatarId(int callerId)
    {
        Console.WriteLine($"CALLER {callerId}: Calling...");
        var i = GetNumberOfAvatarsInFile(callerId);
        Console.WriteLine($"CALLER {callerId}: Done -> there are {i} files.");
        return i;
    }

    // This method has the same signature as yours. It's synchronous in the sense that it
    // does not return an awaitable. However it now uses `Task.Run` in order to execute
    // `Directory.GetFiles` in a threadpool thread, which allows to run other code in
    // parallel (in this example `Sleep` calls, in real life useful code). It finally 
    // blocks waiting for the result of the task, then returns it to the caller as an int.
    // The "callerId" is for the example only, you may remove this everywhere.
    public static int GetNumberOfAvatarsInFile(int callerId)
    {
        Console.WriteLine($"    SERVICE: Called by {callerId}...");

        var path = GetAvatarsFilePath();
        var t = Task.Run(() => Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly).Length);

        // Simulate long work for a caller, showing the caller.
        Console.WriteLine($"    SERVICE: Working for {callerId}...");
        Thread.Sleep(500);
        Console.WriteLine($"    SERVICE: Working for {callerId}...");
        Thread.Sleep(500);
        Console.WriteLine($"    SERVICE: Working for {callerId}...");
        Thread.Sleep(500);
        
        Console.WriteLine($"    SERVICE: Blocking for {callerId} until task completes.");

        return t.Result; // Returns an int.

        // --------------------------------------------------------
        // Side note: you should return `-1` in the `Exception`.
        // Otherwise it is impossible for the caller to know if there was an error or
        // if there is 1 avatar in the file.
        // --------------------------------------------------------
    }

    // Unchanged.
    private string GetAvatarsFilePath()
    {
        var webRootPath = webHostEnvironment.WebRootPath;
        var path = Path.Combine(webRootPath, "path");
        return path;
    }
}
evilmandarine
  • 4,241
  • 4
  • 17
  • 40
  • 1
    This answer violates [Microsoft's guidelines](https://devblogs.microsoft.com/pfxteam/should-i-expose-asynchronous-wrappers-for-synchronous-methods/ "Should I expose asynchronous wrappers for synchronous methods?") regarding the use of the `Task.Run` method, hence my downvote. – Theodor Zoulias Aug 27 '21 at 16:24
  • @TheodorZoulias The only exposed method is the synchronous `GetNumberOfAvatarsInFile`. I may be missing something but I do not believe this is the same as doing what is mentioned in that article. – evilmandarine Aug 27 '21 at 16:47
  • 1
    evilmandarine you are right, the `GetAvatarsFilePathAsync` may not be technically exposed since it is `private`, but it still conveys the wrong semantics to anyone who is going to maintain this piece of code. So it still violates the spirit of Microsoft's article IMHO. – Theodor Zoulias Aug 27 '21 at 16:54
  • 1
    @TheodorZoulias I changed the code, the methods were named poorly. It now matches what I meant to show, and should now be inline with the guidelines. Thank you for explaining the downvote. – evilmandarine Aug 28 '21 at 09:27
  • OK, the reason for my downvote has now been eliminated. :-) IMHO your answer could be improved by making it clearer that the `Task.Run` is used with the intention to introduce parallelism. Also the description above the `GetNumberOfAvatarsInFile` seems not entirely consistent with the rest of the code, after the edit. – Theodor Zoulias Aug 28 '21 at 09:59
  • `Task.Run` is particularly bad in ASP.Net environments. If you `await` an `async` method, the calling thread can be used to process other requests while the method runs. Using `Task.Run` on synchronous methods queues the work to run on the threadpool. So ASP.Net has to queue work on the threadpool, surrender the current thread while the method runs, then handle the continuation when the method is done. You can avoid all this work by simply invoking the synchronous method and blocking until it completes. If the method needs to block, it's better to do it without the threads and continuations. – Patrick Tucci Aug 29 '21 at 13:29
  • @PatrickTucci You're right, thank you for this. In case others want to know more about it (like me :), see [this article](https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html) and [this Q&A](https://stackoverflow.com/a/33764670/2983568). – evilmandarine Aug 29 '21 at 17:45