Why doesn't WithMergeOptions(ParallelMergeOptions.NotBuffered) make results available immediately? - c#

(I am currently restricted to .NET 4.0)
I have a situation where I want to process items in parallel as much as possible, order must be maintained, and items can be added at any time until "stop" is pressed.
Items can come in "bursts", so it is possible that the queue will completely drain, there will be a pause, and then a large number of items will come in at once again.
I want the results to become available as soon as they are done.
Here is a simplified example:
class Program
{
static void Main(string[] args)
{
BlockingCollection<int> itemsQueue = new BlockingCollection<int>();
Random random = new Random();
var results = itemsQueue
.GetConsumingEnumerable()
.AsParallel()
.AsOrdered()
.WithMergeOptions(ParallelMergeOptions.NotBuffered)
.Select(i =>
{
int work = 0;
Console.WriteLine("Working on " + i);
//simulate work
for (int busy = 0; busy <= 90000000; ++busy) { ++work; };
Console.WriteLine("Finished " + i);
return i;
});
TaskCompletionSource<bool> completion = new TaskCompletionSource<bool>();
Task.Factory.StartNew(() =>
{
foreach (int i in results)
{
Console.WriteLine("Result Available: " + i);
}
completion.SetResult(true);
});
int iterations;
iterations = random.Next(5, 50);
Console.WriteLine("------- iterations: " + iterations + "-------");
for (int i = 1; i <= iterations; ++i)
{
itemsQueue.Add(i);
}
while (true)
{
char c = Console.ReadKey().KeyChar;
if (c == 's')
{
break;
}
else
{
++iterations;
Console.WriteLine("adding: " + iterations);
itemsQueue.Add(iterations);
}
}
itemsQueue.CompleteAdding();
completion.Task.Wait();
Console.WriteLine("Done!");
Console.ReadKey();
itemsQueue.Dispose();
}
}
As the above example shows, what will typically happen, is that results will become available up until the last few results (I'm not 100% sure of this, but the number of results that it stops short may be roughly correlated with the number of cores on the box), until itemsQueue.CompleteAdding(); is called (in the example, the "s" key is pressed), at which point the rest of the results will finally become available.
Why do the results not become available immediately despite the fact that I specify .WithMergeOptions(ParallelMergeOptions.NotBuffered), and how can I make them become available immediately?

Note that the problem is not an issue if you can call BlockingQueue.CompleteAdding() instance method - that will cause all results to finish.
Short Answer
If on the other hand, you need to maintain order, and need to have the results available as soon as they can be, and you don't have an opportunity to call BlockingQueue.CompleteAdding(), then if at all possible, you are much better off having the consumption of items in the queue be non-parallel, but parallelize the processing of each individual task.
E.g.
class Program
{
//Not parallel, but suitable for monitoring queue purposes,
//can then focus on parallelizing each individual task
static void Main(string[] args)
{
BlockingCollection<int> itemsQueue = new BlockingCollection<int>();
Random random = new Random();
var results = itemsQueue.GetConsumingEnumerable()
.Select(i =>
{
Console.WriteLine("Working on " + i);
//Focus your parallelization efforts on the work of
//the individual task
//E.g, simulated:
double work = Enumerable.Range(0, 90000000 - (10 * (i % 3)))
.AsParallel()
.Select(w => w + 1)
.Average();
Console.WriteLine("Finished " + i);
return i;
});
TaskCompletionSource<bool> completion = new TaskCompletionSource<bool>();
Task.Factory.StartNew(() =>
{
foreach (int i in results)
{
Console.WriteLine("Result Available: " + i);
}
completion.SetResult(true);
});
int iterations;
iterations = random.Next(5, 50);
Console.WriteLine("------- iterations: " + iterations + "-------");
for (int i = 1; i <= iterations; ++i)
{
itemsQueue.Add(i);
}
while (true)
{
char c = Console.ReadKey().KeyChar;
if (c == 's')
{
break;
}
else
{
++iterations;
Console.WriteLine("adding: " + iterations);
itemsQueue.Add(iterations);
}
}
itemsQueue.CompleteAdding();
completion.Task.Wait();
Console.WriteLine("Done!");
Console.ReadKey();
itemsQueue.Dispose();
}
}
Longer Answer
It appears that there is an interaction between the BlockingQueue in particular and AsOrderable()
It seems that AsOrderable will stop the processing of Tasks whenever one of the enumerators in the partition blocks.
The default partitioner will deal with chunks typically greater than one - and the blocking queue will block until the chunk can be filled (or CompleteAdding is filled).
However, even with a chunk size of 1, the problem does not completely go away.
To play around with this, you can sometimes see the behavior when implementing your own partitioner. (Note, that if you specify .WithDegreeOfParallelism(1) the problem with results waiting to appear goes away - but of course, having a degree of parallelism = 1 kind of defeats the purpose!)
e.g.
public class ImmediateOrderedPartitioner<T> : OrderablePartitioner<T>
{
private readonly IEnumerable<T> _consumingEnumerable;
private readonly Ordering _ordering = new Ordering();
public ImmediateOrderedPartitioner(BlockingCollection<T> collection) : base(true, true, true)
{
_consumingEnumerable = collection.GetConsumingEnumerable();
}
private class Ordering
{
public int Order = -1;
}
private class MyEnumerator<S> : IEnumerator<KeyValuePair<long, S>>
{
private readonly object _orderLock = new object();
private readonly IEnumerable<S> _enumerable;
private KeyValuePair<long, S> _current;
private bool _hasItem;
private Ordering _ordering;
public MyEnumerator(IEnumerable<S> consumingEnumerable, Ordering ordering)
{
_enumerable = consumingEnumerable;
_ordering = ordering;
}
public KeyValuePair<long, S> Current
{
get
{
if (_hasItem)
{
return _current;
}
else
throw new InvalidOperationException();
}
}
public void Dispose()
{
}
object System.Collections.IEnumerator.Current
{
get
{
return Current;
}
}
public bool MoveNext()
{
lock (_orderLock)
{
bool canMoveNext = false;
var next = _enumerable.Take(1).FirstOrDefault(s => { canMoveNext = true; return true; });
if (canMoveNext)
{
_current = new KeyValuePair<long, S>(++_ordering.Order, next);
_hasItem = true;
++_ordering.Order;
}
else
{
_hasItem = false;
}
return canMoveNext;
}
}
public void Reset()
{
throw new NotSupportedException();
}
}
public override IList<IEnumerator<KeyValuePair<long, T>>> GetOrderablePartitions(int partitionCount)
{
var result = new List<IEnumerator<KeyValuePair<long,T>>>();
//for (int i = 0; i < partitionCount; ++i)
//{
// result.Add(new MyEnumerator<T>(_consumingEnumerable, _ordering));
//}
//share the enumerator between partitions in this case to maintain
//the proper locking on ordering.
var enumerator = new MyEnumerator<T>(_consumingEnumerable, _ordering);
for (int i = 0; i < partitionCount; ++i)
{
result.Add(enumerator);
}
return result;
}
public override bool SupportsDynamicPartitions
{
get
{
return false;
}
}
public override IEnumerable<T> GetDynamicPartitions()
{
throw new NotImplementedException();
return base.GetDynamicPartitions();
}
public override IEnumerable<KeyValuePair<long, T>> GetOrderableDynamicPartitions()
{
throw new NotImplementedException();
return base.GetOrderableDynamicPartitions();
}
public override IList<IEnumerator<T>> GetPartitions(int partitionCount)
{
throw new NotImplementedException();
return base.GetPartitions(partitionCount);
}
}
class Program
{
static void Main(string[] args)
{
BlockingCollection<int> itemsQueue = new BlockingCollection<int>();
var partitioner = new ImmediateOrderedPartitioner<int>(itemsQueue);
Random random = new Random();
var results = partitioner
.AsParallel()
.AsOrdered()
.WithMergeOptions(ParallelMergeOptions.NotBuffered)
//.WithDegreeOfParallelism(1)
.Select(i =>
{
int work = 0;
Console.WriteLine("Working on " + i);
for (int busy = 0; busy <= 90000000; ++busy) { ++work; };
Console.WriteLine("Finished " + i);
return i;
});
TaskCompletionSource<bool> completion = new TaskCompletionSource<bool>();
Task.Factory.StartNew(() =>
{
foreach (int i in results)
{
Console.WriteLine("Result Available: " + i);
}
completion.SetResult(true);
});
int iterations;
iterations = 1; // random.Next(5, 50);
Console.WriteLine("------- iterations: " + iterations + "-------");
for (int i = 1; i <= iterations; ++i)
{
itemsQueue.Add(i);
}
while (true)
{
char c = Console.ReadKey().KeyChar;
if (c == 's')
{
break;
}
else
{
++iterations;
Console.WriteLine("adding: " + iterations);
itemsQueue.Add(iterations);
}
}
itemsQueue.CompleteAdding();
completion.Task.Wait();
Console.WriteLine("Done!");
Console.ReadKey();
itemsQueue.Dispose();
}
}
Alternate Approach
If parallelizing the individual task (as recommended in the "short answer") is not a possibility, and all the other problem constraints apply, then you can implement your own type of queue that spins up tasks for each item - thus letting the Task Parallel Library handle the scheduling of work, but synchronize the consumption of results on your own.
For example, something like the below (with the standard "no warranties" disclaimer!)
public class QueuedItem<TInput, TResult>
{
private readonly object _lockObject = new object();
private TResult _result;
private readonly TInput _input;
private readonly TResult _notfinished;
internal readonly bool IsEndQueue = false;
internal QueuedItem()
{
IsEndQueue = true;
}
public QueuedItem(TInput input, TResult notfinished)
{
_input = input;
_notfinished = notfinished;
_result = _notfinished;
}
public TResult ReadResult()
{
lock (_lockObject)
{
if (!IsResultReady)
throw new InvalidOperationException("Check IsResultReady before calling ReadResult()");
return _result;
}
}
public void WriteResult(TResult value)
{
lock (_lockObject)
{
if (IsResultReady)
throw new InvalidOperationException("Result has already been written");
_result = value;
}
}
public TInput Input { get { return _input; } }
public bool IsResultReady
{
get
{
lock (_lockObject)
{
return !object.Equals(_result, _notfinished) || IsEndQueue;
}
}
}
}
public class ParallelImmediateOrderedProcessingQueue<TInput, TResult>
{
private readonly ReaderWriterLockSlim _addLock = new ReaderWriterLockSlim();
private readonly object _readingResultsLock = new object();
private readonly ConcurrentQueue<QueuedItem<TInput, TResult>> _concurrentQueue = new ConcurrentQueue<QueuedItem<TInput, TResult>>();
bool _isFinishedAdding = false;
private readonly TResult _notFinished;
private readonly Action<QueuedItem<TInput, TResult>> _processor;
/// <param name="notFinished">A value that indicates the result is not yet finished</param>
/// <param name="processor">Must call SetResult() on argument when finished.</param>
public ParallelImmediateOrderedProcessingQueue(TResult notFinished, Action<QueuedItem<TInput, TResult>> processor)
{
_notFinished = notFinished;
_processor = processor;
}
public event Action ResultsReady = delegate { };
private void SignalResult()
{
QueuedItem<TInput, TResult> item;
if (_concurrentQueue.TryPeek(out item) && item.IsResultReady)
{
ResultsReady();
}
}
public void Add(TInput input)
{
bool shouldThrow = false;
_addLock.EnterReadLock();
{
shouldThrow = _isFinishedAdding;
if (!shouldThrow)
{
var queuedItem = new QueuedItem<TInput, TResult>(input, _notFinished);
_concurrentQueue.Enqueue(queuedItem);
Task.Factory.StartNew(() => { _processor(queuedItem); SignalResult(); });
}
}
_addLock.ExitReadLock();
if (shouldThrow)
throw new InvalidOperationException("An attempt was made to add an item, but adding items was marked as completed");
}
public IEnumerable<TResult> ConsumeReadyResults()
{
//lock necessary to preserve ordering
lock (_readingResultsLock)
{
QueuedItem<TInput, TResult> queuedItem;
while (_concurrentQueue.TryPeek(out queuedItem) && queuedItem.IsResultReady)
{
if (!_concurrentQueue.TryDequeue(out queuedItem))
throw new ApplicationException("this shouldn't happen");
if (queuedItem.IsEndQueue)
{
_completion.SetResult(true);
}
else
{
yield return queuedItem.ReadResult();
}
}
}
}
public void CompleteAddingItems()
{
_addLock.EnterWriteLock();
{
_isFinishedAdding = true;
var queueCompletion = new QueuedItem<TInput, TResult>();
_concurrentQueue.Enqueue(queueCompletion);
Task.Factory.StartNew(() => { SignalResult(); });
}
_addLock.ExitWriteLock();
}
TaskCompletionSource<bool> _completion = new TaskCompletionSource<bool>();
public void WaitForCompletion()
{
_completion.Task.Wait();
}
}
class Program
{
static void Main(string[] args)
{
const int notFinished = int.MinValue;
var processingQueue = new ParallelImmediateOrderedProcessingQueue<int, int>(notFinished, qi =>
{
int work = 0;
Console.WriteLine("Working on " + qi.Input);
//simulate work
int maxBusy = 90000000 - (10 * (qi.Input % 3));
for (int busy = 0; busy <= maxBusy; ++busy) { ++work; };
Console.WriteLine("Finished " + qi.Input);
qi.WriteResult(qi.Input);
});
processingQueue.ResultsReady += new Action(() =>
{
Task.Factory.StartNew(() =>
{
foreach (int result in processingQueue.ConsumeReadyResults())
{
Console.WriteLine("Results Available: " + result);
}
});
});
int iterations = new Random().Next(5, 50);
Console.WriteLine("------- iterations: " + iterations + "-------");
for (int i = 1; i <= iterations; ++i)
{
processingQueue.Add(i);
}
while (true)
{
char c = Console.ReadKey().KeyChar;
if (c == 's')
{
break;
}
else
{
++iterations;
Console.WriteLine("adding: " + iterations);
processingQueue.Add(iterations);
}
}
processingQueue.CompleteAddingItems();
processingQueue.WaitForCompletion();
Console.WriteLine("Done!");
Console.ReadKey();
}
}

Related

my code seems to ignore a loop for some reason

i'm trying to solve project euler's third problem but it seems that the compiler skips a for loop so it makes my code totally useless
note : the idea didn't show any syntax error
here's the code :
class Program
{
static void Main(string[] args)
{
const long n = 600851475143;
List<long> factors = new List<long>();
factors = getFactors(Math.Sqrt(n));
long max = 0;
for (int i = 0; i<factors.Count ;i++)//this loop in particular , it doesn't print the "testing.."
{
Console.WriteLine("test....");
if(isPrime(getFactors(factors[i])))
{
max = factors[i];
}
}
Console.ReadKey();
}
static List<long> getFactors(double number)
{
List<long> list = new List<long>();
for(int i = 2;i<=number;i++)
{
if(number%i ==0)
{
list.Add(i);
}
}
return list;
}
static bool isPrime(List<long> list)
{
if(list.Count == 2)
{
return true;
}
else
{
return false;
}
}
}
static List<long> getFactors(double number)
{
List<long> list = new List<long>();
for (int i = 2; i <= number; i++)
{
if (Math.Floor(number % i) == 0)
{
list.Add(i);
}
}
return list;
}
number is a fraction, it will never == 0 unless its cast to an int

An object created separately by different threads is still shared

I have a program in which I want to Simulate a queue. To speed things up (lots of different parameters) I thought that I could use a parallel loop, however the queue object (or at least the objects within that object) are still shared while they are all created within either the MGcC function or the queue object. Is there something I forgot about parallel functions?
The object which gives the trouble is the queue.MyHeap.
(Also if more information is needed please ask as I have left out a lot to make it more readably as you might see in the queue object).
Parallel.ForEach(a, (numbers) =>
{
MGcC(a);
});
static public Tuple<Customer[,], List<Interval>[]> MGcC(int a)
{
Queue queue = new Queue(a);
return queue.Simulate(writeFile);
}
public class Queue
{
Func<object, double> arrivalFunction;
Func<object, double> servingFunction;
double lambda;
double v;
object serviceObject;
int minServers;
bool decision;
int idleServers;
int activeServers;
int amountInOrbit;
protected minHeap myHeap;
public Queue(double lambda, double v, object serviceObject, int servers, Func<object, double> arrivalFunction, Func<object, double> servingFunction, bool decision = false)
{
this.arrivalFunction = arrivalFunction;
this.servingFunction = servingFunction;
this.lambda = lambda;
this.v = v;
this.serviceObject = serviceObject;
this.minServers = servers;
this.decision = decision;
idleServers = servers;
activeServers = 0;
amountInOrbit = 0;
myHeap = new minHeap();
}
public class minHeap
{
static protected Action[] heap;
static public int counter;
public minHeap()
{
counter = -1;
heap = new Action[1000000];
}
public Action Pop()
{
if (counter < 0)
{
Console.WriteLine("empty");
return new Action(0, 0, new Customer());
}
Action returnValue = heap[0];
heap[0] = heap[counter];
counter--;
heapify(0);
return (returnValue);
}
public void Push(Action a)
{
counter++;
heap[counter] = new Action(double.PositiveInfinity, 0, new Customer());
InsertKey(counter, a);
}
static void InsertKey(int i, Action a)
{
if (heap[i].TimeOfExecution < a.TimeOfExecution)
Console.WriteLine("should not have happened");
heap[i] = a;
while (i > 0 && heap[Parent(i)].TimeOfExecution > heap[i].TimeOfExecution)
{
Action temp = heap[i];
heap[i] = heap[Parent(i)];
heap[Parent(i)] = temp;
i = Parent(i);
}
}
All the fields on your minHeap type are static. So yes: they're shared - that's what static means. You probably want to make them non-static.
Possibly you used static when you meant readonly?

Task work item priorities

I have a multi-threaded application that has to perform 3 different categories of work items. Category A is the highest priority items, Category B items comes after A and Category C items comes after B. These work items are queued to thread pool using Tasks. Let's say, there are 10 category C items already in queue and then a category B item is added. In this case, I would like category B item to be processed before any of the category C item. Is there anyway of accomplishing this?
You can implement it by creating your own queue process. This is just a code mockup.
Create an object like this
public class PrioritizableTask
{
public PrioritizableTask(Task task, int taskPriority)
{
Task = task;
Priority = taskPriority;
}
public int Priority { get; private set; }
public Task Task { get; private set; }
}
And then create another collection class and implement a new method on it, something like this.
public class PrioritizableTasksCollection : IList<PrioritizableTask>
{
private static readonly List<PrioritizableTask> runners = new List<PrioritizableTask>();
public void Add(PrioritizableTask item)
{
runners.Add(item);
}
public Task GetNextPriority()
{
var priorityTask = runners.OrderBy(x => x.Priority).FirstOrDefault();
return priorityTask != null ? priorityTask.Task : null;
}
}
Consume like
PrioritizableTasksCollection executors = new PrioritizableTasksCollection();
executors.Add(new PrioritizableTask(new Task(() => { }), 4));
executors.Add(new PrioritizableTask(new Task(() => { }), 3));
executors.Add(new PrioritizableTask(new Task(() => { }), 7));
executors.Add(new PrioritizableTask(new Task(() => { }), 5));
executors.Add(new PrioritizableTask(new Task(() => { }), 1));
executors.Add(new PrioritizableTask(new Task(() => { }), 2));
Task executeNext = executors.GetNextPriority();
Implement your own deleting on the collection.
I've been looking at your problem and i did not find a built-in thread-safe sorted collection.
So i built a basic thread-safe SortedSet<int> wrapper class.
Sorted Set
public class MyThreadSafeSortedSet
{
private SortedSet<int> _set = new SortedSet<int>(new MyComparer());
private readonly object _locker = new object();
public void Add(int value)
{
lock (_locker)
{
_set.Add(value);
}
}
public int? Take()
{
lock (_locker)
{
if (_set.Count == 0)
return null;
var item = _set.First();
_set.Remove(item);
return item;
}
}
}
I built a custom comparer which prefers even numbers
public class MyComparer : Comparer<int>
{
public override int Compare(int x, int y)
{
if (x % 2 == 0)
{
if (y % 2 == 0)
return x - y;
else
return -1;
}
else
{
if (y % 2 == 0)
return 1;
else
return x - y;
}
}
}
And finally two threads. One to produce items; the other one to take them
static void Main(string[] args)
{
MyThreadSafeSortedSet queue = new MyThreadSafeSortedSet();
var task1 = Task.Run(() =>
{
Random r = new Random();
for (int i = 0; i < 15; i++)
{
Task.Delay(100).Wait();
var randomNumber = r.Next();
queue.Add(randomNumber);
}
Console.WriteLine("I'm done adding");
});
var task2 = Task.Run(() =>
{
Random r = new Random();
while (true)
{
var delay = r.Next(500);
Task.Delay(delay).Wait();
var item = queue.Take();
Console.WriteLine("Took: {0}", item);
if (item == null)
break;
}
});
Task.WaitAll(task2);
}
You can change the specialized SortedSet and custom comparer for your own classes.
Hope it helped
Please look my version of solution based on BinarySearch method of List class.
enum CategoryOfWorkItem: int { C = 0, B, A };
struct WorkItem : IComparer<WorkItem>
{
public CategoryOfWorkItem Category;
public int Compare(WorkItem x, WorkItem y)
{
return x.Category - y.Category;
}
public void AddTo(List<WorkItem> list)
{
int i = list.BinarySearch(this, this);
if (i < 0) i = ~i;
list.Insert(i, this);
}
}
Example of usage
List<WorkItem> list = new List<WorkItem>();
Task.Run(() =>
{
Random rand = new Random();
for (int i = 0; i < 20; i++)
{
WorkItem item = new WorkItem();
switch (rand.Next(0, 3))
{
case 0: item.Category = CategoryOfWorkItem.A; break;
case 1: item.Category = CategoryOfWorkItem.B; break;
case 2: item.Category = CategoryOfWorkItem.C; break;
}
lock (list)
{
item.AddTo(list);
}
Task.Delay(rand.Next(100, 1000)).Wait();
Console.WriteLine("Put {0}", item.Category);
}
Console.WriteLine("Putting finished.");
});
Task.WaitAll(Task.Run(() =>
{
Random rand = new Random();
while (true)
{
WorkItem item;
Task.Delay(rand.Next(500, 1000)).Wait();
lock (list)
{
if (list.Count < 1) break;
item = list[list.Count - 1];
list.RemoveAt(list.Count - 1);
}
Console.WriteLine("Get {0}", item.Category);
}
Console.WriteLine("Getting finished.");
}));

Enum and IEnumerable in C#

my TIME Enum contains Annual, Monthly, weekly, daily and Hourly.
Here I want to decide which is the minimum and want to return that.
How can I do this ? Here is the code I tried.
private Time DecideMinTime(IEnumerable<Time> g)
{
var minTime = Time.Hourly;
foreach (var element in g)
{
minTime = element;
}
return minTime;
}
Assuming that the numeric value of the enum elements decides what the minimum is:
private Time DecideMinTime(IEnumerable<Time> g)
{
if (g == null) { throw new ArgumentNullException("g"); }
return (Time)g.Cast<int>().Min();
}
If the numeric values indicate the opposite order then you would use .Max() instead of .Min().
As indicated, the numeric order is not consistent. This can be worked around simply by using a mapping indicating the correct order:
static class TimeOrdering
{
private static readonly Dictionary<Time, int> timeOrderingMap;
static TimeOrdering()
{
timeOrderingMap = new Dictionary<Time, int>();
timeOrderingMap[Time.Hourly] = 1;
timeOrderingMap[Time.Daily] = 2;
timeOrderingMap[Time.Weekly] = 3;
timeOrderingMap[Time.Monthly] = 4;
timeOrderingMap[Time.Annual] = 5;
}
public Time DecideMinTime(IEnumerable<Time> g)
{
if (g == null) { throw new ArgumentNullException("g"); }
return g.MinBy(i => timeOrderingMap[i]);
}
public TSource MinBy<TSource, int>(
this IEnumerable<TSource> self,
Func<TSource, int> ordering)
{
if (self == null) { throw new ArgumentNullException("self"); }
if (ordering == null) { throw new ArgumentNullException("ordering"); }
using (var e = self.GetEnumerator()) {
if (!e.MoveNext()) {
throw new ArgumentException("Sequence is empty.", "self");
}
var minElement = e.Current;
var minOrder = ordering(minElement);
while (e.MoveNext()) {
var curOrder = ordering(e.Current);
if (curOrder < minOrder) {
minOrder = curOrder;
minElement = e.Current;
}
}
return minElement;
}
}
}
To make it easier you can assign int values to your enum:
enum Time : byte {Hourly=1, Daily=2, Weekly=3, Monthly=4, Annual=5};
and then
private static Time DecideMinTime(IEnumerable<Time> g)
{
return g.Min();
}
That way you avoid casting back and forth.

TPL DataFlow, link blocks with priority?

Using TPL.DataFlow blocks, is it possible to link two or more sources to a single ITargetBlock(e.g. ActionBlock) and prioritize the sources?
e.g.
BufferBlock<string> b1 = new ...
BufferBlock<string> b2 = new ...
ActionBlock<string> a = new ...
//somehow force messages in b1 to be processed before any message of b2, always
b1.LinkTo (a);
b2.LinkTo (a);
As long as there are messages in b1, I want those to be fed to "a" and once b1 is empty, b2 messages are beeing pushed into "a"
Ideas?
There is nothing like that in TPL Dataflow itself.
The simplest way I can imagine doing this by yourself would be to create a structure that encapsulates three blocks: high priority input, low priority input and output. Those blocks would be simple BufferBlocks, along with a method forwarding messages from the two inputs to the output based on priority, running in background.
The code could look like this:
public class PriorityBlock<T>
{
private readonly BufferBlock<T> highPriorityTarget;
public ITargetBlock<T> HighPriorityTarget
{
get { return highPriorityTarget; }
}
private readonly BufferBlock<T> lowPriorityTarget;
public ITargetBlock<T> LowPriorityTarget
{
get { return lowPriorityTarget; }
}
private readonly BufferBlock<T> source;
public ISourceBlock<T> Source
{
get { return source; }
}
public PriorityBlock()
{
var options = new DataflowBlockOptions { BoundedCapacity = 1 };
highPriorityTarget = new BufferBlock<T>(options);
lowPriorityTarget = new BufferBlock<T>(options);
source = new BufferBlock<T>(options);
Task.Run(() => ForwardMessages());
}
private async Task ForwardMessages()
{
while (true)
{
await Task.WhenAny(
highPriorityTarget.OutputAvailableAsync(),
lowPriorityTarget.OutputAvailableAsync());
T item;
if (highPriorityTarget.TryReceive(out item))
{
await source.SendAsync(item);
}
else if (lowPriorityTarget.TryReceive(out item))
{
await source.SendAsync(item);
}
else
{
// both input blocks must be completed
source.Complete();
return;
}
}
}
}
Usage would look like this:
b1.LinkTo(priorityBlock.HighPriorityTarget);
b2.LinkTo(priorityBlock.LowPriorityTarget);
priorityBlock.Source.LinkTo(a);
For this to work, a also has to have BoundingCapacity set to one (or at least a very low number).
The caveat with this code is that it can introduce latency of two messages (one waiting in the output block, one waiting in SendAsync()). So, if you have a long list of low priority messages and suddenly a high priority message comes in, it will be processed only after those two low-priority messages that are already waiting.
If this is a problem for you, it can be solved. But I believe it would require more complicated code, that deals with the less public parts of TPL Dataflow, like OfferMessage().
Here is an implementation of a PriorityBufferBlock<T> class, that propagates high priority items more frequently than low priority items. The constructor of this class has a priorityPrecedence parameter, that defines how many high priority items will be propagated for each low priority item. If this parameter has the value 1.0 (the smallest valid value), there is no real priority to speak of. If this parameter has the value Double.PositiveInfinity, no low priority item will ever be propagated as long as there are high priority items in the queue. If this parameter has a more normal value, like 5.0 for example, one low priority item will be propagated for every 5 high priority items.
This class maintains internally two queues, one for high and one for low priority items. The number of items stored in each queue is not taken into account, unless one of the two lists is empty, in which case all items of the other queue are freely propagated on demand. The priorityPrecedence parameter influences the behavior of the class only when both internal queues are non-empty. Otherwise, if only one queue has items, the PriorityBufferBlock<T> behaves like a normal BufferBlock<T>.
public class PriorityBufferBlock<T> : IPropagatorBlock<T, T>,
IReceivableSourceBlock<T>
{
private readonly IPropagatorBlock<T, int> _block;
private readonly Queue<T> _highQueue = new();
private readonly Queue<T> _lowQueue = new();
private readonly Predicate<T> _hasPriorityPredicate;
private readonly double _priorityPrecedence;
private double _priorityCounter = 0;
private object Locker => _highQueue;
public PriorityBufferBlock(Predicate<T> hasPriorityPredicate,
double priorityPrecedence,
DataflowBlockOptions dataflowBlockOptions = null)
{
ArgumentNullException.ThrowIfNull(hasPriorityPredicate);
if (priorityPrecedence < 1.0)
throw new ArgumentOutOfRangeException(nameof(priorityPrecedence));
_hasPriorityPredicate = hasPriorityPredicate;
_priorityPrecedence = priorityPrecedence;
dataflowBlockOptions ??= new();
_block = new TransformBlock<T, int>(item =>
{
bool hasPriority = _hasPriorityPredicate(item);
Queue<T> selectedQueue = hasPriority ? _highQueue : _lowQueue;
lock (Locker) selectedQueue.Enqueue(item);
return 0;
}, new()
{
BoundedCapacity = dataflowBlockOptions.BoundedCapacity,
CancellationToken = dataflowBlockOptions.CancellationToken,
MaxMessagesPerTask = dataflowBlockOptions.MaxMessagesPerTask
});
this.Completion = _block.Completion.ContinueWith(completion =>
{
Debug.Assert(this.Count == 0 || !completion.IsCompletedSuccessfully);
lock (Locker) { _highQueue.Clear(); _lowQueue.Clear(); }
return completion;
}, default, TaskContinuationOptions.ExecuteSynchronously |
TaskContinuationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();
}
public Task Completion { get; private init; }
public void Complete() => _block.Complete();
void IDataflowBlock.Fault(Exception exception) => _block.Fault(exception);
public int Count
{
get { lock (Locker) return _highQueue.Count + _lowQueue.Count; }
}
private Queue<T> GetSelectedQueue(bool forDequeue)
{
Debug.Assert(Monitor.IsEntered(Locker));
Queue<T> selectedQueue;
if (_highQueue.Count == 0)
selectedQueue = _lowQueue;
else if (_lowQueue.Count == 0)
selectedQueue = _highQueue;
else if (_priorityCounter + 1 > _priorityPrecedence)
selectedQueue = _lowQueue;
else
selectedQueue = _highQueue;
if (forDequeue)
{
if (_highQueue.Count == 0 || _lowQueue.Count == 0)
_priorityCounter = 0;
else if (++_priorityCounter > _priorityPrecedence)
_priorityCounter -= _priorityPrecedence + 1;
}
return selectedQueue;
}
private T Peek()
{
Debug.Assert(Monitor.IsEntered(Locker));
Debug.Assert(_highQueue.Count > 0 || _lowQueue.Count > 0);
return GetSelectedQueue(false).Peek();
}
private T Dequeue()
{
Debug.Assert(Monitor.IsEntered(Locker));
Debug.Assert(_highQueue.Count > 0 || _lowQueue.Count > 0);
return GetSelectedQueue(true).Dequeue();
}
private class TargetProxy : ITargetBlock<int>
{
private readonly PriorityBufferBlock<T> _parent;
private readonly ITargetBlock<T> _realTarget;
public TargetProxy(PriorityBufferBlock<T> parent, ITargetBlock<T> target)
{
Debug.Assert(parent is not null);
_parent = parent;
_realTarget = target ?? throw new ArgumentNullException(nameof(target));
}
public Task Completion => throw new NotSupportedException();
public void Complete() => _realTarget.Complete();
void IDataflowBlock.Fault(Exception error) => _realTarget.Fault(error);
DataflowMessageStatus ITargetBlock<int>.OfferMessage(
DataflowMessageHeader messageHeader, int messageValue,
ISourceBlock<int> source, bool consumeToAccept)
{
Debug.Assert(messageValue == 0);
if (consumeToAccept) throw new NotSupportedException();
lock (_parent.Locker)
{
T realValue = _parent.Peek();
DataflowMessageStatus response = _realTarget.OfferMessage(
messageHeader, realValue, _parent, consumeToAccept);
if (response == DataflowMessageStatus.Accepted) _parent.Dequeue();
return response;
}
}
}
public IDisposable LinkTo(ITargetBlock<T> target,
DataflowLinkOptions linkOptions)
=> _block.LinkTo(new TargetProxy(this, target), linkOptions);
DataflowMessageStatus ITargetBlock<T>.OfferMessage(
DataflowMessageHeader messageHeader, T messageValue,
ISourceBlock<T> source, bool consumeToAccept)
=> _block.OfferMessage(messageHeader,
messageValue, source, consumeToAccept);
T ISourceBlock<T>.ConsumeMessage(DataflowMessageHeader messageHeader,
ITargetBlock<T> target, out bool messageConsumed)
{
_ = _block.ConsumeMessage(messageHeader, new TargetProxy(this, target),
out messageConsumed);
if (messageConsumed) lock (Locker) return Dequeue();
return default;
}
bool ISourceBlock<T>.ReserveMessage(DataflowMessageHeader messageHeader,
ITargetBlock<T> target)
=> _block.ReserveMessage(messageHeader, new TargetProxy(this, target));
void ISourceBlock<T>.ReleaseReservation(DataflowMessageHeader messageHeader,
ITargetBlock<T> target)
=> _block.ReleaseReservation(messageHeader, new TargetProxy(this, target));
public bool TryReceive(Predicate<T> filter, out T item)
{
if (filter is not null) throw new NotSupportedException();
if (((IReceivableSourceBlock<int>)_block).TryReceive(null, out _))
{
lock (Locker) item = Dequeue(); return true;
}
item = default; return false;
}
public bool TryReceiveAll(out IList<T> items)
{
if (((IReceivableSourceBlock<int>)_block).TryReceiveAll(out IList<int> items2))
{
T[] array = new T[items2.Count];
lock (Locker)
for (int i = 0; i < array.Length; i++)
array[i] = Dequeue();
items = array; return true;
}
items = default; return false;
}
}
Usage example:
var bufferBlock = new PriorityBufferBlock<SaleOrder>(x => x.HasPriority, 2.5);
The above implementation supports all the features of the built-in BufferBlock<T>, except from the TryReceive with not-null filter. The core functionality of the block is delegated to an internal TransformBlock<T, int>, that contains a dummy zero value for every item stored in one of the queues.

Categories

Resources