Getter中仅添加Console.WriteLine时事件才触发的异常问题求助
Console.WriteLine Is Present in a Property Getter Let's break down this weird, non-deterministic issue you're seeing, and walk through both the root causes and practical fixes. First, I get you're cutting corners for draft code brevity—but that getter-side-effect hack is actually the core of the problem, and Console.WriteLine is just a flaky band-aid masking underlying thread and optimization quirks.
Your Scenario Recap
You're running a repeated action via RepeatAction.Every wrapped in Task.Run, checking the AllCyclesEnded property each loop. The getter for this property fires OnCycleEnded and OnCyclesEnded events when conditions are met—but these events only trigger (about 60% of the time) if you leave the Console.WriteLine line in; remove it, and events don't fire at all, even though the getter runs and HaveAllCyclesEnded is true.
Here's your key code for reference:
The Loop Logic
await Task.Run(() => RepeatAction.Every(() => { if (!this.AllCyclesEnded || NumberOfSteps == 1) { if (NumberOfSteps > 1) ++Counter; var tidalVolume = 500; var plateauPressure = SimulatePlateauAdjustment(12f, this.CurrentPEEP); //var plateauPressure = 29; var thisCycle = MeasurementCycle.Creator(this.CurrentPEEP, tidalVolume, plateauPressure); if (!IsIdealPEEP) CycleIterator(thisCycle); } }, TimeSpan.FromSeconds(StepDuration), cancellation.Token).Wait() , abortedCyclesBecauseIdealPEEPFound.Token);
The Problematic Property Getter
public bool AllCyclesEnded { get { if (HaveAllCyclesEnded && !CyclesEndedEventFired) { CyclesEndedEventFired = true; OnCycleEnded(new CycleEndedEventArgs(Cycle.Count > 0 ? Cycle.LastOrDefault() : default , this.Counter, this.CurrentPEEP, this.EndPEEP , this.AllCyclesEnded, this.CyclePhase)); Console.WriteLine($"\t\tEnded @ {this.Counter} loops with currentPEEP of {this.CurrentPEEP} in {this.CyclePhase} phase."); //移除上述代码后,以下事件均不会触发... OnCyclesEnded(new CyclesEndedEventArgs(Cycle.Count > 0 ? Cycle.LastOrDefault() : default , this.Counter, this.CurrentPEEP, this.EndPEEP, this.CyclePhase)); } return HaveAllCyclesEnded; } }
The RepeatAction Helper
public static class RepeatAction { public static async Task Every(Action action, TimeSpan interval, CancellationToken cancellationToken) { while (true) { action(); Task task = Task.Delay(interval, cancellationToken); try { await task; } catch (TaskCanceledException) { return; } } } }
Root Causes to Investigate
JIT Optimization Hates Side-Effect Getters
.NET design rules explicitly state property getters should be side-effect free. The JIT compiler may optimize away code it sees as unnecessary if it doesn't impact the getter's return value. When you removeConsole.WriteLine, the JIT might flag the event-firing code as a "useless" side effect (since the getter returnsHaveAllCyclesEndedregardless) and strip it out. AddingConsole.WriteLineintroduces an I/O operation the compiler can't optimize away, so the event code runs—sometimes.Thread Synchronization & Memory Visibility Gaps
Your code runs across multiple threads: theTask.Runbackground thread, and whatever thread subscribes to your events.- The
CyclesEndedEventFiredflag isn't marked asvolatileor protected by a lock, so the background thread's update to this value might stay trapped in CPU cache and not be visible to other threads. Console.WriteLineimplicitly uses a lock to synchronize output, creating a memory barrier that accidentally forces the flag's updated value to be visible across threads. Without that barrier, the flag's state might be stale, so the event code never runs.
- The
CancellationToken Timing Conflicts
You're passing two different cancellation tokens: one toRepeatAction.Everyand another toTask.Run. It's possible theabortedCyclesBecauseIdealPEEPFound.Tokentriggers cancellation before the event code finishes. TheConsole.WriteLineadds a tiny delay, giving events just enough time to fire before the token cancels the task.
Fixes (From Quick Draft Fixes to Permanent Solutions)
1. Ditch Side-Effect Getters (Permanent Fix)
This is the only real long-term solution. Move the event-firing logic to a dedicated method, and call it explicitly when you need to check for cycle completion:
// Replace the AllCyclesEnded property check in your loop if (!HaveAllCyclesEnded || NumberOfSteps == 1) { // ... existing loop logic } else { FireCycleEndEventsIfNeeded(); } // Dedicated thread-safe method for event logic private void FireCycleEndEventsIfNeeded() { lock (this) { if (!CyclesEndedEventFired) { CyclesEndedEventFired = true; var lastCycle = Cycle.Count > 0 ? Cycle.LastOrDefault() : default; OnCycleEnded(new CycleEndedEventArgs(lastCycle, this.Counter, this.CurrentPEEP, this.EndPEEP, true, this.CyclePhase)); OnCyclesEnded(new CyclesEndedEventArgs(lastCycle, this.Counter, this.CurrentPEEP, this.EndPEEP, this.CyclePhase)); } } } // Keep AllCyclesEnded as a simple, side-effect-free getter public bool AllCyclesEnded => HaveAllCyclesEnded;
2. Fix Memory Visibility (Quick Draft Hack)
If you absolutely must keep the logic in the getter (only for draft code!), fix the thread issues:
- Mark
CyclesEndedEventFiredandHaveAllCyclesEndedasvolatileto ensure their values are visible across threads:private volatile bool CyclesEndedEventFired; private volatile bool HaveAllCyclesEnded; - Wrap the event-firing code in a lock to prevent race conditions:
public bool AllCyclesEnded { get { lock (this) { if (HaveAllCyclesEnded && !CyclesEndedEventFired) { CyclesEndedEventFired = true; // ... event firing code } } return HaveAllCyclesEnded; } }
3. Clean Up CancellationToken Logic
Ensure your cancellation tokens aren't conflicting. If abortedCyclesBecauseIdealPEEPFound.Token is meant to cancel the task when ideal PEEP is found, make sure it only triggers after your events have fired. Alternatively, use a single token for both the repeat action and the outer task to avoid timing issues.
Final Notes
The Console.WriteLine was never a solution—it was just a lucky accident that introduced a memory barrier and prevented JIT optimization. The real fix is adhering to .NET's property design rules and handling multi-threaded code properly. Even for draft code, avoiding side effects in getters will save you hours of debugging weird, non-deterministic issues like this.
内容的提问来源于stack exchange,提问作者Marcelo Scofano Diniz

