你如何设置一些函数在后台线程中运行但保持正常的返回类型而不是 Task<int>?

Posted

技术标签:

【中文标题】你如何设置一些函数在后台线程中运行但保持正常的返回类型而不是 Task<int>?【英文标题】:How do you set up some function to run in a background thread but keep the normal return type instead of the Task<int>? 【发布时间】:2021-10-27 01:54:12 【问题描述】:

我有一个从一个服务到一个函数,它将获取目录中所有文件的计数。另一个服务将获取该 int 编号并对其进行处理。

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;

其他服务会这样使用这个函数

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

如何设置以便所有这些文件获取逻辑和字符串组合将通过Task.Run 或类似的东西分离到后台线程/另一个线程?

当我尝试通过实现await Task.Run(async () =&gt; LOGIC+Return int); 来设置GetNumberOfAvatarsInFile() 我必须从调用它的其他服务返回一个 Task 而不是 int,这是不可取的,因为这些是其他人的代码,我不应该更改它们。另外据我所知,所有Path.CombineDirectory 函数都没有使用任何等待者。

有没有办法实现这个?

【问题讨论】:

对于这样的IO操作,我们为什么不一直做async/await呢?那么只要等待任务就可以轻松取出号码了? 你的意思是函数内部的异步等待?最后我检查一下,我认为 Path.Combine 或 Directory 没有与之链接的等待者? 我的意思是你想将GetNumberOfAvatarsInFile 包装在Task.Run 这样的后台任务中,对吗?这将返回一个Task&lt;int&gt;,因此您也可以将外部级别方法处理为Task&lt;something&gt;,并将其标记为异步,然后等待您的包装器,就完成了。 问题是调用此方法的服务需要一个 int 而不是 Task @Noelia 我猜彼得再次推荐了我刚开始问的问​​题why don't we just do async/await all the way,我的意思是让这个the service that calls this method 方法也返回一个任务,因此,一路异步跨度> 【参考方案1】:

如 cmets 中所述,最佳实践是向调用者提供异步方法并一直使用异步(请参阅this article)。但是,有两件事已经可以完成: 1。使您的 I/O 方法在单独的线程中异步运行。 2。即使实现是同步的,也让调用者异步调用您的方法。

客户端和服务端的实现是独立的。这是一个带注释的示例,我希望展示如何执行此操作。下面的大部分代码都是不必要的,只是为了说明当多个调用者调用您的方法时会发生什么以及何时执行什么。您可以更改Thread.Sleep() 的值来模拟不同的执行时间。

我还添加了关于您在异常中返回的值的附注,这对我来说看起来不太好。

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;
    

【讨论】:

这个答案违反了Microsoft's guidelines 关于Task.Run 方法的使用,因此我投反对票。 @TheodorZoulias 唯一公开的方法是同步的GetNumberOfAvatarsInFile。我可能遗漏了一些东西,但我认为这与做那篇文章中提到的事情不同。 evilmandarine 你是对的,GetAvatarsFilePathAsync 在技术上可能不会暴露,因为它是private,但它仍然向任何要维护这段代码的人传达了错误的语义。所以还是违背了微软文章恕我直言的精神。 @TheodorZoulias 我更改了代码,方法名称很差。它现在符合我的意思,现在应该符合指南。感谢您解释否决票。 好的,我投反对票的原因现在已经消除。 :-) 恕我直言,您的答案可以通过更清楚地说明Task.Run 用于引入并行性来改进。此外,GetNumberOfAvatarsInFile 上方的描述似乎与编辑后的其余代码并不完全一致。

以上是关于你如何设置一些函数在后台线程中运行但保持正常的返回类型而不是 Task<int>?的主要内容,如果未能解决你的问题,请参考以下文章

如何在后台运行的Controller中调用函数

从 asp.net API 中的方法返回后,如何保持线程运行?

在后台线程执行硬任务,在主线程返回结果

多线程(10) — Future模式

如何使用 Kotlin-Multiplatform 在 iOS 应用程序的后台线程中运行任务?

说说你对线程池的了解 —— 初识线程池