// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ // // ActionBlock.cs // // // A target block that executes an action for each message. // // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; using System.Threading.Tasks.Dataflow.Internal; namespace System.Threading.Tasks.Dataflow { /// Provides a dataflow block that invokes a provided delegate for every data element received. /// Specifies the type of data operated on by this . [DebuggerDisplay("{DebuggerDisplayContent,nq}")] [DebuggerTypeProxy(typeof(ActionBlock<>.DebugView))] public sealed class ActionBlock : ITargetBlock, IDebuggerDisplay { /// The core implementation of this message block when in default mode. private readonly TargetCore _defaultTarget; /// The core implementation of this message block when in SPSC mode. private readonly SpscTargetCore _spscTarget; /// Initializes the with the specified . /// The action to invoke with each data element received. /// The is null (Nothing in Visual Basic). public ActionBlock(Action action) : this((Delegate)action, ExecutionDataflowBlockOptions.Default) { } /// Initializes the with the specified and . /// The action to invoke with each data element received. /// The options with which to configure this . /// The is null (Nothing in Visual Basic). /// The is null (Nothing in Visual Basic). public ActionBlock(Action action, ExecutionDataflowBlockOptions dataflowBlockOptions) : this((Delegate)action, dataflowBlockOptions) { } /// Initializes the with the specified . /// The action to invoke with each data element received. /// The is null (Nothing in Visual Basic). public ActionBlock(Func action) : this((Delegate)action, ExecutionDataflowBlockOptions.Default) { } /// Initializes the with the specified and . /// The action to invoke with each data element received. /// The options with which to configure this . /// The is null (Nothing in Visual Basic). /// The is null (Nothing in Visual Basic). public ActionBlock(Func action, ExecutionDataflowBlockOptions dataflowBlockOptions) : this((Delegate)action, dataflowBlockOptions) { } /// Initializes the with the specified delegate and options. /// The action to invoke with each data element received. /// The options with which to configure this . /// The is null (Nothing in Visual Basic). /// The is null (Nothing in Visual Basic). private ActionBlock(Delegate action, ExecutionDataflowBlockOptions dataflowBlockOptions) { // Validate arguments if (action == null) throw new ArgumentNullException("action"); if (dataflowBlockOptions == null) throw new ArgumentNullException("dataflowBlockOptions"); Contract.Ensures((_spscTarget != null) ^ (_defaultTarget != null), "One and only one of the two targets must be non-null after construction"); Contract.EndContractBlock(); // Ensure we have options that can't be changed by the caller dataflowBlockOptions = dataflowBlockOptions.DefaultOrClone(); // Based on the mode, initialize the target. If the user specifies SingleProducerConstrained, // we'll try to employ an optimized mode under a limited set of circumstances. var syncAction = action as Action; if (syncAction != null && dataflowBlockOptions.SingleProducerConstrained && dataflowBlockOptions.MaxDegreeOfParallelism == 1 && !dataflowBlockOptions.CancellationToken.CanBeCanceled && dataflowBlockOptions.BoundedCapacity == DataflowBlockOptions.Unbounded) { // Initialize the SPSC fast target to handle the bulk of the processing. // The SpscTargetCore is only supported when BoundedCapacity, CancellationToken, // and MaxDOP are all their default values. It's also only supported for sync // delegates and not for async delegates. _spscTarget = new SpscTargetCore(this, syncAction, dataflowBlockOptions); } else { // Initialize the TargetCore which handles the bulk of the processing. // The default target core can handle all options and delegate flavors. if (syncAction != null) // sync { _defaultTarget = new TargetCore(this, messageWithId => ProcessMessage(syncAction, messageWithId), null, dataflowBlockOptions, TargetCoreOptions.RepresentsBlockCompletion); } else // async { var asyncAction = action as Func; Debug.Assert(asyncAction != null, "action is of incorrect delegate type"); _defaultTarget = new TargetCore(this, messageWithId => ProcessMessageWithTask(asyncAction, messageWithId), null, dataflowBlockOptions, TargetCoreOptions.RepresentsBlockCompletion | TargetCoreOptions.UsesAsyncCompletion); } // Handle async cancellation requests by declining on the target Common.WireCancellationToComplete( dataflowBlockOptions.CancellationToken, Completion, state => ((TargetCore)state).Complete(exception: null, dropPendingMessages: true), _defaultTarget); } #if FEATURE_TRACING DataflowEtwProvider etwLog = DataflowEtwProvider.Log; if (etwLog.IsEnabled()) { etwLog.DataflowBlockCreated(this, dataflowBlockOptions); } #endif } /// Processes the message with a user-provided action. /// The action to use to process the message. /// The message to be processed. private void ProcessMessage(Action action, KeyValuePair messageWithId) { try { action(messageWithId.Key); } catch (Exception exc) { // If this exception represents cancellation, swallow it rather than shutting down the block. if (!Common.IsCooperativeCancellation(exc)) throw; } finally { // We're done synchronously processing an element, so reduce the bounding count // that was incrementing when this element was enqueued. if (_defaultTarget.IsBounded) _defaultTarget.ChangeBoundingCount(-1); } } /// Processes the message with a user-provided action that returns a task. /// The action to use to process the message. /// The message to be processed. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] private void ProcessMessageWithTask(Func action, KeyValuePair messageWithId) { Contract.Requires(action != null, "action needed for processing"); // Run the action to get the task that represents the operation's completion Task task = null; Exception caughtException = null; try { task = action(messageWithId.Key); } catch (Exception exc) { caughtException = exc; } // If no task is available, we're done. if (task == null) { // If we didn't get a task because an exception occurred, // store it (if the exception was cancellation, just ignore it). if (caughtException != null && !Common.IsCooperativeCancellation(caughtException)) { Common.StoreDataflowMessageValueIntoExceptionData(caughtException, messageWithId.Key); _defaultTarget.Complete(caughtException, dropPendingMessages: true, storeExceptionEvenIfAlreadyCompleting: true, unwrapInnerExceptions: false); } // Signal that we're done this async operation. _defaultTarget.SignalOneAsyncMessageCompleted(boundingCountChange: -1); return; } else if (task.IsCompleted) { AsyncCompleteProcessMessageWithTask(task); } else { // Otherwise, join with the asynchronous operation when it completes. task.ContinueWith((completed, state) => { ((ActionBlock)state).AsyncCompleteProcessMessageWithTask(completed); }, this, CancellationToken.None, Common.GetContinuationOptions(TaskContinuationOptions.ExecuteSynchronously), TaskScheduler.Default); } } /// Completes the processing of an asynchronous message. /// The completed task. private void AsyncCompleteProcessMessageWithTask(Task completed) { Contract.Requires(completed != null, "Need completed task for processing"); Contract.Requires(completed.IsCompleted, "The task to be processed must be completed by now."); // If the task faulted, store its errors. We must add the exception before declining // and signaling completion, as the exception is part of the operation, and the completion conditions // depend on this. if (completed.IsFaulted) { _defaultTarget.Complete(completed.Exception, dropPendingMessages: true, storeExceptionEvenIfAlreadyCompleting: true, unwrapInnerExceptions: true); } // Regardless of faults, note that we're done processing. There are // no outputs to keep track of for action block, so we always decrement // the bounding count here (the callee will handle checking whether // we're actually in a bounded mode). _defaultTarget.SignalOneAsyncMessageCompleted(boundingCountChange: -1); } /// public void Complete() { if (_defaultTarget != null) { _defaultTarget.Complete(exception: null, dropPendingMessages: false); } else { _spscTarget.Complete(exception: null); } } /// void IDataflowBlock.Fault(Exception exception) { if (exception == null) throw new ArgumentNullException("exception"); Contract.EndContractBlock(); if (_defaultTarget != null) { _defaultTarget.Complete(exception, dropPendingMessages: true); } else { _spscTarget.Complete(exception); } } /// public Task Completion { get { return _defaultTarget != null ? _defaultTarget.Completion : _spscTarget.Completion; } } /// Posts an item to the . /// The item being offered to the target. /// true if the item was accepted by the target block; otherwise, false. /// /// This method will return once the target block has decided to accept or decline the item, /// but unless otherwise dictated by special semantics of the target block, it does not wait /// for the item to actually be processed (for example, /// will return from Post as soon as it has stored the posted item into its input queue). From the perspective /// of the block's processing, Post is asynchronous. For target blocks that support postponing offered messages, /// or for blocks that may do more processing in their Post implementation, consider using /// SendAsync, /// which will return immediately and will enable the target to postpone the posted message and later consume it /// after SendAsync returns. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Post(TInput item) { // Even though this method is available with the exact same functionality as an extension method // on ITargetBlock, using that extension method goes through an interface call on ITargetBlock, // which for very high-throughput scenarios shows up as noticeable overhead on certain architectures. // We can eliminate that call for direct ActionBlock usage by providing the same method as an instance method. return _defaultTarget != null ? _defaultTarget.OfferMessage(Common.SingleMessageHeader, item, null, false) == DataflowMessageStatus.Accepted : _spscTarget.Post(item); } /// DataflowMessageStatus ITargetBlock.OfferMessage(DataflowMessageHeader messageHeader, TInput messageValue, ISourceBlock source, Boolean consumeToAccept) { return _defaultTarget != null ? _defaultTarget.OfferMessage(messageHeader, messageValue, source, consumeToAccept) : _spscTarget.OfferMessage(messageHeader, messageValue, source, consumeToAccept); } /// public int InputCount { get { return _defaultTarget != null ? _defaultTarget.InputCount : _spscTarget.InputCount; } } /// Gets the number of messages waiting to be processed. This must only be used from the debugger. private int InputCountForDebugger { get { return _defaultTarget != null ? _defaultTarget.GetDebuggingInformation().InputCount : _spscTarget.InputCount; } } /// public override string ToString() { return Common.GetNameForDebugger(this, _defaultTarget != null ? _defaultTarget.DataflowBlockOptions : _spscTarget.DataflowBlockOptions); } /// The data to display in the debugger display attribute. [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider")] private object DebuggerDisplayContent { get { return string.Format("{0}, InputCount={1}", Common.GetNameForDebugger(this, _defaultTarget != null ? _defaultTarget.DataflowBlockOptions : _spscTarget.DataflowBlockOptions), InputCountForDebugger); } } /// Gets the data to display in the debugger display attribute for this instance. object IDebuggerDisplay.Content { get { return DebuggerDisplayContent; } } /// Provides a debugger type proxy for the Call. private sealed class DebugView { /// The action block being viewed. private readonly ActionBlock _actionBlock; /// The action block's default target being viewed. private readonly TargetCore.DebuggingInformation _defaultDebugInfo; /// The action block's SPSC target being viewed. private readonly SpscTargetCore.DebuggingInformation _spscDebugInfo; /// Initializes the debug view. /// The target being debugged. public DebugView(ActionBlock actionBlock) { Contract.Requires(actionBlock != null, "Need a block with which to construct the debug view."); _actionBlock = actionBlock; if (_actionBlock._defaultTarget != null) { _defaultDebugInfo = actionBlock._defaultTarget.GetDebuggingInformation(); } else { _spscDebugInfo = actionBlock._spscTarget.GetDebuggingInformation(); } } /// Gets the messages waiting to be processed. public IEnumerable InputQueue { get { return _defaultDebugInfo != null ? _defaultDebugInfo.InputQueue : _spscDebugInfo.InputQueue; } } /// Gets any postponed messages. public QueuedMap, DataflowMessageHeader> PostponedMessages { get { return _defaultDebugInfo != null ? _defaultDebugInfo.PostponedMessages : null; } } /// Gets the number of outstanding input operations. public Int32 CurrentDegreeOfParallelism { get { return _defaultDebugInfo != null ? _defaultDebugInfo.CurrentDegreeOfParallelism : _spscDebugInfo.CurrentDegreeOfParallelism; } } /// Gets the ExecutionDataflowBlockOptions used to configure this block. public ExecutionDataflowBlockOptions DataflowBlockOptions { get { return _defaultDebugInfo != null ? _defaultDebugInfo.DataflowBlockOptions : _spscDebugInfo.DataflowBlockOptions; } } /// Gets whether the block is declining further messages. public bool IsDecliningPermanently { get { return _defaultDebugInfo != null ? _defaultDebugInfo.IsDecliningPermanently : _spscDebugInfo.IsDecliningPermanently; } } /// Gets whether the block is completed. public bool IsCompleted { get { return _defaultDebugInfo != null ? _defaultDebugInfo.IsCompleted : _spscDebugInfo.IsCompleted; } } /// Gets the block's Id. public int Id { get { return Common.GetBlockId(_actionBlock); } } } } }