[英]Run a method only once in case of concurrent calls and return the same result to all of them
我正在編寫一個 ASP.net Core 6 應用程序(但問題更多的是關於 C# 的一般情況),其中我有一個 controller 操作,如下所示:
[HttpGet]
public async Task<IActionResult> MyAction() {
var result = await myService.LongOperationAsync();
return Ok(result);
}
基本上,該操作調用需要執行一些復雜操作的服務,並且可能需要一些時間來響應,最多一分鍾。 顯然,如果同時另一個請求到達,則LongOperationAsync()
的第二次運行開始,消耗更多的資源。
我想做的是重新設計它,以便對LongOperationAsync()
的調用不會並行運行,而是等待第一次調用的結果,然后全部返回相同的結果,以便LongOperationAsync()
只運行一次.
我想了幾種方法來實現這一點(例如,使用 Quartz 之類的調度程序來運行調用,然后在將另一個作業排入隊列之前檢查相關作業是否已經在運行),但它們都需要相當多的相對復雜的管道。
所以我想我的問題是:
您可以使用Lazy<T>
的異步版本來執行此操作。
Stephen Toub 在這里發布了LazyAsync<T>
的示例實現,我在下面復制:
public class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<T> valueFactory) :
base(() => Task.Factory.StartNew(valueFactory))
{ }
public AsyncLazy(Func<Task<T>> taskFactory) :
base(() => Task.Factory.StartNew(taskFactory).Unwrap())
{ }
public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); }
}
你可以像這樣使用它:
public class Program
{
public static async Task Main()
{
var test = new Test();
var task1 = Task.Run(async () => await test.AsyncString());
var task2 = Task.Run(async () => await test.AsyncString());
var task3 = Task.Run(async () => await test.AsyncString());
var results = await Task.WhenAll(task1, task2, task3);
Console.WriteLine(string.Join(", ", results));
}
}
public sealed class Test
{
public async Task<string> AsyncString()
{
Console.WriteLine("Started awaiting lazy string.");
var result = await _lazyString;
Console.WriteLine("Finished awaiting lazy string.");
return result;
}
static async Task<string> longRunningOperation()
{
Console.WriteLine("longRunningOperation() started.");
await Task.Delay(4000);
Console.WriteLine("longRunningOperation() finished.");
return "finished";
}
readonly AsyncLazy<string> _lazyString = new (longRunningOperation);
}
如果你運行這個控制台應用程序,你會看到longRunningOperation()
只被調用一次,當它完成時,所有等待它的任務都會完成。
正如馬修的回答所指出的那樣,您正在尋找的是“異步懶惰”。 這沒有內置類型,但創建起來並不難。
但是,您應該注意的是,在異步惰性類型中存在一些設計權衡:
Task.Factory.StartNew
是不必要的開銷。AsyncLazy<T>
方法中,如果工廠 function 失敗,則將無限期緩存故障任務。AsyncLazy<T>
代碼永遠不會重置; 成功的響應也會被無限期地緩存。我假設您確實希望代碼運行多次; 你只是希望它不要同時運行多次。 在這種情況下,您希望異步延遲在完成后立即重置,無論成功還是失敗。
重置可能很棘手。 您只想在完成后重置,並且只重置一次(即,您不希望重置代碼清除下一個操作)。 我對這種邏輯的首選是唯一標識符。 我喜歡為此使用new object()
。
因此,我將從Lazy<Task<T>>
想法開始,但將其包裝而不是派生,這樣您就可以進行重置,如下所示:
public class AsyncLazy<T>
{
private readonly Func<Task<T>> _factory;
private readonly object _mutex = new();
private Lazy<Task<T>> _lazy;
private object _id;
public AsyncLazy(Func<Task<T>> factory)
{
_factory = factory;
_lazy = new(_factory);
_id = new();
}
private (object LocalId, Task<T> Task) Start()
{
lock (_mutex)
{
return (_id, _lazy.Value);
}
}
private void Reset(object localId)
{
lock (_mutex)
{
if (localId != _id)
return;
_lazy = new(_factory);
_id = new();
}
}
public async Task<T> InvokeAsync()
{
var (localId, task) = Start();
try
{
return await task;
}
finally
{
Reset(localId);
}
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.