【问题标题】:Updating a REF param in an Async method callback在异步方法回调中更新 REF 参数
【发布时间】:2019-07-08 08:45:44
【问题描述】:

我正在尝试在异步方法回调中更新由 ref 传递给我的值。

// this Main method parameters are given and can not be changed.
public static void Main(ref System.String msg)
{
    // here we should invoke an async code,
    // which updates the msg parameter.
}

现在,我认识你 can not pass ref values to async methods。但我仍然想以某种方式更新该 ref 值,而不会阻塞我的 UI 线程。 对我来说,这听起来不合理,无法做到。

我尝试了什么:

// Our entry point
public static void Main(ref System.String msg)
{
    Foo(msg);
}

// calls the updater (can't use 'await' on my entry point. its not 'async method')
static async void Foo(ref System.String m)
{
    var progress = new Progress<string>(update => { m = update; });
    await Task.Run(() => MyAsyncUpdaterMethod(progress));
}

// update the variable
static void MyAsyncUpdaterMethod(IProgress<string> progress)
{
    Thread.Sleep(3000);

    if (progress != null)
    {
        progress.Report("UPDATED");
    }
}

显然,由于无法将 msg 参数超出异步方法 lambda 表达式的范围,这将不起作用。我的问题是:什么会?如何实现?

是否可以设置一个全局静态变量来保存 ref 参数,并在回调中使用它?

【问题讨论】:

  • Mainvoid 而不是 Task 背后有什么原因吗?
  • 它是整个RAD应用程序(低代码)的一部分,它使用.net,这就是为什么我提到输入方法不能改变,它给你。

标签: c# asynchronous


【解决方案1】:

你想要突变。 string 是不可变的。一种解决方案是为字符串创建一个新的可变容器。

public class StringContainer
{
    public string String { get; set; }
}

static async void Foo(StringContainer container)
{
    var progress = new Progress<string>(update => container.String = update);
    await Task.Run(() => MyAsyncUpdaterMethod(progress));
}

【讨论】:

    【解决方案2】:

    嗯,我看不出你为什么不能单独计算答案,然后在最后分配给ref 值:

    public static void Main(ref System.String msg)
    {
        var result = Foo(msg).GetAwaiter().GetResult();
        msg = result;
    }
    
    private static async Task<string> Foo(string msg)
    {   
        await Task.Delay(3000).ConfigureAwait(false);
        return "UPDATED";
    }
    

    请记住在任何地方添加ConfigureAwait(false) 以避免死锁。

    【讨论】:

      【解决方案3】:

      您仍在同步调用Foo。我会尝试这样的事情:

      public static void Main(ref string msg)
      {
          msg = Foo(msg);
      }
      
      public static string Foo(string msg)
      {
          return Task.Run(async ()=> await DoAsyncWork(msg)).Result;
      }
      
      async Task<string> DoAsyncWork(string msg)
      {
          string result = await DoMaybeSomeOtherTask(msg);
          return result;
      }
      

      现在,我们不知道它是控制台还是窗口应用程序。如果它是一个窗口应用程序,则不应在 Task 上调用 ResultWait() 方法,因为这可能导致死锁。如果是控制台应用程序,您可以这样做。

      如果是窗口应用程序,您应该运行 Task.Run 略有不同。您也可以使用StringBuilder 代替string

      【讨论】:

        【解决方案4】:

        我仍然想以某种方式更新该 ref 值,而不会阻塞我的 UI 线程。对我来说,这听起来不合理,无法做到。

        这是不可能的。

        根据线程的堆栈来考虑它。当ref 参数传递给方法时,该参数只能写入该堆栈帧(或以下)。同时,异步代码在操作完成之前返回到调用方法。所以那里有一个不可逾越的鸿沟:必须保留堆栈帧才能写入ref,并且必须弹出堆栈帧才能异步。这就是ref 与异步代码不兼容的原因。

        更一般地说,ref 不是指针。它在逻辑上相似,但 .NET 是一种“安全”的编程语言,这就是为什么存在“ref 必须存在于堆栈框架中”规则的原因。 .NET 故意阻止诸如将ref 复制到全局变量中,在弹出堆栈帧后可以对其进行更新。

        您有可能通过获取指向对象的指针并以这种方式操作它(使用unsafe 代码)来做一些危险的事情,但在走这条路之前我会仔细考虑很久。

        没有unsafe 代码,您的选择是:

        1. 通过使异步代码同步来保持堆栈帧。请注意,您的 UI 将被阻止。
        2. 通过允许代码异步丢弃堆栈帧。请注意,该堆栈框架之外的任何代码(包括异步代码)都无法更新该 ref 参数。

        当然,核心问题是Main方法签名。这没有任何意义。方法签名是同步的,它必须在 UI 更新之前返回。它确实应该有一个异步签名。

        【讨论】:

          猜你喜欢
          • 2019-11-16
          • 2021-07-03
          • 2014-07-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-08-18
          • 1970-01-01
          相关资源
          最近更新 更多