(The full set of ParallelExtensionsExtras Tour posts is available here.)  

As mentioned in the previous ParallelExtensionsExtras blog tour post, the Task Parallel Library (TPL) supports an extensible task scheduling mechanism, and we demonstrated how an StaTaskScheduler could be implemented that scheduled tasks onto custom STA threads.  Not all TaskScheduler implementations require dedicated threads, however: it’s actually common to want to layer additional functionality on top of an existing pool of threads, such as the ThreadPool, or any other TaskScheduler implementation.  There are a variety of scenarios that call for such an approach.  In this post, we’ll talk about one such scenario: concurrent/exclusive semantics.

Consider a reader/writer lock.  Multiple threads attempt to access resources protected by the lock, and they declare whether they’ll be reading or writing.  Since reading doesn’t cause any mutations, we can allow any number of readers into the critical region at the same time.  However, if any writers come along, only one writer and no readers may be allowed into the critical region concurrently.  We could handle such cases with Tasks simply by having the Tasks use a reader/writer lock. However, that will cause threads to block, which could significantly decrease the throughput of our system.  It’s also a rather imperative approach, and it’d be nice to be able to specify more declaratively which tasks are readers and which are writers.  We can accomplish that with the ConcurrentExclusiveInterleave type, found in the ConcurrentExclusiveInterleave.cs file in ParallelExtensionsExtras.

ConcurrentExclusiveInterleave is itself not a TaskScheduler: instead, it exposes TaskScheduler instances from two properties on the type:

public sealed class ConcurrentExclusiveInterleave

{

    public TaskScheduler ConcurrentTaskScheduler { get; }

    public TaskScheduler ExclusiveTaskScheduler { get; }

}

 

Any tasks scheduled to the ConcurrentTaskScheduler are treated like readers, with any number of them able to execute concurrently.  Any tasks scheduled to ExclusiveTaskScheduler are treated like writers: only one may be executing at a time, and no other tasks scheduled to this interleave’s ConcurrentTaskScheduler or ExclusiveTaskScheduler may be running concurrently.  Additionally, since in these cases writers are considered to be rare but important, we give preference to writers: as soon as one comes along, we don’t allow any more readers to be scheduled until all writers have completed.

For relatively complicated logic, the core of the ConcurrentExclusiveInterleave is actually straightforward:

private void ConcurrentExclusiveInterleaveProcessor()

{

    bool runTasks = true;

    while (runTasks)

    {

        try

        {

            foreach (var task in GetExclusiveTasks())

                _exclusiveTaskScheduler.ExecuteTask(task);

 

            Parallel.ForEach(

                GetConcurrentTasksUntilExclusiveExists(),

                _parallelOptions,

                ExecuteConcurrentTask);

        }

        finally

        {

            lock (_internalLock)

            {

                if (_concurrentTaskScheduler.Tasks.Count == 0 &&

                    _exclusiveTaskScheduler.Tasks.Count == 0)

                {

                    _taskExecuting = null;

                    runTasks = false;

                }

            }

        }

    }

}

 

This ConcurrentExclusiveInterleaveProcessor function is executed in a Task (e.g. Task.Factory.StartNew(ConcurrentExclusiveInterleaveProcessor)) when work arrives and a copy of the task isn’t already running.  The core of the function first iterates through any exclusive tasks that may exist, executing each.  When there aren’t any more, it iterates in parallel (using Parallel.ForEach) through any concurrent tasks that may have been scheduled.  However, it can’t just grab all of the concurrent tasks, as in the middle of processing them another exclusive task may arrive, in which case the interleave should stop processing concurrent tasks and loop around again to process exclusive tasks.  This is accomplished by the GetConcurrentTasksUntilExclusiveExists method, which is a C# iterator that takes and yields tasks from the ConcurrentTaskScheduler’s queue until it’s empty or until any Task arrives on the ExclusiveTaskScheduler.

With our ConcurrentExclusiveInterleave, we can now be more declarative about the concurrent/exclusive needs of our tasks, and we can take advantage of this functionality anywhere a TaskScheduler is supported.