ValueTask 实例不应直接访问其结果,除非实例已经完成

nop*_*nop 1 .net c# valuetask

有一个库返回 aValueTask并且我有一个消耗 ValueTask 的同步方法。问题是有以下警告:

CA2012:ValueTask 实例不应直接访问其结果,除非该实例已完成。与任务不同,在 ValueTask 上调用 Result 或 GetAwaiter().GetResult() 不能保证在操作完成之前阻塞。如果您不能简单地等待实例,请考虑首先检查其 IsCompleted 属性(或者如果您知道情况如此,则断言它是 true)。

我如何解决它?

public void CreateListenKey()
{
    var result = CreateSpotListenKeyAsync().GetAwaiter().GetResult(); // CA2012: ValueTask instances should not have their result directly accessed unless the instance has already completed. Unlike Tasks, calling Result or GetAwaiter().GetResult() on a ValueTask is not guaranteed to block until the operation completes. If you can't simply await the instance, consider first checking its IsCompleted property (or asserting it's true if you know that to be the case).

    if (result.Success)
    {
        using var document = JsonDocument.Parse(result.Data!);
        lock (_listenKeyLocker)
        {
            if (document.RootElement.TryGetProperty("listenKey", out var listenKeyElement))
            {
                var listenKey = listenKeyElement.GetString();
                ListenKey = listenKey;
            }
        }
    }
}

// library
public async ValueTask<CallResult<string>> CreateSpotListenKeyAsync()
{
    var result = await SendPublicAsync<string>(
        "/api/v3/userDataStream",
        Method.Post);

    return result;
}

// Can't just make it async, because these listen key methods are used in an event handler.
private void OnKeepAliveTimerElapsed(object? sender, ElapsedEventArgs e)
{
    RestApi.PingListenKey();
}
Run Code Online (Sandbox Code Playgroud)

The*_*ias 6

同步等待 a 完成的推荐方法ValueTask是将其转换为 a Task,方法如下AsTask

Task<CallResult<string>> task = CreateSpotListenKeyAsync().AsTask();
var result = task.GetAwaiter().GetResult();

if (result.Success)
{
    //...
Run Code Online (Sandbox Code Playgroud)

在您的情况下,根据该CreateSpotListenKeyAsync方法的实现,此转换很可能会产生零开销。假设ValueTask<CallResult<string>>创建时尚未完成,很可能它在内部包装了 a Task<CallResult<string>>,因此转换将仅返回包装的任务。它似乎不太可能包装接口的实现IValueTaskSource<T>,在这种情况下,转换将产生分配一些轻量级对象的成本。

如果您预计ValueTask<CallResult<string>>创建后会经常完成,您可以像这样优化代码:

var valueTask = CreateSpotListenKeyAsync();
CallResult<string> result;
if (valueTask.IsCompletedSuccessfully)
{
    result = valueTask.Result;
}
else
{
    result = valueTask.AsTask().GetAwaiter().GetResult();
}
Run Code Online (Sandbox Code Playgroud)

上面代码的目的是避免对Task.FromResult方法的内部调用,该方法通常是分配的。

还可以优化IValueTaskSource<T>基于值的任务的同步等待,但这并不是微不足道的,而且预期的好处非常微不足道,以至于不太值得付出努力。


如果是非通用的,您可以按照 @nop 在评论ValueTask中建议的操作:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void GetResult(this in ValueTask valueTask)
{
    if (valueTask.IsCompletedSuccessfully)
    {
        valueTask.GetAwaiter().GetResult();
        return;
    }

    valueTask.AsTask().GetAwaiter().GetResult();
}
Run Code Online (Sandbox Code Playgroud)

但这并不是真正需要的。如果ValueTask已成功完成,则将AsTask永远不会分配,无论 的底层实现是什么ValueTask。如果它由 a 支持IValueTaskSource,则将AsTask返回静态单例Task.CompletedTask。所以简单地做valueTask.AsTask().GetAwaiter().GetResult();几乎是一样的。