@@ -25,7 +25,20 @@ internal class Cli(string url, string dir) {
2525public static partial class Program {
2626 private static readonly ILogger Logger = CreateLogger ( "Program" ) ;
2727
28- // -1: not initialized yet, 0: initialized but unable to get value
28+ // -1: not initialized yet
29+ // 0: initialized, but unable to determine expected image count
30+ // >0: initialized with expected image count
31+ //
32+ // SAFETY:
33+ // - `_expectedImgCount` is effectively a `static mut`: global and mutable.
34+ // - Only one logical flow (`AreAllImagesLoaded`) ever accesses it.
35+ // - That flow is always sequentially awaited, so no parallel invocations occur.
36+ // - However, the async runtime may resume the flow on different threads.
37+ // Thus, ordinary reads/writes would risk stale or reordered values.
38+ // - Volatile operations are used to enforce safe publication:
39+ // - First write is `VolatileWrite`: publishes initialized value to other threads.
40+ // - First read in subsequent calls is `VolatileRead`: ensures visibility of the published value.
41+ // - Later reads are plain, since the value is immutable after initialization.
2942 private static int _expectedImgCount = - 1 ;
3043
3144 private static async Task BlockRequest ( IBrowserContext context ) {
@@ -133,20 +146,26 @@ async Task<bool> HasReachedCount(int expectedCount) {
133146 return actualCount == expectedCount ;
134147 }
135148
149+ // SAFETY: `VolatileRead` ensures the second and later calls observe
150+ // the published value from the first initialization, even if resumed on another thread.
136151 if ( Thread . VolatileRead ( ref _expectedImgCount ) == - 1 ) { // not cached
137152 // ReSharper disable once StringLiteralTypo
138153 var title = page . Locator ( "h1.focusbox-title" ) ;
139154 var text = await title . InnerTextAsync ( ) ;
140155 var match = ImgCountPattern ( ) . Match ( text ) ;
141156 if ( match . Success ) {
142157 var count = int . Parse ( match . Groups [ 1 ] . Value ) ; // impossible to be negative
158+ // SAFETY: `VolatileWrite` publishes the initialized value
159+ // so subsequent calls on other threads will see it.
143160 Thread . VolatileWrite ( ref _expectedImgCount , count ) ;
144161 return await HasReachedCount ( count ) ; // practically non-zero
145162 } else {
146163 Thread . VolatileWrite ( ref _expectedImgCount , 0 ) ;
147164 return null ;
148165 }
149166 } else { // cached
167+ // SAFETY: after a `VolatileRead` has established visibility,
168+ // ordinary reads are safe, since `_expectedImgCount` never mutates again.
150169 if ( _expectedImgCount == 0 ) {
151170 return null ;
152171 } else {
0 commit comments