簡體   English   中英

在並發調用的情況下只運行一次方法並將相同的結果返回給所有它們

[英]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 之類的調度程序來運行調用,然后在將另一個作業排入隊列之前檢查相關作業是否已經在運行),但它們都需要相當多的相對復雜的管道。

所以我想我的問題是:

  • 是否有既定的設計模式/最佳實踐來實現這個場景? 它甚至實用/一個好主意嗎?
  • C# 語言和/或 ASP.net 核心框架中是否有有助於實現此類功能的功能?

您可以使用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()只被調用一次,當它完成時,所有等待它的任務都會完成。

在 DotNetFiddle 上試試

正如馬修的回答所指出的那樣,您正在尋找的是“異步懶惰”。 這沒有內置類型,但創建起來並不難。

但是,您應該注意的是,在異步惰性類型中存在一些設計權衡:

  • 工廠 function 在什么上下文上運行(第一個調用者的上下文或根本沒有上下文)。 在 ASP.NET 內核中,沒有上下文。 所以 Stephen Toub 的示例代碼中的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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM