The PLINQ library does not allow configuring the TaskScheduler
, for good reasons:
The reason we didn't make this public initially and then haven't each time the question has come up is due to fear of deadlocks. Some PLINQ operators use barriers, such that all tasks/partitions involved in processing the query need to join before any of them can make further progress. If the underlying scheduler can't guarantee that all of the tasks queued as part of PLINQ's processing will be able to run at the same time, e.g. if it's a scheduler created with a fixed number of threads, then we risk deadlock.
If you feel adventurous you could consider doing this configuration with reflection. Here is a PLINQ operator that does exactly this:
/// <summary>
/// Sets the scheduler that should execute the query.
/// </summary>
/// <remarks>Has been tested on .NET 6 only.</remarks>
public static ParallelQuery<T> WithScheduler<T>(this ParallelQuery<T> source,
TaskScheduler scheduler)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (scheduler == null) throw new ArgumentNullException(nameof(scheduler));
const string name1 = "_specifiedSettings";
FieldInfo fi1 = typeof(ParallelQuery).GetField(name1,
BindingFlags.NonPublic | BindingFlags.Instance);
if (fi1 == null) throw new InvalidOperationException($"Field {name1} not found.");
object querySettings = fi1.GetValue(source);
if (querySettings == null) throw new InvalidOperationException($"{name1} is null.");
const string name2 = "TaskScheduler";
PropertyInfo pi2 = querySettings.GetType().GetProperty(name2,
BindingFlags.NonPublic | BindingFlags.Instance);
if (pi2 == null) throw new InvalidOperationException($"Property {name2} not found.");
pi2.SetValue(querySettings, scheduler);
fi1.SetValue(source, querySettings); // The QuerySettings is a struct
return source;
}
Usage example:
TaskScheduler lowPriorityScheduler = new ConcurrentExclusiveSchedulerPair(
TaskScheduler.Default, Environment.ProcessorCount).ConcurrentScheduler;
var query = source
.AsParallel()
.WithScheduler(lowPriorityScheduler)
//...
You could use the same lowPriorityScheduler
to schedule other operations that should run with low priority, in parallel with this and other queries. All these operations combined will use at most Environment.ProcessorCount
threads from the ThreadPool
. All other threads that are available beyond this limit, will be used exclusively by non-low-priority operations.
You could also take a look at this somewhat related question.