[英]Binding source thread in PLINQ
我有一個我正在使用 PLINQ 並行化的計算,如下所示:
源IEnumerable<T> source
提供從文件讀取的對象。
我有一個重量級計算HeavyComputation
我需要在每個T
上做,我希望這些跨線程,所以我使用 PLINQ 像: AsParallel().Select(HeavyComputation)
這就是有趣的地方:由於對提供source
的文件讀取器類型的限制,我需要在初始線程而不是並行工作線程上枚舉source
。 我需要將source
的完整評估綁定到主線程。 然而,似乎源實際上是在工作線程上枚舉的。
我的問題是:是否有一種直接的方法可以修改此代碼以將source
的枚舉綁定到初始線程,同時將繁重的工作交給並行工作者? 請記住,只是在做一個熱心.ToList()
在之前AsParallel()
是不是一個不錯的選擇,因為從文件來的數據流是巨大的。
下面是一些示例代碼,它演示了我所看到的問題:
using System.Threading;
using System.Collections.Generic;
using System.Linq;
using System;
public class PlinqTest
{
private static string FormatItems<T>(IEnumerable<T> source)
{
return String.Format("[{0}]", String.Join(";", source));
}
public static void Main()
{
var expectedThreadIds = new[] { Thread.CurrentThread.ManagedThreadId };
var threadIds = Enumerable.Range(1, 1000)
.Select(x => Thread.CurrentThread.ManagedThreadId) // (1)
.AsParallel()
.WithDegreeOfParallelism(8)
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
.AsOrdered()
.Select(x => x) // (2)
.ToArray();
// In the computation above, the lambda in (1) is a
// stand in for the file-reading operation that we
// want to be bound to the main thread, while the
// lambda in (2) is a stand-in for the "expensive
// computation" that we want to be farmed out to the
// parallel worker threads. In fact, (1) is being
// executed on all threads, as can be seen from the
// output.
Console.WriteLine("Expected thread IDs: {0}",
FormatItems(expectedThreadIds));
Console.WriteLine("Found thread IDs: {0}",
FormatItems(threadIds.Distinct()));
}
}
我得到的示例輸出是:
Expected thread IDs: [1]
Found thread IDs: [7;4;8;6;11;5;10;9]
如果您放棄 PLINQ 而只是顯式地使用任務並行庫,這將相當簡單(雖然可能不那么簡潔):
// Limits the parallelism of the "expensive task"
var semaphore = new SemaphoreSlim(8);
var tasks = Enumerable.Range(1, 1000)
.Select(x => Thread.CurrentThread.ManagedThreadId)
.Select(async x =>
{
await semaphore.WaitAsync();
var result = await Task.Run(() => Tuple.Create(x, Thread.CurrentThread.ManagedThreadId));
semaphore.Release();
return result;
});
return Task.WhenAll(tasks).Result;
請注意,我使用Tuple.Create
來記錄來自主線程的線程 ID 和來自衍生任務的線程 ID。 根據我的測試,前者對於每個元組總是相同的,而后者則各不相同,這是應該的。
信號量確保並行度永遠不會超過 8(盡管創建元組的廉價任務無論如何這不太可能)。 如果達到 8,則任何新任務都將等到信號量上有可用點。
您可以使用下面的OffloadQueryEnumeration
方法,以確保源序列的枚舉將發生在枚舉結果IEnumerable<TResult>
的同一線程上。 querySelector
是將源序列的代理轉換為ParallelQuery<T>
的委托。 此查詢在ThreadPool
線程內部枚舉,但輸出值會返回到當前線程。
/// <summary>Enumerates the source sequence on the current thread, and enumerates
/// the projected query on a ThreadPool thread.</summary>
public static IEnumerable<TResult> OffloadQueryEnumeration<TSource, TResult>(
this IEnumerable<TSource> source,
Func<IEnumerable<TSource>, IEnumerable<TResult>> querySelector)
{
// Arguments validation omitted
var locker = new object();
(TSource Value, bool HasValue) input = default; bool inputCompleted = false;
(TResult Value, bool HasValue) output = default; bool outputCompleted = false;
using var sourceEnumerator = source.GetEnumerator();
IEnumerable<TSource> GetSourceProxy()
{
while (true)
{
TSource item;
lock (locker)
{
if (!input.HasValue)
{
if (inputCompleted) yield break;
Monitor.Wait(locker); continue;
}
item = input.Value; input = default;
Monitor.PulseAll(locker);
}
yield return item;
}
}
var query = querySelector(GetSourceProxy());
var task = Task.Run(() =>
{
try
{
foreach (var result in query)
{
lock (locker)
{
while (output.HasValue) Monitor.Wait(locker);
output = (result, true);
Monitor.PulseAll(locker);
}
}
}
finally
{
lock (locker) { outputCompleted = true; Monitor.PulseAll(locker); }
}
});
Exception sourceEnumeratorException = null;
while (true)
{
TResult result;
lock (locker)
{
if (output.HasValue)
{
result = output.Value; output = default;
Monitor.PulseAll(locker);
goto yieldResult;
}
if (outputCompleted) break;
if (input.HasValue || inputCompleted)
{
Monitor.Wait(locker); continue;
}
try
{
if (sourceEnumerator.MoveNext())
input = (sourceEnumerator.Current, true);
else
inputCompleted = true;
}
catch (Exception ex)
{
sourceEnumeratorException = ex;
inputCompleted = true;
}
Monitor.PulseAll(locker); continue;
}
yieldResult:
yield return result;
}
task.GetAwaiter().GetResult(); // Propagate possible exceptions
lock (locker) if (sourceEnumeratorException != null)
ExceptionDispatchInfo.Capture(sourceEnumeratorException).Throw();
}
此方法使用Monitor.Wait
/ Monitor.Pulse
機制( 教程),以便同步將值從一個線程傳輸到另一個線程。
用法示例:
int[] threadIds = Enumerable
.Range(1, 1000)
.Select(x => Thread.CurrentThread.ManagedThreadId)
.OffloadQueryEnumeration(proxy => proxy
.AsParallel()
.WithDegreeOfParallelism(8)
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
.AsOrdered()
.Select(x => x)
)
.ToArray();
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.