Как сделать TaskCompletionSource.Task завершенным, используя определенный TaskScheduler

Как сделать доработку TaskCompletionSource.Task случиться на конкретном TaskSchedulerкогда я звоню TaskCompletionSource.SetResult?

В настоящее время я использую идею, которую я позаимствовал из этого поста:

static public Task<TResult> ContinueOnTaskScheduler<TResult>(
    this Task<TResult> @this, TaskScheduler scheduler)
{
    return @this.ContinueWith(
        antecedent => antecedent,
        CancellationToken.None,
        TaskContinuationOptions.ExecuteSynchronously,
        scheduler).Unwrap();
}

Поэтому, когда бы я ни вернулся TaskCompletionSource.Task звонящему я сейчас вернусь TaskCompletionSource.Task.ContinueOnTaskScheduler(scheduler) вместо.

Можно ли как-то избежать этого другого уровня косвенности ContinueWith?

1 ответ

Решение

Было бы интересно узнать ваши цели за этим. Во всяком случае, если вы хотите избежать накладных расходов ContinueWith (который я считаю довольно низким), вам, вероятно, придется придумать свою собственную версию шаблона, похожую на TaskCompletionSource,

Это не так сложно. Например, что-то вроде Promise ниже могут быть использованы так же, как вы используете TaskCompletionSource, но позволил бы предоставить кастом TaskScheduler для завершения (отказ от ответственности: почти не проверен):

public class Promise
{
    readonly Task _task;
    readonly CancellationTokenSource _cts;
    readonly object _lock = new Object();
    Action _completionAction = null;

    // public API

    public Promise()
    {
        _cts = new CancellationTokenSource();
        _task = new Task(InvokeCompletionAction, _cts.Token); 
    }

    public Task Task { get { return _task; } }

    public void SetCompleted(TaskScheduler sheduler = null)
    {
        lock(_lock)
            Complete(sheduler);
    }

    public void SetException(Exception ex, TaskScheduler sheduler = null)
    {
        lock (_lock)
        {
            _completionAction = () => { throw ex; };
            Complete(sheduler);
        }
    }

    public void SetException(System.Runtime.ExceptionServices.ExceptionDispatchInfo edi, TaskScheduler sheduler = null)
    {
        lock (_lock)
        {
            _completionAction = () => { edi.Throw(); };
            Complete(sheduler);
        }
    }

    public void SetCancelled(TaskScheduler sheduler = null)
    {
        lock (_lock)
        {
            // don't call _cts.Cancel() outside _completionAction
            // otherwise the cancellation won't be done on the sheduler
            _completionAction = () =>
            {
                _cts.Cancel();
                _cts.Token.ThrowIfCancellationRequested();
            };
            Complete(sheduler);
        }
    }

    // implementation

    void InvokeCompletionAction()
    {
        if (_completionAction != null)
            _completionAction();
    }

    void Complete(TaskScheduler sheduler)
    {
        if (Task.Status != TaskStatus.Created)
            throw new InvalidOperationException("Invalid task state.");
        _task.RunSynchronously(sheduler?? TaskScheduler.Current);
    }
}

Кстати, эта версия имеет переопределение для SetException(ExceptionDispatchInfo edi), чтобы вы могли распространять состояние активного исключения изнутри catch:

catch(Exception ex)
{
    var edi = ExceptionDispatchInfo.Capture(ex);
    promise.SetException(edi);
}

Также легко создать общую версию этого.

Однако у этого подхода есть и обратная сторона. Третья сторона может сделать promise.Task.Run или же promise.Task.RunSynchronouslyкак Task выставлен в TaskStatus.Created государство.

Вы можете добавить чек для этого в InvokeCompletionActionили, возможно, вы могли бы скрыть это, используя вложенные задачи / Task.Unwrap (хотя последний принес бы некоторые накладные расходы обратно).

Другие вопросы по тегам