C# 幽灵般的远距离动作 --await 块中的值类型发生变化?

问题描述 投票:0回答:1

我对用 C# 编写一个重要的项目(在 Godot 4 平台上使用)还很陌生,但对一般编程不太熟悉。 我有以下片段:

public async Task RequestAcknowledgementEvent(uint eventId)
    {
        Log($"Waiting for acknowledgement of event {eventId}");
        //read from the channel until it yields an event with this id
        await foreach (var e in serverEventsInFlightChannel.Reader.ReadAllAsync())
        {
            Log($"Wait for ack of evnt {eventId}");
            Log("Got event from channel " + e);
            if (e != eventId) continue;
            Log($"Event {e} acked");
            return;
        }
    }

这个函数在这里被调用:

 private async Task SendCountedClientEvent(
        ClientEvent clientEvent)
    {
        if (clientStream is null)
        {
            Log("Client is null", LogLevel.Error);
            return;
        }

        var eid = Interlocked.Increment(ref _eventCounter) - 1;
        clientEvent.EventId = eid;
        //send event to server
        GD.Print("Send event to server" + clientEvent);
        clientStream.RequestStream.WriteAsync(clientEvent).Wait();
        //wait for acknowledgement
        await RequestAcknowledgementEvent(clientEvent.EventId);
        Log("Event " + clientEvent.EventId + " acknowledged");
    }

相当简单的事情——向服务器发送请求,并等待服务器完成此特定请求后返回(流量通过 grpc 流发送)。 当客户端收到服务器的确认事件时:

case ServerEvent.EventOneofCase.Acknowledge:
 var ack = serverEvent.Acknowledge!;
 //see if there's any client event that server acknowledged
 Log("Got acknowledge event with id " + ack.EventId);
 await serverEventsInFlightChannel.Writer.WriteAsync(ack.EventId);
 Log("Wrote event " + ack.EventId + " to channel");

serverEventsInFlightChannel
Channel
的 C#
uint

我遇到的真正令人困惑的问题是,

eventId
(该函数中的第一条日志消息)开头的
RequestAcknowledgementEvent
的值比await块中的eventId的值(该函数中的第二条日志消息)大1。功能)。从结构上来说,这应该是不可能的?
uint
应该被克隆到函数中,任何人都没有办法改变它,即使是跨线程,对吧? 如果我没记错的话,你不能分发对值类型的可变引用! 我假设等待块与它有关?

有谁知道这是怎么发生的(以及我如何解决它)? 我很困惑。

c# .net async-await race-condition godot
1个回答
0
投票
public async Task RequestAcknowledgementEvent(uint eventId)
{
    Log($"Waiting for acknowledgement of event {eventId}");
    //read from the channel until it yields an event with this id
    await foreach (var e in serverEventsInFlightChannel.Reader.ReadAllAsync())
    {
        Log($"Wait for ack of evnt {eventId}");
        Log("Got event from channel " + e);
        if (e != eventId) continue;
        Log($"Event {e} acked");
        return;
    }
}

当您到达

await foreach (var e in serverEventsInFlightChannel.Reader.ReadAllAsync())
行时,正在执行此函数的线程将返回到调用函数并可以自由地执行其他工作。如果它是 UI 线程,它可以继续执行 UI 操作,或者如果它是线程池线程,则它会返回到线程池并可以执行其他工作。假设有人使用 eventId 10 调用此方法,那么您会收到一条日志,显示“正在等待事件 10 的确认”,但是当它进入等待状态时,线程将被释放以在等待响应时执行其他工作。在等待时,也许另一个调用者使用 eventId 11 调用此方法,因此您会收到另一个“等待事件 11 的确认”,也许这就是您遇到断点的地方,因此 eventId 显示 11,但随后您单步执行在调试器中,您认为您将进入 foreach 块并期待“等待 evnt 11 的确认”,但实际上 11 的调用线程在等待响应时被释放以执行其他工作,但之前调用的响应eventId 10 已准备就绪,因此您不是在调用 eventId 11 的上下文中继续,而是在之前调用 eventId 10 的上下文中继续。您认为您仍在同一方法调用中,但实际上并不在即使您在调试器中执行了“单步”,也会调用相同的方法。

还需要记住的是,当使用

await foreach
时,您并不是在等待项目集合,而是只等待一次。相反,您正在等待集合中的每一项。每次迭代 foreach 块时,您都在等待枚举中的下一项,因此每次循环 foreach 块时,上下文都可能会混淆。当我说上下文可能会混淆时,我的意思是您可以有多个上下文同时调用同一个 foreach 块,并且不能保证上下文的执行顺序。

© www.soinside.com 2019 - 2024. All rights reserved.