簡體   English   中英

在 PLINQ 中綁定源線程

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM