使用GetAwaiter().GetResult()和ServiceBusReceiver.PeekMessagesAsync时出现System.InvalidOperationException错误。

3

背景

我们使用GetAwaiter().GetResult(),因为PowerShell的Cmdlet.ProcessRecord()不支持异步/等待。

代码示例

class Program
{
    static async Task Main(string[] args)
    {
        var topicPath = "some-topic";
        var subscriptionName = "some-subscription";
        var connectionString = "some-connection-string";

        var subscriptionPath = EntityNameHelper.FormatSubscriptionPath(
            topicPath,
            subscriptionName
        );

        var serviceBusClient = new ServiceBusClient(connectionString);
        var receiver = serviceBusClient.CreateReceiver(queueName: subscriptionPath);

        // This one works. :-) 
        await foreach (var item in GetMessages(receiver, maxMessagesPerFetch: 5))
        {
            Console.WriteLine("async/await: " + item);
        }

        // This one explodes.
        var enumerator = GetMessages(receiver, maxMessagesPerFetch: 5).GetAsyncEnumerator();
        while (enumerator.MoveNextAsync().GetAwaiter().GetResult())
        {
            // Unhandled exception. System.InvalidOperationException: Operation is not valid due to the current state of the object.
            //    at NonSync.IAsyncEnumerable.Program.GetMessages(ServiceBusReceiver receiver, Int32 maxMessagesPerFetch)+System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult()
            //    at NonSync.IAsyncEnumerable.Program.Main(String[] args) in C:\dev\mediavalet\MediaValet.Learning\entropy\NonSynchronousDotNet\NonSync.IAsyncEnumerable\Program.cs:line 42
            //    at NonSync.IAsyncEnumerable.Program.<Main>(String[] args)
            Console.WriteLine("GetAwaiter().GetResult(): " + enumerator.Current);
        }
    }

    public static async IAsyncEnumerable<string> GetMessages(
        ServiceBusReceiver receiver,
        int maxMessagesPerFetch
    )
    {
        yield return "Foo";
        var messages = await receiver.PeekMessagesAsync(maxMessagesPerFetch);
        yield return "Bar";
    }
}

问题

这里发生了什么?我们如何在不更改 GetMessages 的情况下修复它?

2个回答

3
根据 ValueTask<TResult> 结构的文档:
以下操作不应在 ValueTask<TResult> 实例上执行: • 多次等待实例。 • 多次调用 AsTask。 • 当操作尚未完成时使用 .Result.GetAwaiter().GetResult(),或多次使用它们。 • 使用多种技术来消耗该实例。
如果您执行以上任何操作,则结果是未定义的。
您可以通过使用 AsTask 方法将 ValueTask<bool> 转换为 Task<bool>
while (enumerator.MoveNextAsync().AsTask().GetAwaiter().GetResult())

1
这个答案提供了一个代码示例,补充了Theodor的答案。我们遇到的具体问题是,在ValueTask完成之前,我们调用了GetResult()。文档指定不允许这样做:

一个ValueTask实例只能被等待一次,消费者不能在实例完成之前读取Result。如果这些限制是不可接受的,请通过调用AsTask将ValueTask转换为Task。(强调添加)。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

var enumerator = GetAsyncEnumerable().GetAsyncEnumerator();

while (true)
{
    var moveNext = enumerator.MoveNextAsync();
    var moveNextAwaiter = moveNext.GetAwaiter();

    Console.WriteLine("ValueTask.IsCompleted: {0}", moveNext.IsCompleted);

    try
    {
        if (moveNextAwaiter.GetResult())
        {
            Console.WriteLine("IAsyncEnumerator.Current: {0}", enumerator.Current);
            continue;
        }

        Console.WriteLine("Done! We passed the end of the collection.");
        break;
    }
    catch (InvalidOperationException)
    {
        Console.WriteLine("Boom! GetResult() before the ValueTask completed.");
        continue;
    }
}

async IAsyncEnumerable<int> GetAsyncEnumerable()
{
    yield return 1;
    await Task.Delay(1000);
    yield return 2; // <---- We never access this, because GetResult() explodes.
    yield return 3;
}

输出:

ValueTask.IsCompleted: True
IAsyncEnumerator.Current: 1

ValueTask.IsCompleted: False
Boom! GetResult() before the ValueTask completed.

ValueTask.IsCompleted: True
IAsyncEnumerator.Current: 3

ValueTask.IsCompleted: True
Done! We passed the end of the collection.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接