Я написал небольшой конвейер, используя API потока данных TPL, который получает данные из нескольких потоков и выполняет обработку на них.
Настройка 1
Когда я настраиваю его на использование MaxDegreeOfParallelism = Environment.ProcessorCount
(приходит в 8
в моем случае) для каждого блока, я замечаю, что он заполняет буферы в нескольких потоках, а обработка второго блока начинается не раньше, чем до + 1700 элементов были получены во всех потоках. Вы можете увидеть это в действии здесь.
Настройка 2
Когда я устанавливаю MaxDegreeOfParallelism = 1
, я замечаю, что все элементы получены в одном потоке, и обработка отправки уже начинается после приема + - 40 элементов. Данные здесь.
Настройка 3
Когда я устанавливаю MaxDegreeOfParallelism = 1
и вводю задержку в 1000 мс перед отправкой каждого входа, я замечаю, что элементы отправляются сразу после их получения, и каждый полученный элемент помещается в отдельный поток. Данные здесь.
Пока настройка. Мои вопросы следующие:
-
Когда я сравниваю установки 1 и 2, я замечаю, что элементы обработки запускаются намного быстрее, когда они выполняются последовательно по сравнению с параллельными (даже после учета того факта, что параллель имеет 8x столько потоков). Что вызывает эту разницу?
-
Так как это будет запущено в среде ASP.NET, я не хочу создавать ненужные потоки, поскольку все они происходят из одного потока. Как показано в настройке 3, он все равно будет распространяться на несколько потоков, даже если имеется только несколько данных. Это также удивительно, потому что из установки 1 я бы предположил, что данные распределяются последовательно по потокам (обратите внимание, как первые 50 элементов все идут в поток 16). Могу ли я убедиться, что он создает новые потоки только по запросу?
-
Существует еще одна концепция, называемая
BufferBlock<T>
. Если входTransformBlock<T>
уже вставлен в очередь, какова будет практическая разница в замене первого шага в моем конвейере (ReceiveElement
) дляBufferBlock
?
class Program
{
static void Main(string[] args)
{
var dataflowProcessor = new DataflowProcessor<string>();
var amountOfTasks = 5;
var tasks = new Task[amountOfTasks];
for (var i = 0; i < amountOfTasks; i++)
{
tasks[i] = SpawnThread(dataflowProcessor, $"Task {i + 1}");
}
foreach (var task in tasks)
{
task.Start();
}
Task.WaitAll(tasks);
Console.WriteLine("Finished feeding threads"); // Needs to use async main
Console.Read();
}
private static Task SpawnThread(DataflowProcessor<string> dataflowProcessor, string taskName)
{
return new Task(async () =>
{
await FeedData(dataflowProcessor, taskName);
});
}
private static async Task FeedData(DataflowProcessor<string> dataflowProcessor, string threadName)
{
foreach (var i in Enumerable.Range(0, short.MaxValue))
{
await Task.Delay(1000); // Only used for the delayedSerialProcessing test
dataflowProcessor.Process($"Thread name: {threadName}\t Thread ID:{Thread.CurrentThread.ManagedThreadId}\t Value:{i}");
}
}
}
public class DataflowProcessor<T>
{
private static readonly ExecutionDataflowBlockOptions ExecutionOptions = new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount
};
private static readonly TransformBlock<T, T> ReceiveElement = new TransformBlock<T, T>(element =>
{
Console.WriteLine($"Processing received element in thread {Thread.CurrentThread.ManagedThreadId}");
return element;
}, ExecutionOptions);
private static readonly ActionBlock<T> SendElement = new ActionBlock<T>(element =>
{
Console.WriteLine($"Processing sent element in thread {Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine(element);
}, ExecutionOptions);
static DataflowProcessor()
{
ReceiveElement.LinkTo(SendElement);
ReceiveElement.Completion.ContinueWith(x =>
{
if (x.IsFaulted)
{
((IDataflowBlock) ReceiveElement).Fault(x.Exception);
}
else
{
ReceiveElement.Complete();
}
});
}
public void Process(T newElement)
{
ReceiveElement.Post(newElement);
}
}