├── .gitignore ├── README.md └── Shaman.SingleThreadSynchronizationContext ├── ApplicationHangException.cs ├── Async.SingleThreadSynchronizationContext.cs ├── ResponsivenessWatchdog.cs ├── Shaman.SingleThreadSynchronizationContext.csproj ├── ShamanOpenSourceKey.snk └── SyncCtxExtensions.cs /.gitignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | bin 3 | obj 4 | *.user 5 | *.xproj 6 | project.lock.json 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shaman.SingleThreadSynchronizationContext 2 | Provides a single-threaded `SynchronizationContext` for console applications. 3 | 4 | ## Usage 5 | ```csharp 6 | using Shaman.Runtime; 7 | 8 | static void Main(string args[]) 9 | { 10 | SingleThreadSynchronizationContext.Run(async () => MainAsync(args)); 11 | } 12 | static async Task MainAsync(string args[]) 13 | { 14 | // Tasks awaited here will complete their callbacks on the main thread. 15 | // This makes it easier to reason about async code (by always using coroutines/async, as opposed to real threading). 16 | } 17 | ``` 18 | -------------------------------------------------------------------------------- /Shaman.SingleThreadSynchronizationContext/ApplicationHangException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading; 7 | 8 | namespace Shaman.Runtime 9 | { 10 | public class ApplicationHangException : Exception 11 | { 12 | public long LoopId { get; internal set; } 13 | 14 | public ApplicationHangException(StackTrace st, string threadName) 15 | : base() 16 | { 17 | if (st != null) 18 | { 19 | var toStringMethod = typeof(StackTrace).GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).First(x => x.Name == "ToString" && x.GetParameters().Length == 1); 20 | var stackTrace = toStringMethod.Invoke(st, new object[] { 0 }); 21 | var stackTraceStringField = typeof(Exception).GetField("_stackTraceString", BindingFlags.Instance | BindingFlags.NonPublic); 22 | stackTraceStringField.SetValue(this, stackTrace); 23 | } 24 | this.ThreadName = threadName; 25 | } 26 | 27 | public TimeSpan HangTime { get; internal set; } 28 | public string ThreadName { get; private set; } 29 | private static bool IsMono = typeof(string).GetTypeInfo().Assembly.GetType("Mono.Runtime") != null; 30 | 31 | public static ApplicationHangException CreateForThread(Thread thread) 32 | { 33 | StackTrace tt = null; 34 | if (!IsMono) 35 | { 36 | #if !CORECLR 37 | #pragma warning disable 0618 38 | try 39 | { 40 | thread.Suspend(); 41 | } 42 | catch 43 | { 44 | return null; 45 | } 46 | try 47 | { 48 | tt = new System.Diagnostics.StackTrace(thread, false); 49 | } 50 | catch (ThreadStateException) 51 | { 52 | } 53 | try 54 | { 55 | thread.Resume(); 56 | } 57 | catch (Exception) 58 | { 59 | Thread.Sleep(3000); 60 | thread.Resume(); 61 | } 62 | #pragma warning restore 0618 63 | #endif 64 | } 65 | 66 | 67 | return new ApplicationHangException( 68 | tt, 69 | thread.Name 70 | ); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Shaman.SingleThreadSynchronizationContext/Async.SingleThreadSynchronizationContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Reflection; 8 | using System.Runtime.CompilerServices; 9 | using System.Text; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace Shaman.Runtime 14 | { 15 | public sealed class SingleThreadSynchronizationContext : SynchronizationContext 16 | { 17 | private int threadId; 18 | 19 | private struct Job 20 | { 21 | public Job(SendOrPostCallback action, object state, TaskCompletionSource onCompleted) 22 | { 23 | this.Action = action; 24 | this.State = state; 25 | this.OnCompleted = onCompleted; 26 | } 27 | public readonly SendOrPostCallback Action; 28 | public readonly object State; 29 | public readonly TaskCompletionSource OnCompleted; 30 | } 31 | 32 | private BlockingCollection m_queue = new BlockingCollection(); 33 | private LateArrivalBehavior lateArrivalBehavior; 34 | private SingleThreadSynchronizationContext onFailureForwardTo; 35 | private int firstLateArrivalCallbackExecuted; 36 | private TaskCompletionSource drainingCompletion = new TaskCompletionSource(); 37 | public Thread Thread { get; private set; } 38 | #if SHAMAN_CORE 39 | internal bool hasInstalledResponsivenessWatchdog; 40 | private static MethodInfo SendMethod; 41 | private static Func SendGetAction; 42 | #endif 43 | private static MethodInfo BoringAsyncMethod; 44 | 45 | private static Type ContinuationWrapperType; 46 | private static Func ContinuationWrapperGetContinuation; 47 | private static Func ContinuationWrapperGetInnerTask; 48 | private static Func ContinuationWrapperGetInvokeAction; 49 | private static Func MoveNextRunnerGetStateMachine; 50 | 51 | 52 | static SingleThreadSynchronizationContext() 53 | { 54 | var syncCtxAwaitContinuationType = Type.GetType("System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation+<>c__DisplayClass2"); 55 | if (syncCtxAwaitContinuationType != null) 56 | BoringAsyncMethod = syncCtxAwaitContinuationType.GetMethod("<_cctor>b__3", BindingFlags.Instance | BindingFlags.NonPublic); 57 | ContinuationWrapperType = Type.GetType("System.Runtime.CompilerServices.AsyncMethodBuilderCore+ContinuationWrapper"); 58 | 59 | if (ContinuationWrapperType != null) 60 | { 61 | ContinuationWrapperGetContinuation = GetGetter(ContinuationWrapperType, "m_continuation"); 62 | ContinuationWrapperGetInnerTask = GetGetter(ContinuationWrapperType, "m_innerTask"); 63 | ContinuationWrapperGetInvokeAction = GetGetter(ContinuationWrapperType, "m_invokeAction"); 64 | } 65 | 66 | var moveNextRunnerType = Type.GetType("System.Runtime.CompilerServices.AsyncMethodBuilderCore+MoveNextRunner"); 67 | if (moveNextRunnerType != null) 68 | MoveNextRunnerGetStateMachine = GetGetter(moveNextRunnerType, "m_stateMachine"); 69 | 70 | #if SHAMAN_CORE 71 | SendMethod = typeof(SynchronizationContextExtensionMethods).GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic).Select(x => x.GetMethod("b__0", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)).SingleOrDefault(x => x != null); 72 | if (SendMethod != null) 73 | SendGetAction = GetGetter(SendMethod.DeclaringType, "action"); 74 | #endif 75 | } 76 | 77 | private static Func GetGetter(Type declaringType, string fieldName) 78 | { 79 | var field = declaringType.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance); 80 | var param = Expression.Parameter(typeof(object), "obj"); 81 | var lambda = Expression.Lambda>(Expression.Field(Expression.Convert(param, declaringType), field), param); 82 | return lambda.Compile(); 83 | } 84 | 85 | public override void Post(SendOrPostCallback d, object state) 86 | { 87 | Interlocked.Increment(ref CtxSwitchCount); 88 | 89 | EnqueueJob(new Job(d, state, null), true); 90 | } 91 | 92 | 93 | #if CORECLR 94 | static Func StackTraceCtor; 95 | #endif 96 | 97 | private static StackTrace GetStackTrace(bool needFileInfo) 98 | { 99 | #if CORECLR 100 | if (StackTraceCtor == null) 101 | { 102 | StackTraceCtor = ReflectionHelper.GetWrapper>(typeof(StackTrace), ".ctor"); 103 | } 104 | return StackTraceCtor(needFileInfo); 105 | #else 106 | return new StackTrace(needFileInfo); 107 | #endif 108 | } 109 | 110 | private bool EnqueueJob(Job job, bool allowForward) 111 | { 112 | 113 | if (!completed) 114 | { 115 | try 116 | { 117 | var q = m_queue; 118 | q.Add(job); 119 | return true; 120 | } 121 | catch (InvalidOperationException) 122 | { 123 | } 124 | } 125 | 126 | if (onFirstLateArrival != null) 127 | { 128 | if (Interlocked.Increment(ref firstLateArrivalCallbackExecuted) == 1) 129 | { 130 | onFirstLateArrival(); 131 | onFirstLateArrival = null; 132 | } 133 | } 134 | 135 | if (allowForward && onFailureForwardTo != null) return onFailureForwardTo.EnqueueJob(job, false); 136 | 137 | if (lateArrivalBehavior == LateArrivalBehavior.Throw) 138 | { 139 | #if DESKTOP 140 | var frames = GetStackTrace(false).GetFrames(); 141 | foreach (var frame in frames) 142 | { 143 | var m = frame.GetMethod(); 144 | if (m != null) 145 | { 146 | if (m.DeclaringType == typeof(SingleThreadSynchronizationContext)) continue; 147 | if (m.DeclaringType.GetTypeInfo().Assembly == typeof(Task).GetTypeInfo().Assembly && m.DeclaringType.Name == "SynchronizationContextAwaitTaskContinuation") 148 | return false; // We have to pretend nothing happened and not to throw the exception 149 | break; 150 | } 151 | } 152 | #else 153 | var frames = new Exception().StackTrace.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); 154 | foreach (var frame in frames) 155 | { 156 | if (frame.Contains("SingleThreadSynchronizationContext")) continue; 157 | if (frame.Contains("SynchronizationContextAwaitTaskContinuation")) return false; 158 | break; 159 | } 160 | #endif 161 | throw new InvalidOperationException("The SingleThreadSynchronizationContext has completed and is no longer accepting continuations or callbacks."); 162 | } 163 | if (lateArrivalBehavior == LateArrivalBehavior.SpawnNewThread) 164 | { 165 | lock (this) 166 | { 167 | if (lateArrivalSyncCtx == null || !lateArrivalSyncCtx.EnqueueJob(job, false)) 168 | { 169 | var readyTcs = new TaskCompletionSource(); 170 | var firstContinuationExecutedTcs = new TaskCompletionSource(); 171 | Action deleg = () => 172 | { 173 | SingleThreadSynchronizationContext.Run(async () => 174 | { 175 | var ctx = (SingleThreadSynchronizationContext)SynchronizationContext.Current; 176 | ctx.onFailureForwardTo = this; 177 | lateArrivalSyncCtx = ctx; 178 | readyTcs.SetResult(true); 179 | drainingCompletion.Task.Wait(); 180 | await firstContinuationExecutedTcs.Task; 181 | await Task.Delay(10000); 182 | }, LateArrivalBehavior.Suppress); 183 | }; 184 | #if DESKTOP 185 | new Thread(() => deleg()) { Name = "Respawned thread for SingleThreadSynchronizationContext" }.Start(); 186 | #else 187 | Task.Run(deleg); 188 | #endif 189 | readyTcs.Task.Wait(); 190 | if (!lateArrivalSyncCtx.EnqueueJob(job, false)) throw new Exception(); 191 | firstContinuationExecutedTcs.SetResult(true); 192 | } 193 | return true; 194 | } 195 | } 196 | 197 | 198 | return false; 199 | 200 | } 201 | 202 | private SingleThreadSynchronizationContext lateArrivalSyncCtx; 203 | private bool completed; 204 | private Action onFirstLateArrival; 205 | 206 | public bool HasPendingContinuations 207 | { 208 | get 209 | { 210 | return this.m_queue.Count != 0; 211 | } 212 | } 213 | 214 | public bool HasCompleted 215 | { 216 | get { return completed; } 217 | } 218 | 219 | public override void Send(SendOrPostCallback d, object state) 220 | { 221 | if (Environment.CurrentManagedThreadId == threadId) 222 | { 223 | d(state); 224 | } 225 | else 226 | { 227 | var tcs = new TaskCompletionSource(); 228 | if (!EnqueueJob(new Job(d, state, tcs), true)) 229 | throw new InvalidOperationException("The target SynchronizationContext has completed and is no longer accepting tasks."); 230 | Interlocked.Increment(ref CtxSwitchCount); 231 | tcs.Task.Wait(); 232 | } 233 | } 234 | 235 | internal static long CtxSwitchCount; 236 | 237 | public long LoopId 238 | { 239 | get 240 | { 241 | return loopId; 242 | 243 | } 244 | 245 | } 246 | 247 | private void RunOnCurrentThread() 248 | { 249 | while (true) 250 | { 251 | Job workItem; 252 | Interlocked.Increment(ref loopId); 253 | try 254 | { 255 | var queue = m_queue; 256 | if (queue == null) return; 257 | if (!queue.TryTake(out workItem, Timeout.Infinite, CancellationToken.None)) return; 258 | } 259 | catch (OperationCanceledException) 260 | { 261 | return; 262 | } 263 | Volatile.Write(ref IsBusy, true); 264 | Interlocked.Increment(ref loopId); 265 | bool faulted = false; 266 | ticksStart = Stopwatch.GetTimestamp(); 267 | try 268 | { 269 | workItem.Action(workItem.State); 270 | } 271 | catch (Exception ex) 272 | { 273 | if (workItem.OnCompleted != null) workItem.OnCompleted.SetException(ex); 274 | faulted = true; 275 | } 276 | if (!faulted && workItem.OnCompleted != null) workItem.OnCompleted.SetResult(true); 277 | 278 | 279 | var handler = OnEventLoopIterationExecuted; 280 | if (handler != null) 281 | { 282 | var ticksEnd = Stopwatch.GetTimestamp(); 283 | var elapsedMs = (double)(ticksEnd - ticksStart) / Stopwatch.Frequency; 284 | if (elapsedMs >= MinimumEventLoopIterationDurationForCallback.TotalSeconds) 285 | { 286 | handler(this, ticksStart, ticksEnd, GetMethod(workItem), GetMethod(previousWorkItem)); 287 | } 288 | } 289 | previousWorkItem = workItem; 290 | Volatile.Write(ref IsBusy, false); 291 | ticksStart = 0; 292 | } 293 | } 294 | 295 | private Job previousWorkItem; 296 | public TimeSpan MinimumEventLoopIterationDurationForCallback { get; set; } 297 | 298 | internal bool IsBusy; 299 | 300 | 301 | private MethodInfo GetMethod(Job job) 302 | { 303 | if (job.Action == null) return null; 304 | var method = job.Action.GetMethodInfo(); 305 | if (method == BoringAsyncMethod) 306 | { 307 | var deleg = (Delegate)job.State; 308 | var contWrapper = deleg.Target; 309 | 310 | 311 | while (contWrapper.GetType() == ContinuationWrapperType) contWrapper = ContinuationWrapperGetContinuation(contWrapper).Target; 312 | var stateMachine = MoveNextRunnerGetStateMachine(contWrapper); 313 | method = stateMachine.GetType().GetMethod("MoveNext", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 314 | } 315 | #if SHAMAN_CORE 316 | else if (method == SendMethod) 317 | { 318 | var action = SendGetAction(job.Action.Target); 319 | method = action.GetMethodInfo(); 320 | } 321 | #endif 322 | return method; 323 | } 324 | 325 | public Action OnEventLoopIterationExecuted { get; set; } 326 | 327 | public static new SingleThreadSynchronizationContext Current 328 | { 329 | get 330 | { 331 | return (SingleThreadSynchronizationContext)SynchronizationContext.Current; 332 | } 333 | } 334 | 335 | internal long ticksStart; 336 | 337 | private SingleThreadSynchronizationContext() 338 | { 339 | 340 | threadId = Environment.CurrentManagedThreadId; 341 | 342 | } 343 | 344 | 345 | //~SingleThreadSynchronizationContext() 346 | //{ 347 | // Console.WriteLine("Finalizing " + this.GetHashCode() + " ***************************"); 348 | //} 349 | 350 | public static SingleThreadSynchronizationContext CreateInNewThread() 351 | { 352 | return CreateInNewThread(LateArrivalBehavior.Throw); 353 | } 354 | public static SingleThreadSynchronizationContext CreateInNewThread(LateArrivalBehavior lateArrivalBehavior) 355 | { 356 | return CreateInNewThread(lateArrivalBehavior, null); 357 | } 358 | public static SingleThreadSynchronizationContext CreateInNewThread(LateArrivalBehavior lateArrivalBehavior, string threadName) 359 | { 360 | var tcs = new TaskCompletionSource(); 361 | #if DESKTOP 362 | var thread = new Thread(() => 363 | { 364 | try 365 | { 366 | Run(() => 367 | { 368 | tcs.TrySetResult((SingleThreadSynchronizationContext)SynchronizationContext.Current); 369 | return new TaskCompletionSource().Task; 370 | }, lateArrivalBehavior); 371 | } 372 | catch (Exception ex) 373 | { 374 | tcs.TrySetException(ex); 375 | } 376 | 377 | }); 378 | if (threadName != null) thread.Name = threadName; 379 | thread.IsBackground = true; 380 | #if !CORECLR 381 | thread.SetApartmentState(ApartmentState.STA); 382 | #endif 383 | thread.IsBackground = true; 384 | thread.Start(); 385 | 386 | #else 387 | Task.Run(()=> 388 | { 389 | try 390 | { 391 | Run(() => 392 | { 393 | tcs.TrySetResult((SingleThreadSynchronizationContext)SynchronizationContext.Current); 394 | return new TaskCompletionSource().Task; 395 | }, lateArrivalBehavior); 396 | } 397 | catch (Exception ex) 398 | { 399 | tcs.TrySetException(ex); 400 | } 401 | }); 402 | #endif 403 | tcs.Task.Wait(); 404 | return tcs.Task.Result; 405 | } 406 | 407 | public static void Run(Func func) 408 | { 409 | Run(func, LateArrivalBehavior.Throw, null); 410 | } 411 | public static void Run(Func func, LateArrivalBehavior lateArrivalBehavior) 412 | { 413 | Run(func, lateArrivalBehavior, null); 414 | } 415 | 416 | 417 | public static void Run(Func func, Action onFirstLateArrival) 418 | { 419 | Run(func, LateArrivalBehavior.Suppress, onFirstLateArrival); 420 | } 421 | 422 | private static void Run(Func func, LateArrivalBehavior lateArrivalBehavior, Action onFirstLateArrival) 423 | { 424 | if (func == null) throw new ArgumentNullException("func"); 425 | 426 | var prevCtx = SynchronizationContext.Current; 427 | 428 | var syncCtx = new SingleThreadSynchronizationContext(); 429 | try 430 | { 431 | syncCtx.Thread = Thread.CurrentThread; 432 | syncCtx.onFirstLateArrival = onFirstLateArrival; 433 | //Console.WriteLine("Running " + syncCtx.GetHashCode() + " on thread " + Environment.CurrentManagedThreadId); 434 | syncCtx.lateArrivalBehavior = lateArrivalBehavior; 435 | SynchronizationContext.SetSynchronizationContext(syncCtx); 436 | 437 | var t = func(); 438 | if (t == null) throw new InvalidOperationException("No task provided."); 439 | t.ContinueWith(delegate 440 | { 441 | try 442 | { 443 | var q = syncCtx.m_queue; 444 | if (q != null) 445 | { 446 | q.CompleteAdding(); 447 | } 448 | 449 | } 450 | catch 451 | { 452 | // TODO should we do something here? can it happen? 453 | //Console.WriteLine("Perdindirindina"); 454 | } 455 | syncCtx.completed = true; 456 | }, TaskScheduler.Default); 457 | syncCtx.RunOnCurrentThread(); 458 | if (!syncCtx.aborted) 459 | { 460 | t.GetAwaiter().GetResult(); 461 | } 462 | if (syncCtx.aborted) throw new OperationCanceledException("The SingleThreadSynchronizationContext was aborted."); 463 | } 464 | finally 465 | { 466 | SynchronizationContext.SetSynchronizationContext(prevCtx); 467 | 468 | var q = syncCtx.m_queue; 469 | if (q != null) 470 | q.Dispose(); 471 | syncCtx.m_queue = null; 472 | 473 | var d = syncCtx.drainingCompletion; 474 | if (d != null) d.TrySetResult(true); 475 | 476 | } 477 | } 478 | 479 | internal long loopId; 480 | private volatile bool aborted; 481 | #if SHAMAN_CORE 482 | internal long responsivenessWatchdogThresholdStopwatchTicks; 483 | public ApplicationHangException LastHangException { get; internal set; } 484 | 485 | internal long lastHangLoopId; 486 | internal long lastSubmittedHangLoopId; 487 | #endif 488 | public enum LateArrivalBehavior 489 | { 490 | Throw, 491 | Suppress, 492 | SpawnNewThread 493 | } 494 | 495 | public void Abort() 496 | { 497 | completed = true; 498 | aborted = true; 499 | var q = this.m_queue; 500 | if (q != null) 501 | { 502 | 503 | try 504 | { 505 | q.CompleteAdding(); 506 | } 507 | catch (ObjectDisposedException) 508 | { 509 | } 510 | try 511 | { 512 | if (q.Count == 0) 513 | { 514 | q.Dispose(); 515 | this.m_queue = null; 516 | } 517 | } 518 | catch (ObjectDisposedException) 519 | { 520 | } 521 | var f = this.onFailureForwardTo; 522 | if (f != null) 523 | f.Abort(); 524 | this.onFailureForwardTo = null; 525 | f = this.lateArrivalSyncCtx; 526 | if (f != null) 527 | f.Abort(); 528 | this.lateArrivalSyncCtx = null; 529 | this.onFirstLateArrival = null; 530 | } 531 | } 532 | 533 | public static Task BackgroundAsync() 534 | { 535 | if (SingleThreadSynchronizationContext.Current == null) throw new InvalidOperationException("Must be called from a SingleThreadSynchronizationContext."); 536 | return new TaskCompletionSource().Task; 537 | } 538 | 539 | } 540 | } 541 | -------------------------------------------------------------------------------- /Shaman.SingleThreadSynchronizationContext/ResponsivenessWatchdog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Shaman.Runtime 10 | { 11 | public class ResponsivenessWatchdog 12 | { 13 | 14 | private static List> list = new List>(); 15 | private static bool started; 16 | 17 | 18 | 19 | [Configuration] 20 | private static volatile int Configuration_CheckerIntervalMs = 10; 21 | 22 | [Configuration(PerformanceValue = false)] 23 | private static volatile bool Configuration_Enable = true; 24 | 25 | [Configuration] 26 | private static int Configuration_CleanupPeriodWhenDisabledMs = 2000; 27 | 28 | [Configuration] 29 | private static long Configuration_MaxHangReportDelay = 5000; 30 | 31 | public static void InstallForCurrentThread(TimeSpan timeSpan) 32 | { 33 | 34 | 35 | var ctx = SingleThreadSynchronizationContext.Current; 36 | if (ctx == null) throw new InvalidOperationException(); 37 | 38 | 39 | lock (list) 40 | { 41 | if (ctx.hasInstalledResponsivenessWatchdog) throw new InvalidOperationException(); 42 | ctx.hasInstalledResponsivenessWatchdog = true; 43 | ctx.responsivenessWatchdogThresholdStopwatchTicks = (long)(timeSpan.TotalSeconds * Stopwatch.Frequency); 44 | list.Add(new WeakReference(ctx)); 45 | StartThreadInternal(); 46 | } 47 | 48 | } 49 | 50 | 51 | 52 | private static void StartThreadInternal() 53 | { 54 | 55 | if (!started) 56 | { 57 | started = true; 58 | var checker = new Thread(() => 59 | { 60 | while (true) 61 | { 62 | Thread.Sleep(Configuration_Enable ? Configuration_CheckerIntervalMs : Configuration_CleanupPeriodWhenDisabledMs); 63 | lock (list) 64 | { 65 | if (Configuration_Enable) 66 | { 67 | for (int i = 0; i < list.Count; i++) 68 | { 69 | SingleThreadSynchronizationContext ctx; 70 | list[i].TryGetTarget(out ctx); 71 | if (ctx?.HasCompleted == false) 72 | { 73 | var currentLoopId = Volatile.Read(ref ctx.loopId); 74 | if (ctx.LastHangException != null && currentLoopId != ctx.lastHangLoopId) 75 | { 76 | ReportHang(ctx); 77 | } 78 | var ticksEnd = Stopwatch.GetTimestamp(); 79 | var ticksStart = ctx.ticksStart; 80 | var elapsed = ticksEnd - ticksStart; 81 | if (ctx.IsBusy && elapsed >= ctx.responsivenessWatchdogThresholdStopwatchTicks && ctx.lastSubmittedHangLoopId != currentLoopId) 82 | { 83 | ApplicationHangException ex = ctx.LastHangException; 84 | if (ex == null) 85 | { 86 | ex = ApplicationHangException.CreateForThread(ctx.Thread); 87 | ex.LoopId = currentLoopId; 88 | ctx.LastHangException = ex; 89 | } 90 | ex.HangTime = TimeSpan.FromSeconds((double)elapsed / Stopwatch.Frequency); 91 | ctx.lastHangLoopId = currentLoopId; 92 | if (elapsed > Configuration_MaxHangReportDelay) 93 | { 94 | ReportHang(ctx); 95 | } 96 | } 97 | } 98 | else 99 | { 100 | list.RemoveAt(i); 101 | i--; 102 | } 103 | } 104 | } 105 | else 106 | { 107 | RemoveWhere(list, x => 108 | { 109 | SingleThreadSynchronizationContext target; 110 | x.TryGetTarget(out target); 111 | 112 | return target?.HasCompleted != false; 113 | }); 114 | } 115 | } 116 | 117 | } 118 | }); 119 | checker.IsBackground = true; 120 | #if !CORECLR 121 | checker.Priority = ThreadPriority.AboveNormal; 122 | #endif 123 | checker.Start(); 124 | } 125 | } 126 | 127 | 128 | private static int RemoveWhere(IList items, Func predicate) 129 | { 130 | List indexes = null; 131 | var index = 0; 132 | foreach (var item in items) 133 | { 134 | if (predicate(item)) 135 | { 136 | if (indexes == null) indexes = new List(); 137 | indexes.Add(index); 138 | } 139 | else index++; 140 | } 141 | if (indexes == null) return 0; 142 | foreach (var idx in indexes) 143 | { 144 | items.RemoveAt(idx); 145 | } 146 | return indexes.Count; 147 | } 148 | 149 | 150 | private static void ReportHang(SingleThreadSynchronizationContext ctx) 151 | { 152 | var handler = ReportHangHandler; 153 | handler?.Invoke(ctx.LastHangException); 154 | 155 | ctx.LastHangException = null; 156 | ctx.lastSubmittedHangLoopId = ctx.lastHangLoopId; 157 | } 158 | 159 | public static Action ReportHangHandler { get; set; } 160 | 161 | 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Shaman.SingleThreadSynchronizationContext/Shaman.SingleThreadSynchronizationContext.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Provides a single-threaded synchronization context, which makes it easy to reason about asynchronous code in environments that don't usually provide a synchronization context (for example, console applications). 4 | 1.0.0.9 5 | Andrea Martinelli 6 | net45;netstandard2.0 7 | $(DefineConstants);SHAMAN_CORE;DESKTOP 8 | Shaman.SingleThreadSynchronizationContext 9 | ShamanOpenSourceKey.snk 10 | true 11 | 1.0.0.0 12 | true 13 | Shaman.SingleThreadSynchronizationContext 14 | SynchronizationContext;async;asynchronous;continuations;coroutines;console 15 | http://shaman.io/images/shaman-nuget-icon.png 16 | https://github.com/antiufo/Shaman.SingleThreadSynchronizationContext 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | $(DefineConstants);CORECLR 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Shaman.SingleThreadSynchronizationContext/ShamanOpenSourceKey.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antiufo/Shaman.SingleThreadSynchronizationContext/b58772be39be98fafa97f4e12e19512daf7e2824/Shaman.SingleThreadSynchronizationContext/ShamanOpenSourceKey.snk -------------------------------------------------------------------------------- /Shaman.SingleThreadSynchronizationContext/SyncCtxExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Shaman.Runtime 6 | { 7 | 8 | public static class SynchronizationContextExtensionMethods 9 | { 10 | 11 | public static Task SendAsync(this SynchronizationContext syncCtx, Action action) 12 | { 13 | var tcs = new TaskCompletionSource(); 14 | syncCtx.Post(dummy => 15 | { 16 | try 17 | { 18 | action(); 19 | tcs.TrySetResult(true); 20 | } 21 | catch (Exception ex) 22 | { 23 | tcs.TrySetException(ex); 24 | } 25 | }, null); 26 | return tcs.Task; 27 | } 28 | 29 | public static Task SendAsync(this SynchronizationContext syncCtx, Func func) 30 | { 31 | var tcs = new TaskCompletionSource(); 32 | syncCtx.Post(dummy => 33 | { 34 | try 35 | { 36 | var result = func(); 37 | tcs.TrySetResult(result); 38 | } 39 | catch (Exception ex) 40 | { 41 | tcs.TrySetException(ex); 42 | } 43 | }, null); 44 | return tcs.Task; 45 | } 46 | 47 | public static void Post(this SynchronizationContext ctx, Action action) 48 | { 49 | if ((ctx as SingleThreadSynchronizationContext)?.Thread == Thread.CurrentThread) action(); 50 | else ctx.Post(dummy => action(), null); 51 | } 52 | public static void Send(this SynchronizationContext ctx, Action action) 53 | { 54 | if ((ctx as SingleThreadSynchronizationContext)?.Thread == Thread.CurrentThread) action(); 55 | else ctx.Send(dummy => action(), null); 56 | } 57 | 58 | public static void Post(this SynchronizationContext ctx, Func func) 59 | { 60 | if ((ctx as SingleThreadSynchronizationContext)?.Thread == Thread.CurrentThread) func(); 61 | else ctx.Post(dummy => func(), null); 62 | 63 | } 64 | 65 | public static Action SynchronousSendCallback { get; set; } 66 | 67 | public static void Send(this SynchronizationContext ctx, Func func) 68 | { 69 | if (ctx == SynchronizationContext.Current) 70 | throw new InvalidOperationException("Cannot call a synchronous Send() when the argument is an asynchronous function and the synchronization context is the currently active one."); 71 | SynchronousSendCallback?.Invoke(); 72 | var t = ctx.SendAsync(func); 73 | try 74 | { 75 | t.Wait(); 76 | } 77 | catch (AggregateException ex) when (ex.InnerException != null) 78 | { 79 | if (ex.InnerException != null) throw ex.InnerException; 80 | } 81 | 82 | } 83 | public static Task SendAsync(this SynchronizationContext ctx, Func func) 84 | { 85 | var tcs = new TaskCompletionSource(); 86 | ctx.Post(async dummy => 87 | { 88 | try 89 | { 90 | await func(); 91 | tcs.TrySetResult(true); 92 | } 93 | catch (Exception ex) 94 | { 95 | tcs.TrySetException(ex); 96 | } 97 | 98 | }, null); 99 | return tcs.Task; 100 | } 101 | 102 | 103 | } 104 | } --------------------------------------------------------------------------------