@@ -28,6 +28,7 @@ internal class WorkerThread : IDisposable
2828 private readonly ManualResetEvent _runningOpCompleteEvent ; // fired when either m_syncOp finishes, or the kick off of m_async
2929 private readonly Object _eventLock = new object ( ) ; // Locking on an event directly can cause Mono to stop responding.
3030 private readonly Queue < Operation > _postedOperations ; // queue of fire-and-forget operations
31+ private readonly Queue < ( AsyncOperation Operation , Action < Exception > OnError ) > _postedAsyncOperations ; // queue of fire-and-forget async operations
3132
3233 public event EventHandler < Exception > PostedOperationErrorEvent ;
3334
@@ -37,6 +38,13 @@ private class OperationDescriptor
3738 /// Delegate that was added via 'RunOperation'. Is of type 'Operation' or 'AsyncOperation'
3839 /// </summary>
3940 public readonly Delegate Target ;
41+
42+ /// <summary>
43+ /// Handler invoked if the operation faults. For async operations, this may be invoked on a non-worker thread.
44+ /// Only set for PostAsyncOperation.
45+ /// </summary>
46+ public Action < Exception > ErrorHandler ;
47+
4048 public ExceptionDispatchInfo ExceptionDispatchInfo ;
4149 public Task Task ;
4250 private bool _isStarted ;
@@ -81,6 +89,7 @@ public WorkerThread(Logger logger)
8189 _opSet = new AutoResetEvent ( false ) ;
8290 _runningOpCompleteEvent = new ManualResetEvent ( true ) ;
8391 _postedOperations = new Queue < Operation > ( ) ;
92+ _postedAsyncOperations = new Queue < ( AsyncOperation Operation , Action < Exception > OnError ) > ( ) ;
8493
8594 _thread = new Thread ( new ThreadStart ( ThreadFunc ) ) ;
8695 _thread . Name = "MIDebugger.PollThread" ;
@@ -121,6 +130,30 @@ public void RunOperation(string text, CancellationTokenSource canTokenSource, As
121130 SetOperationInternalWithProgress ( op , text , canTokenSource ) ;
122131 }
123132
133+ /// <summary>
134+ /// Queue an async operation to run on the worker thread and return immediately, without waiting for the
135+ /// operation to start or finish. Posted async operations run one at a time, in the order posted, and
136+ /// serialize behind any in-flight operation. Faults are reported to <paramref name="onError"/>.
137+ /// </summary>
138+ public void PostAsyncOperation ( AsyncOperation op , Action < Exception > onError )
139+ {
140+ if ( op == null )
141+ throw new ArgumentNullException ( nameof ( op ) ) ;
142+ if ( onError == null )
143+ throw new ArgumentNullException ( nameof ( onError ) ) ;
144+
145+ if ( _isClosed )
146+ throw new ObjectDisposedException ( "WorkerThread" ) ;
147+
148+ lock ( _postedAsyncOperations )
149+ {
150+ if ( _isClosed )
151+ throw new ObjectDisposedException ( "WorkerThread" ) ;
152+
153+ _postedAsyncOperations . Enqueue ( ( op , onError ) ) ;
154+ _opSet . Set ( ) ;
155+ }
156+ }
124157
125158 public void Close ( )
126159 {
@@ -150,6 +183,22 @@ public void Close()
150183 }
151184 }
152185 }
186+
187+ // Fail any queued async operations that will never run now that we are closed, instead of
188+ // dropping them silently. The poll thread won't promote them once _isClosed is set.
189+ while ( true )
190+ {
191+ Action < Exception > onError ;
192+ lock ( _postedAsyncOperations )
193+ {
194+ if ( _postedAsyncOperations . Count == 0 )
195+ break ;
196+
197+ onError = _postedAsyncOperations . Dequeue ( ) . OnError ;
198+ }
199+
200+ InvokeErrorHandler ( onError , new ObjectDisposedException ( "WorkerThread" ) ) ;
201+ }
153202 }
154203
155204 internal void SetOperationInternal ( Delegate op )
@@ -189,6 +238,7 @@ internal void SetOperationInternalWithProgress(AsyncProgressOperation op, string
189238 }
190239 }
191240 }
241+
192242 public void PostOperation ( Operation op )
193243 {
194244 if ( op == null )
@@ -212,71 +262,154 @@ public void PostOperation(Operation op)
212262
213263 private bool TrySetOperationInternal ( Delegate op )
214264 {
215- lock ( _eventLock )
265+ bool claimed = false ;
266+ try
216267 {
217- if ( _isClosed )
218- throw new ObjectDisposedException ( "WorkerThread" ) ;
219-
220- if ( _runningOp == null )
268+ lock ( _eventLock )
221269 {
222- _runningOpCompleteEvent . Reset ( ) ;
270+ if ( _isClosed )
271+ throw new ObjectDisposedException ( "WorkerThread" ) ;
223272
224- OperationDescriptor runningOp = new OperationDescriptor ( op ) ;
225- _runningOp = runningOp ;
273+ if ( _runningOp == null )
274+ {
275+ _runningOpCompleteEvent . Reset ( ) ;
226276
227- _opSet . Set ( ) ;
277+ OperationDescriptor runningOp = new OperationDescriptor ( op ) ;
278+ _runningOp = runningOp ;
228279
229- _runningOpCompleteEvent . WaitOne ( ) ;
280+ _opSet . Set ( ) ;
230281
231- Debug . Assert ( runningOp . IsComplete , "Why isn't the running op complete?" ) ;
282+ _runningOpCompleteEvent . WaitOne ( ) ;
283+ claimed = true ;
232284
233- if ( runningOp . ExceptionDispatchInfo != null )
234- {
235- runningOp . ExceptionDispatchInfo . Throw ( ) ;
285+ Debug . Assert ( runningOp . IsComplete , "Why isn't the running op complete?" ) ;
286+
287+ if ( runningOp . ExceptionDispatchInfo != null )
288+ {
289+ runningOp . ExceptionDispatchInfo . Throw ( ) ;
290+ }
291+
292+ return true ;
236293 }
294+ }
237295
238- return true ;
296+ return false ;
297+ }
298+ finally
299+ {
300+ // The running-op slot was just freed and _eventLock is now released. If promotion on the poll
301+ // thread lost the TryEnter race against this method, re-arm _opSet so a queued async operation
302+ // is promoted immediately instead of stranded until the next unrelated wakeup.
303+ if ( claimed && HasPostedAsyncOperation ( ) )
304+ {
305+ _opSet . Set ( ) ;
239306 }
240307 }
241-
242- return false ;
243308 }
244309
245310 private bool TrySetOperationInternalWithProgress ( AsyncProgressOperation op , string text , CancellationTokenSource canTokenSource )
246311 {
247312 var waitLoop = new HostWaitLoop ( text ) ;
248313
249- lock ( _eventLock )
314+ bool claimed = false ;
315+ try
250316 {
251- if ( _isClosed )
252- throw new ObjectDisposedException ( "WorkerThread" ) ;
253-
254- if ( _runningOp == null )
317+ lock ( _eventLock )
255318 {
256- _runningOpCompleteEvent . Reset ( ) ;
319+ if ( _isClosed )
320+ throw new ObjectDisposedException ( "WorkerThread" ) ;
257321
258- OperationDescriptor runningOp = new OperationDescriptor ( new AsyncOperation ( ( ) => { return op ( waitLoop ) ; } ) ) ;
259- _runningOp = runningOp ;
322+ if ( _runningOp == null )
323+ {
324+ _runningOpCompleteEvent . Reset ( ) ;
260325
261- _opSet . Set ( ) ;
326+ OperationDescriptor runningOp = new OperationDescriptor ( new AsyncOperation ( ( ) => { return op ( waitLoop ) ; } ) ) ;
327+ _runningOp = runningOp ;
262328
263- waitLoop . Wait ( _runningOpCompleteEvent , canTokenSource ) ;
329+ _opSet . Set ( ) ;
264330
265- Debug . Assert ( runningOp . IsComplete , "Why isn't the running op complete?" ) ;
331+ waitLoop . Wait ( _runningOpCompleteEvent , canTokenSource ) ;
332+ claimed = true ;
266333
267- if ( runningOp . ExceptionDispatchInfo != null )
268- {
269- runningOp . ExceptionDispatchInfo . Throw ( ) ;
334+ Debug . Assert ( runningOp . IsComplete , "Why isn't the running op complete?" ) ;
335+
336+ if ( runningOp . ExceptionDispatchInfo != null )
337+ {
338+ runningOp . ExceptionDispatchInfo . Throw ( ) ;
339+ }
340+
341+ return true ;
270342 }
343+ }
271344
272- return true ;
345+ return false ;
346+ }
347+ finally
348+ {
349+ // The running-op slot was just freed and _eventLock is now released. If promotion on the poll
350+ // thread lost the TryEnter race against this method, re-arm _opSet so a queued async operation
351+ // is promoted immediately instead of stranded until the next unrelated wakeup.
352+ if ( claimed && HasPostedAsyncOperation ( ) )
353+ {
354+ _opSet . Set ( ) ;
273355 }
274356 }
275-
276- return false ;
277357 }
278358
359+ // Called on the poll thread to promote the next posted async operation into the running-op slot when it
360+ // is free. Returns true if an operation was moved into the slot.
361+ private bool TryStartPostedAsyncOperation ( )
362+ {
363+ Debug . Assert ( IsPollThread ( ) , "TryStartPostedAsyncOperation must run on the poll thread." ) ;
364+
365+ // Cheap early-out so the poll loop does not contend on _eventLock when there is nothing to promote.
366+ lock ( _postedAsyncOperations )
367+ {
368+ if ( _isClosed || _postedAsyncOperations . Count == 0 )
369+ return false ;
370+ }
371+
372+ // Never block on _eventLock here: a client in TrySetOperationInternal holds it across
373+ // _runningOpCompleteEvent.WaitOne() until its operation completes, and only the poll thread can
374+ // complete that operation, so blocking here would deadlock. If a client is mid-set, skip promotion;
375+ // it will be retried on a later poll-loop iteration or wakeup.
376+ if ( ! Monitor . TryEnter ( _eventLock ) )
377+ return false ;
378+
379+ try
380+ {
381+ if ( _isClosed || _runningOp != null )
382+ return false ;
383+
384+ ( AsyncOperation Operation , Action < Exception > OnError ) posted ;
385+ lock ( _postedAsyncOperations )
386+ {
387+ if ( _postedAsyncOperations . Count == 0 )
388+ return false ;
389+
390+ posted = _postedAsyncOperations . Dequeue ( ) ;
391+ }
392+
393+ _runningOpCompleteEvent . Reset ( ) ;
394+
395+ // Unlike TrySetOperationInternal, no one waits for completion; faults are routed to the handler.
396+ _runningOp = new OperationDescriptor ( posted . Operation ) { ErrorHandler = posted . OnError } ;
397+
398+ return true ;
399+ }
400+ finally
401+ {
402+ Monitor . Exit ( _eventLock ) ;
403+ }
404+ }
279405
406+ private bool HasPostedAsyncOperation ( )
407+ {
408+ lock ( _postedAsyncOperations )
409+ {
410+ return _postedAsyncOperations . Count > 0 ;
411+ }
412+ }
280413
281414 // Thread routine for the poll loop. It handles calls coming in from the debug engine as well as polling for debug events.
282415 private void ThreadFunc ( )
@@ -292,6 +425,11 @@ private void ThreadFunc()
292425 {
293426 ranOperation = false ;
294427
428+ if ( _runningOp == null )
429+ {
430+ TryStartPostedAsyncOperation ( ) ;
431+ }
432+
295433 OperationDescriptor runningOp = _runningOp ;
296434 if ( runningOp != null && ! runningOp . IsStarted )
297435 {
@@ -333,11 +471,20 @@ private void ThreadFunc()
333471
334472 if ( ! completeAsync )
335473 {
474+ // Capture the fault before clearing the slot so a synchronous throw is still reported.
475+ Action < Exception > errorHandler = runningOp . ErrorHandler ;
476+ ExceptionDispatchInfo exceptionDispatchInfo = runningOp . ExceptionDispatchInfo ;
477+
336478 runningOp . MarkComplete ( ) ;
337479
338480 Debug . Assert ( _runningOp == runningOp , "How did m_runningOp change?" ) ;
339481 _runningOp = null ;
340482 _runningOpCompleteEvent . Set ( ) ;
483+
484+ if ( errorHandler != null && exceptionDispatchInfo != null )
485+ {
486+ InvokeErrorHandler ( errorHandler , exceptionDispatchInfo . SourceException ) ;
487+ }
341488 }
342489 }
343490
@@ -389,8 +536,43 @@ internal void OnAsyncRunningOpComplete(Task t)
389536 }
390537 }
391538 _runningOp . MarkComplete ( ) ;
539+
540+ // Capture the fault before clearing the slot so it is routed to the handler, not discarded.
541+ Action < Exception > errorHandler = _runningOp . ErrorHandler ;
542+ ExceptionDispatchInfo exceptionDispatchInfo = _runningOp . ExceptionDispatchInfo ;
543+
544+ // Invoke the handler while the running-op slot is still held so it does not run concurrently
545+ // with the next queued operation. This may still run on the task's completion thread.
546+ if ( errorHandler != null && exceptionDispatchInfo != null )
547+ {
548+ InvokeErrorHandler ( errorHandler , exceptionDispatchInfo . SourceException ) ;
549+ }
550+
392551 _runningOp = null ;
393552 _runningOpCompleteEvent . Set ( ) ;
553+
554+ lock ( _postedAsyncOperations )
555+ {
556+ if ( _postedAsyncOperations . Count > 0 )
557+ {
558+ _opSet . Set ( ) ;
559+ }
560+ }
561+ }
562+
563+ private void InvokeErrorHandler ( Action < Exception > errorHandler , Exception exception )
564+ {
565+ try
566+ {
567+ errorHandler ( exception ) ;
568+ }
569+ catch ( Exception e ) when ( ExceptionHelper . BeforeCatch ( e , Logger , reportOnlyCorrupting : false ) )
570+ {
571+ if ( PostedOperationErrorEvent != null )
572+ {
573+ PostedOperationErrorEvent ( this , e ) ;
574+ }
575+ }
394576 }
395577
396578 internal bool IsPollThread ( )
0 commit comments