【问题标题】:How do I overload function to accept both async and synchronized version of callback parameter如何重载函数以接受回调参数的异步和同步版本
【发布时间】:2020-12-08 01:25:38
【问题描述】:
public static T SyncVer<T>(Func<T> callback)
{
    using (new LogContext("new logging context"))
    {
        try
        {
            return callback();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);

            throw;
        }
    }
}

public static async Task<T> AsyncVer<T>(Func<Task<T>> callback)
{
    using (new LogContext("new logging context"))
    {
        try
        {
            return await callback();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);

            throw;
        }
    }
}

请考虑上面的代码。您可能会看到两个函数中的大部分代码是相同的。我正在寻找一种方法来将它们重载为一个(或者是否有办法取出两个函数的相似部分),这样我就不需要复制内容了?

我们将不胜感激提供的任何帮助。提前致谢。

【问题讨论】:

  • 不要尝试方钉圆孔,只需复制非常少的代码并完成它。将同步版本包装在 Task.FromResult 中只会导致不必要的分配,除非您返回的值的子集非常小,例如 boolints 的一小部分子集

标签: c# async-await task overloading multitasking


【解决方案1】:

我会尝试这样的:

public static T SyncVer<T>(Func<T> callback)
{
    return AsyncVer(() => Task.FromResult(callback())).GetAwaiter().GetResult();
}

【讨论】:

  • 不错的解决方案,点赞。应该注意的是,使用.GetAwaiter().GetResult() 阻塞异步方法可能会导致死锁,以防AsyncVer 还包含其他等待调用(例如await Task.Delay(1000))而没有ConfigureAwait(false),并且在环境中使用安装了同步上下文(例如 WinForms)。
  • @TheodorZoulias 公平点!近十年没有做过winforms :)
【解决方案2】:

注意Task.FromResult会分配,GetAwaiter().GetResult()可能会死锁

就我个人而言,我只会创建两个方法而不用担心它。

但是,另一种(并且稍微安全一点)的方法是将回调包装在 ValueTask 中。 ValueTasks 如果它们同步执行,则效果最好,但是它们有一些细微的限制,永远不应等待超过一次。

假设是,这都是关于创建一个 awaitable 和非 awaitable 委托重载、代码重用以及等待此调用的同步版本对你。

给定

public static async ValueTask<T> SomethingAsync<T>(Func<T> callback)
 =>  await SomethingAsync(() => new ValueTask<T>(callback()));

public static async ValueTask<T> SomethingAsync<T>(Func<Task<T>> callback)
 =>  await SomethingAsync(() => new ValueTask<T>(callback()));

public static async ValueTask<T> SomethingAsync<T>(Func<ValueTask<T>> callback)
{
   using (new LogContext("new logging context"))
   {
      try
      {
         return await callback();
      }
      catch (Exception ex)
      {
         Console.WriteLine(ex);
         throw;
      }
   }
}

用法

public static string DoSomething()
{
   Console.WriteLine("execute sync");
   return "sync result";
}

public static async Task<string> DoSomethingAsync()
{
   Console.WriteLine("Execute async");
   await Task.Delay(100);
   return "async result";
}


...

Console.WriteLine(await SomethingAsync(DoSomething));
Console.WriteLine(await SomethingAsync(DoSomethingAsync));

输出

Create
execute sync
Dispose
sync result
Create
Execute async
Dispose
async result

要提高效率,您可以省略包装器

示例

public static  ValueTask<T> SomethingAsync<T>(Func<T> callback)
{
   try
   {
      return SomethingAsync(() => new ValueTask<T>(callback()));
   }
   catch (Exception e)
   {
      return ValueTask.FromException<T>(e);
   }
}

public static ValueTask<T> SomethingAsync<T>(Func<Task<T>> callback)
{
   try
   {
      return SomethingAsync(() => new ValueTask<T>(callback()));
   }
   catch (Exception e)
   {
      return ValueTask.FromException<T>(e);
   }
}

注意ValueTask.FromException 仅适用于 .NET 5.0+

这种方法的好处:

  • 此方法的同步版本分配更少。
  • 不可能出现死锁
  • 它不会阻塞异步方法
  • 它给你一个价值任务重载

缺点是

  • 您需要将 异步任务 包装在 ValueTask 中,尽管它只是 堆栈分配
  • 您需要为每个方法创建两个重载(总共三个签名)
  • 这两个版本都不能等待两次。
  • 同步版本会稍微慢一些,因为它会创建状态机

注意:我个人从来没有需要这样做,我只会创建两个方法¯\_(ツ)_/¯


其他资源

【讨论】:

  • “假设 [...] 等待此调用的同步版本对您来说不是问题。” SyncVer,因为调用站点不是异步的。否则他们为什么需要它?
  • @TheodorZoulias 是的,如果 OP 想要一个非异步方法,那么你是绝对正确的,但是,用例是如果想要重载并重用代码(如在 using 语句中),最小分配并且不阻塞异步方法。不过说实话,我真的不会这样做:) 另请注意,这在同步路径上慢了大约 10%,但是当在被调用的方法上考虑等待时,一半的分配
  • () =&gt; 也分配。
  • @StephenCleary 是的,我不确定是否有办法缓存它。我猜不是
猜你喜欢
  • 2020-06-28
  • 2018-04-09
  • 2016-09-07
  • 2012-07-25
  • 1970-01-01
  • 2017-04-07
  • 1970-01-01
  • 2020-12-20
相关资源
最近更新 更多