Search code examples
c#asp.netasynchronousvaluetask

How can I synchronously wait for ValueTask without performance loss?


First of all, our platform for this question is C# 9.0 and .NET core 7.0. Lets suppose that I have to wait for a ValueTask to complete in a synchronous method. When dealing with tasks in this context, I generally use the following scheme:

var task = Get_Task_PlaceHolderMethod();
task.Wait(timeoutedToken);
var value = task.Result // use from here

Otherwise I just await, normally.

how can I use something like this to wait for ValueTasks? They don't have any .Wait method and all I can do is convert it to task with .AsTask(), but then I will lose the benefits of the ValueTask creating an instance of a task to wrap it.

I tried to code some logic but always end up with less performant code than before. As following I had to create an instance of a task, so the above implications applies too. I have no benefit from the ValueTask.

private static void SyncallyAwait(Func<ValueTask> getTask, TimeSpan timeout)
{
    var operationWaiter = new CancellationTokenSource();
    var v_task = getTask();
    var timeouter = Task.Delay(timeout, operationWaiter.Token);
    while ( // wait until done
        v_task.IsCompleted is false &&
        timeouter.IsCompleted is false
    ){ }

    if (timeouter.IsCompleted)
        throw ERROR_TaskTimeouted();

    operationWaiter.Cancel();

    if (v_task.IsCompleted is false)
        throw ERROR_TaskNotAccessible();

    //throw any resulting exception
    v_task.GetAwaiter().GetResult();
}

private static readonly Func<ApplicationException> ERROR_TaskNotAccessible = () => new($"""
    Its not possible to ensure that the valuetask is done running, which means that the 
    result is not accurate or safe to access without putting the thread on deadlock.
    to prevent the unexpected lack of response this exception has been thrown
""");

private static readonly Func<TimeoutException> ERROR_TaskTimeouted = () => new($"""
    MovingOperation exceeded the sync waiter timeout. The database didn't returned in
    reasonable time expected.
""");

Another possibility I could try is to use the start time of the v_task and receive a TimeSpan to count the current time in the loop while, checking if the Now - start_time is greater than the limit timeSpan. But that doesn't seem right.


Solution

  • I have not used value tasks that much. But my understanding is that the primary benefit is avoiding allocation of a Task object when the result is already available and the async method can complete synchronously. So I think the correct approach should be:

    var valueTask = getTask();
    if(valueTask.IsCompleted){
        return valueTask.Result;
    }
    return valueTask.AsTask().Result;
    

    If getTask() was really run synchronously, then the returned value task will already be completed, and you can just check this and return the result. If it did not complete synchronously you have to convert it to a task and wait on the task. I do not see any reason why this should cause any performance impact.

    Your example seem to be spinwaiting on the value task, and that is almost certainly not a good approach.