我有一个 `List`,我想将每个 `byte[]` 反序列化为 `Foo`。这个 `List` 是有序的,我想写一个并行循环,使得结果的 `List` 包含所有 `Foo`,并且顺序与原始的 `byte[]` 相同。由于列表的规模很大,进行并行操作是值得的。有没有内置的方法可以实现这个?
如果没有,有没有任何想法可以加速比起同步运行的速度?
如果没有,有没有任何想法可以加速比起同步运行的速度?
根据您提供的信息,我理解您想要一个大小与输入字节数组相等的Foo输出数组?是这样吗?
如果是这样,那么操作很简单。不必担心锁定或同步结构,这些将消耗并行化带来的所有加速效果。
相反,如果您遵守这个简单的规则,任何算法都可以在没有锁定或同步的情况下并行化:
对于每个处理的输入元素X[i],您可以从任何输入元素X[j]读取,但只能写入输出元素Y[i]
查找Scatter / Gather,这种操作称为gather,因为只有一个输出元素被写入。
如果您可以使用上述原则,则需要预先创建输出数组Foo [],并在输入数组上使用Parallel.For而不是ForEach。
例如:
List<byte[]> inputArray = new List<byte[]>();
int[] outputArray = new int[inputArray.Count];
var waitHandle = new ManualResetEvent(false);
int counter = 0;
Parallel.For(0, inputArray.Count, index =>
{
// Pass index to for loop, do long running operation
// on input items
// writing to only a single output item
outputArray[index] = DoOperation(inputArray[index]);
if(Interlocked.Increment(ref counter) == inputArray.Count -1)
{
waitHandle.Set();
}
});
waitHandler.WaitOne();
// Optional conversion back to list if you wanted this
var outputList = outputArray.ToList();
Interlocked.Increment(ref counter)
进行完整通知,以测试是否已处理所有元素。然后设置一个等待句柄以继续进行。 - Dr. Andrew Burnett-Thompson将结果收集到数组中比收集到 List<Foo>
更容易。假设 List<byte[]>
的名称为 source
,你可以这样做:
Foo[] output = new Foo[source.Count];
ParallelOptions options = new() { MaxDegreeOfParallelism = Environment.ProcessorCount };
Parallel.ForEach(source, options, (byteArray, state, index) =>
{
output[index] = Deserialize(byteArray);
});
List<Foo>
中收集结果更复杂,因为List<T>
集合在并发写操作方面被明确文档化为不具备线程安全性。您可以使用lock
语句安全地更新它,如下所示:List<Foo> output = new(source.Count);
for (int i = 0; i < source.Count; i++) output.Add(default);
Parallel.ForEach(source, options, (byteArray, state, index) =>
{
Foo foo = Deserialize(byteArray);
lock (output) output[(int)index] = foo;
});
lock
仅保护output
列表的更新。如果Deserialize
同步化,那么并行化的目的将会失败。CollectionsMarshal.SetCount
API,预先填充一个未初始化的List<T>
,而无需进行循环。List<Foo> output = new(source.Count); // Set the Capacity
CollectionsMarshal.SetCount(output, source.Count); // Set the Count
替代方案:如果你愿意从Parallel.ForEach
切换到PLINQ,那就更简单了。通过使用PLINQ查询,你可以在并行操作中收集结果,而无需依赖副作用。只需在查询的末尾使用ToList
或ToArray
运算符:
List<Foo> output = source
.AsParallel()
.AsOrdered()
.WithDegreeOfParallelism(Environment.ProcessorCount)
.Select(byteArray => Deserialize(byteArray))
.ToList();
AsOrdered
操作符,否则顺序将不会保留。
Select
或Map
的并行等效版本以保留输入顺序。 - leppie