【问题标题】:Is it possible to convert a `ref bool` to a CancellationToken?是否可以将 `ref bool` 转换为 CancellationToken?
【发布时间】:2020-02-27 21:03:40
【问题描述】:

我有一个遗留场景,其中 ref bool 被用于向实现发送取消信号。现在,我想调用一个基于Task 的库方法,该方法采用CancellationToken 实例,我也想在布尔值更改时取消该方法。

这是我必须处理的:

void Method(ref bool isCancelled)
{
    while (!isCancelled)
    {
        ...
        DoThis();
        DoThat();
        ...
    }
}

这就是我想做的:

Task MethodAsync(ref bool isCancelled)
{
    while (!isCancelled)
    {
        ...
        DoThis();
        await DoTheNewThingAsync(isCancelled.ToCancellationToken());
        DoThat();
        ...
    }
}

ToCancellationToken() 当然在这个上下文中不存在,只是用来表示意图。

我尝试创建CancellationTokenSource 的自定义实现,但类中没有我可以使用的虚拟对象。也不能直接创建自定义CancellationToken,因为它是struct,不能被继承。

我知道使用ref bool 是一种不好的做法,但我目前无法更改依赖它的底层实现,所以我需要一种方法来使用它的值作为基于任务的取消机制打电话。

【问题讨论】:

  • 为布尔值创建扩展方法public static CancellationToken(this bool isCancelled){//do stuff}
  • @DetectivePikachu 你如何建议我在布尔值更改时取消令牌?
  • 由于无法在bool 更改值时得到通知,因此无论您想出什么,都需要间歇性地轮询该值以实际触发CancellationToken。这不会是有效的。 CancellationTokenSource 不仅仅是一个简单的是/否取消标志,所以即使有一些黑魔法巫术方法以某种方式给布尔值取别名,你仍然不能将其用作成熟的实现。
  • 你必须使用各种骇人听闻的技巧来完成这项工作,使用后台线程的事件发射器或类似的东西。我认为您最好更新旧版应用程序以在整个过程中使用 CancellationTokens,或者更可能只是坚持他们当前的方案

标签: c# .net async-await task-parallel-library cancellation


【解决方案1】:

我想出了一个可行的解决方案:

public static class TaskRefBoolCancellable
{
    public static T SynchronousAwait<T>(Func<CancellationToken, Task<T>> taskToRun, ref bool isCancelled)
    {
        using (var cts = new CancellationTokenSource())
        {
            var runningTask = taskToRun(cts.Token);

            while (!runningTask.IsCompleted)
            {
                if (isCancelled)
                    cts.Cancel();

                Thread.Sleep(100);
            }

            return runningTask.Result;
        }
    }
}

void Method(ref bool isCancelled)
{
    while (!isCancelled)
    {
        ...
        DoThis();
        var result = TaskRefBoolCancellable.SynchronousAwait(DoTheNewThingAsync, ref isCancelled);
        DoThat();
        ...
    }
}

警告:此代码在调用线程上同步运行。所以不能保证它会很好地与代码的其他部分一起工作,因为它会阻塞调用线程。此外,它还会轮询isCancelled 变量,使其既无效又不会立即取消。

我认为这是一个权宜之计,因为您将 ref bool isCancelled 替换为适当的基于任务的取消。

【讨论】:

    【解决方案2】:

    这很复杂。有几个原因:

    1. 您不能通过ref 将参数传递给async 方法。您正在使用await,但要使用await,您的方法需要标记为async。并且async 方法不能有ref 参数。例如,这不会编译:
    async Task MethodAsync(ref bool isCancelled)
    {
        while (!isCancelled)
        {
            DoThis();
            await DoTheNewThingAsync(isCancelled.ToCancellationToken());
            DoThat();
        }
    }
    

    这会给你编译器错误:

    CS1988:异步方法不能有 ref、in 或 out 参数

    1. 您不能在匿名方法中使用ref 参数。我考虑过使用Timer 来检查变量。像这样:
    public static CancellationToken ToCancellationToken(ref bool isCancelled)
    {
        var tokenSource = new CancellationTokenSource();
    
        var timer = new System.Timers.Timer()
        {
            AutoReset = true,
            Interval = 100
        };
        timer.Elapsed += (source, e) =>
        {
            if (isCancelled)
            {
                tokenSource.Cancel();
                timer.Dispose();
            }
        };
        timer.Enabled = true;
    
        return tokenSource.Token;
    }
    

    但这会给你编译器错误:

    CS1628:不能在匿名方法、lambda 表达式、查询表达式或本地函数中使用 ref、out 或 in 参数“isCancelled”

    我没有看到任何其他方法可以通过引用将bool 放入事件处理程序中。

    1. 我能得到的最接近的是这样的:
    void Method(ref bool isCancelled)
    {
        while (!isCancelled)
        {
            DoThis();
            using (var tokenSource = new CancellationTokenSource()) {
                var mytask = DoTheNewThingAsync(tokenSource.Token);
                while (true)
                {
                    //wait for either the task to finish, or 100ms
                    if (Task.WaitAny(mytask, Task.Delay(100)) == 0)
                    {
                        break; //mytask finished
                    }
                    if (isCancelled) tokenSource.Cancel();
                }
    
                // This will throw an exception if an exception happened in
                // DoTheNewThingAsync. Otherwise we'd never know if it
                // completed successfully or not.
                mytask.GetAwaiter().GetResult();
            }
            DoThat();
        }
    }
    

    但是,这会阻止调用者,所以我不完全明白这有什么用(如果被阻止,调用者如何更改isCancelled?)。但这就是您现有方法正在做的事情,所以也许它会起作用?

    但这是超级hacky。如果您完全可以控制上游的操作方式,请改为这样做。

    【讨论】:

      【解决方案3】:

      如果您正在创建方法async Task 并且仍想使用bool 语义,则必须传递一个对象,以便in 可以保留对bool 值的引用。如果可以在客户端代码中将bool 参数转换为Ref&lt;bool&gt;,则无需任何阻塞操作即可完成此操作:

      public class Ref
      {
          public static Ref<T> Create<T>(T value) => new Ref<T>(value);
      }
      
      public class Ref<T> : Ref
      {
          private T value;
      
          public Ref(T value) => Value = value;
      
          public T Value
          {
              get => value;
              set
              {
                  this.value = value;
                  OnChanged?.Invoke(value);
              }
          }
      
          public override string ToString() => Value?.ToString() ?? "";
          public static implicit operator T(Ref<T> r) => r.Value;
          public event Action<T> OnChanged;
      }
      
      public static class RefExtensions
      {
          public static CancellationToken ToCancellationToken(this Ref<bool> cancelled)
          {
              var cts = new CancellationTokenSource();
              cancelled.OnChanged += value => { if (value) cts.Cancel(); };
              return cts.Token;
          }
      }
      
      public async Task Method(Ref<bool> isCancelled)
      {
          var cancellationToken = isCancelled.ToCancellationToken();
      
          while(!isCancelled)
          {
              ...
              DoThis();
              await DoTheNewThingAsync(cancellationToken);
              DoThat();
              ...
          }
      }
      
      public class Tests
      {
          [Fact]
          public async Task Fact()
          {
              var cancelled = Ref.Create(false);
      
              Task.Run(async () =>
              {
                  await Task.Delay(500);
                  cancelled.Value = true;
              });
      
              var task = Method(cancelled);
              await Task.Delay(1000);
      
              task.Status.Should().Be(TaskStatus.RanToCompletion);
          }
      }
      

      【讨论】:

      • 我认为你需要实现一个Ref&lt;bool&gt;.ToCancellationToken 方法。这就是 OP 想要的!
      • @TheodorZoulias 这个问题是在无法将传入的ref bool 修改为容器对象的情况下提出的。如果可能的话,我可以直接使用CancellationToken 作为替代品(例如,不需要使用Ref&lt;bool&gt;)。
      • @julealgon 但您更改了方法Task MethodAsync(ref bool isCancelled) 的签名。如果您必须保留签名(意味着阻止),那么您必须遵循类似于@GabrielLuci 建议的方法。
      • @AndriiLitvinov 对不起,你是对的。我的例子具有误导性,并不反映我的实际情况。我不是从参数中获取布尔值,而是从基类protected 字段中获取。这意味着我可以随意更改调用站点上的签名,但在这种情况下我不能更改基类及其底层取消机制。
      • @julealgon 那么它使解决方案变得更容易。这意味着您不必拥有ref 参数,对吗?并且可以访问基类的受保护字段吗?如果是这种情况,我最初的答案版本就是这样做的。如果您使用基类和方法的更多详细信息更新问题,我将能够为您提供一些更好的解决方案。您还必须从调用者(基类)的角度决定方法的执行是同步还是异步。我想它是调用你的方法的基类,对吧?
      【解决方案4】:

      这是对 Euphoric 创造性的 solution 的尝试改进。而不是Thread.Sleep,这个使用接受超时的Task.Wait 重载。这样就不会对任务的完成造成额外的延迟。

      public static void Wait(Func<CancellationToken, Task> taskFactory,
          ref bool cancel, int pollInterval = 100)
      {
          using (var cts = new CancellationTokenSource())
          {
              if (cancel) cts.Cancel();
              var task = taskFactory(cts.Token);
              while (!cancel)
              {
                  if (task.Wait(pollInterval)) return;
              }
              cts.Cancel();
              task.Wait();
          }
      }
      

      使用示例:

      Wait(DoTheNewThingAsync, ref isCancelled);
      

      【讨论】:

        猜你喜欢
        • 2023-03-31
        • 2018-11-21
        • 2010-09-28
        • 1970-01-01
        • 2021-10-28
        • 2021-10-15
        • 2020-11-24
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多