Можно ли преобразовать `ref bool` в CancellationToken? - PullRequest
5 голосов
/ 01 ноября 2019

У меня есть унаследованный сценарий, в котором 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 - плохая практика, но в настоящее время я не могуизмените базовую реализацию, которая опирается на нее, поэтому мне нужен способ использовать ее значение в качестве механизма отмены для вызова на основе задач.

Ответы [ 4 ]

6 голосов
/ 01 ноября 2019

Это сложно. По нескольким причинам:

  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

Вы не можете использовать 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: Невозможно использовать ref, out или в параметре isCancelled внутри анонимногометод, лямбда-выражение, выражение запроса или локальная функция

Я не вижу другого способа получить bool в обработчике событий по ссылке.

Самое близкое, что я мог бы получить, это что-то вроде этого:
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, если он заблокирован?). Но это похоже на то, что делает ваш существующий метод, так что, возможно, он будет работать?

Но это супер хакерский. Если вы вообще можете контролировать, как что-либо делается вверх по течению, сделайте это вместо этого.

2 голосов
/ 01 ноября 2019

Я взломал несколько работающее решение:

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 на надлежащую отмену на основе задач.

1 голос
/ 02 ноября 2019

Это попытка усовершенствования изобретательского решения Euphoric . Вместо 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);
1 голос
/ 01 ноября 2019

Если вы создаете метод async Task и по-прежнему хотите использовать семантику bool, вам нужно передать объект, чтобы он мог сохранить ссылку на значение bool. Это можно сделать без каких-либо операций блокировки, если параметр bool можно преобразовать в Ref<bool> в коде клиента:

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);
    }
}
...