/// <summary> /// Execute the (asynchronous) operation with the specified context. /// </summary> private TResult ExecuteOperation <TWork, TExecutor, TResult>(object state) { // Extract the expected operation context from the task state. var context = state as AsyncOperationContext <TWork, TExecutor, TResult> ?? state as OperationContext <TWork, TResult>; TaskOperation op = context.Operation; CancellationToken ct = context.CancellationToken; TResult result = default; Exception exception = null; // The operation execution logic uses two task completion sources: (1) an executor TCS and (2) a result TCS. // We do this to model the execution of tasks in the .NET runtime. For example, the `Task.Factory.StartNew` // method has different semantics from `Task.Run`, e.g. the returned task from `Task.Factory.StartNew(Func<T>)` // completes at the start of an asynchronous operation, so someone cannot await on it for the completion of // the operation. Instead, someone needs to first use `task.Unwrap()`, and then await on the unwrapped task. // To model this, the executor TCS completes at the start of the operation, and contains in its `task.AsyncState` // a reference to the result TCS. This approach allows us to implement `task.Unwrap` in a way that gives access // to the result TCS, which someone can then await for the asynchronous completion of the operation. try { // Update the current asynchronous control flow with the current runtime instance, // allowing future retrieval in the same asynchronous call stack. CoyoteRuntime.AssignAsyncControlFlowRuntime(this.Runtime); // Notify the scheduler that the operation started. This will yield execution until // the operation is ready to get scheduled. this.Scheduler.StartOperation(op); if (context is AsyncOperationContext <TWork, TExecutor, TResult> asyncContext) { // If the operation is asynchronous, then set the executor task completion source, which // can be used by `UnwrapTask` to unwrap and return the task executing this operation. this.SetTaskCompletionSource(asyncContext.ExecutorSource, asyncContext.Executor, null, ct); } if (context.Predecessor != null) { // If there is a predecessor task, then wait until the predecessor completes. ct.ThrowIfCancellationRequested(); op.OnWaitTask(context.Predecessor); } // Check if the operation must be canceled before starting the work. ct.ThrowIfCancellationRequested(); // Start executing the (asynchronous) work. Task executor = null; if (context.Work is Func <Task <TResult> > funcWithTaskResult) { executor = funcWithTaskResult(); } else if (context.Work is Func <Task> funcWithTask) { executor = funcWithTask(); } else if (context.Work is Func <CoyoteTasks.Task <TResult> > funcWithCoyoteTaskResult) { // TODO: temporary until we remove the custom task type. executor = funcWithCoyoteTaskResult().UncontrolledTask; } else if (context.Work is Func <CoyoteTasks.Task> funcWithCoyoteTask) { // TODO: temporary until we remove the custom task type. executor = funcWithCoyoteTask().UncontrolledTask; } else if (context.Work is Func <TResult> func) { result = func(); } else { throw new NotSupportedException($"Unable to execute work with unsupported type {context.Work.GetType()}."); } if (executor != null) { // If the work is asynchronous, then wait until it completes. this.OnWaitTask(op.Id, executor); if (executor.IsFaulted) { // Propagate the failing exception by rethrowing it. ExceptionDispatchInfo.Capture(executor.Exception).Throw(); } else if (executor.IsCanceled) { if (op.Exception != null) { // An exception has been already captured, so propagate it. ExceptionDispatchInfo.Capture(op.Exception).Throw(); } else { // Wait the canceled executor (which is non-blocking as it has already completed) // to throw the generated `OperationCanceledException`. executor.Wait(); } } // Safely get the result without blocking as the work has completed. result = executor is Task <TResult> resultTask ? resultTask.Result : executor is TResult r ? r : default; } } catch (Exception ex) { // Unwrap and cache the exception to propagate it. exception = ControlledRuntime.UnwrapException(ex); this.ReportThrownException(exception); } finally { IO.Debug.WriteLine("<ScheduleDebug> Completed operation '{0}' on task '{1}'.", op.Name, Task.CurrentId); op.OnCompleted(); // Set the result task completion source to notify to the awaiters that the operation // has been completed, and schedule the next enabled operation. this.SetTaskCompletionSource(context.ResultSource, result, exception, default); this.Scheduler.ScheduleNextOperation(); } return(result); }
/// <summary> /// Execute the operation with the specified context. /// </summary> private void ExecuteOperation(object state) { // Extract the expected operation context from the task state. var context = state as OperationContext <Action, object>; TaskOperation op = context.Operation; CancellationToken ct = context.CancellationToken; Exception exception = null; try { // Update the current asynchronous control flow with the current runtime instance, // allowing future retrieval in the same asynchronous call stack. CoyoteRuntime.AssignAsyncControlFlowRuntime(this.Runtime); // Notify the scheduler that the operation started. This will yield execution until // the operation is ready to get scheduled. this.Scheduler.StartOperation(op); if (context.Predecessor != null) { // If there is a predecessor task, then wait until the predecessor completes. ct.ThrowIfCancellationRequested(); op.OnWaitTask(context.Predecessor); } if (context.Options.HasFlag(OperationExecutionOptions.YieldAtStart)) { // Try yield execution to the next operation. this.Scheduler.ScheduleNextOperation(true); } // Check if the operation must be canceled before starting the work. ct.ThrowIfCancellationRequested(); // Start executing the work. context.Work(); } catch (Exception ex) { if (context.Options.HasFlag(OperationExecutionOptions.FailOnException)) { this.Assert(false, "Unhandled exception. {0}", ex); } else { // Unwrap and cache the exception to propagate it. exception = ControlledRuntime.UnwrapException(ex); this.ReportThrownException(exception); } } finally { IO.Debug.WriteLine("<ScheduleDebug> Completed operation '{0}' on task '{1}'.", op.Name, Task.CurrentId); op.OnCompleted(); // Set the result task completion source to notify to the awaiters that the operation // has been completed, and schedule the next enabled operation. this.SetTaskCompletionSource(context.ResultSource, null, exception, default); this.Scheduler.ScheduleNextOperation(); } }