如何将 C# Parallel.For 与线程本地存储引用类型一起使用

How to use C# Parallel.For with thread local storage reference type

我正在寻找有关如何在 C# 中使用具有引用类型的 Parallel.For 的示例。我已经阅读了 MSDN 文档,我能找到的所有内容都是使用值类型进行线程本地存储的示例。我正在尝试的代码如下:

public string[] BuildStrings(IEnumerable<string> str1, IEnumerable<string> str2, IEnumerable<string> str3)
{
    // This method aggregates the strings in each of the collections and returns the combined set of strings.  For example:
    // str1 = "A1", "B1", "C1"
    // str2 = "A2", "B2", "C2"
    // str3 = "A3", "B3", "C3"
    //
    // Should return:
    // "A1 A2 A3"
    // "B1 B2 B3"
    // "C1 C2 C3"
    //
    // The idea behind this code is to use a Parallel.For along with a thread local storage StringBuilder object per thread.
    // Don't need any final method to execute after each partition has completed.
    // No example on how to do this that I can find.

    int StrCount = str1.Count(); // str1, str2, and str3 guaranteed to be equal in size and > 0.
    var RetStr = new string[StrCount];
    Parallel.For<StringBuilder>(0, StrCount, () => new StringBuilder(200), (i, j, sb1) =>
    {
        sb1.Clear();
        sb1.Append(str1.ElementAt(i)).Append(' ').Append(str2.ElementAt(i)).Append(' ').Append(str3.ElementAt(i));
        RetStr[i] = sb1.ToString();
    }, (x) => 0);
    return RetStr;
}

此代码无法在 Visual Studio 2013 Express 版上编译。错误出现在 Parallel.For 行,就在“(200),”之后:

"Not all code paths return a value in lambda expression of type 'System.Func< int,System.Threading.Tasks.ParallelLoopState,System.Text.StringBuilder,System.Text.StringBuilder>'"

测试代码如下所示:

static void Main(string[] args)
{
    int Loop;
    const int ArrSize = 50000;
    // Declare the lists to hold the first, middle, and last names of the clients.
    List<string> List1 = new List<string>(ArrSize);
    List<string> List2 = new List<string>(ArrSize);
    List<string> List3 = new List<string>(ArrSize);
    // Init the data.
    for (Loop = 0; Loop < ArrSize; Loop++)
    {
       List1.Add((Loop + 10000000).ToString());
       List2.Add((Loop + 10100000).ToString());
       List3.Add((Loop + 1100000).ToString());
    }
    IEnumerable<string> FN = List1;
    IEnumerable<string> MN = List2;
    IEnumerable<string> LN = List3;
    //
    // Time running the Parallel.For version.
    //
    Stopwatch SW = new Stopwatch();
    SW.Start();
    string[] RetStrings;
    RetStrings = BuildMatchArrayOld(FN, MN, LN);
    // Get the elapsed time as a TimeSpan value.
    SW.Stop();
    TimeSpan TS = SW.Elapsed;
    // Format and display the TimeSpan value. 
    string ElapsedTime = TS.TotalSeconds.ToString();
    Console.WriteLine("Old  RunTime = " + ElapsedTime);
}

我发现了另一个有点类似的问题 here,但它也无法编译。但是,使用更简单形式的函数的公认答案在这里对我没有帮助。对于这种特殊情况,我可以这样做,但我真的很想知道将来如何使用具有引用类型的线程本地存储。这是 MS 错误,还是我缺少正确的语法?

编辑

我确实尝试过此 link 中的代码:

static void Main()
{
    int[] nums = Enumerable.Range(0, 1000000).ToArray();
    long total = 0;

    // Use type parameter to make subtotal a long, not an int
    Parallel.For<long>(0, nums.Length, () => 0, (j, loop, subtotal) =>
    {
        subtotal += nums[j];
        return subtotal;
    },
        (x) => Interlocked.Add(ref total, x)
    );

    Console.WriteLine("The total is {0:N0}", total);
    Console.WriteLine("Press any key to exit");
    Console.ReadKey();
}

似乎工作正常。

问题是当我尝试在我的代码中使用 Parallel.For 并指定 return 值时,它给出了其他错误:

sb1.Append(str1.ElementAt(i)).Append(' ').Append(str2.ElementAt(i)).Append(' ').Append(str3.ElementAt(i));

此行现在生成错误:

Error 'System.Collections.Generic.IEnumerable' does not contain a definition for 'ElementAt' and the best extension method overload 'System.Linq.Enumerable.ElementAt(System.Collections.Generic.IEnumerable, int)' has some invalid arguments

所以,我不知道问题出在哪里。

原来让代码编译正确的问题是语法问题。如果微软为这种情况发布了一个示例,那真的会有所帮助。以下代码将构建并 运行 正确:

public string[] BuildStrings(IEnumerable<string> str1, IEnumerable<string> str2, IEnumerable<string> str3)
{
    // This method aggregates the strings in each of the collections and returns the combined set of strings.  For example:
    // str1 = "A1", "B1", "C1"
    // str2 = "A2", "B2", "C2"
    // str3 = "A3", "B3", "C3"
    //
    // Should return:
    // "A1 A2 A3"
    // "B1 B2 B3"
    // "C1 C2 C3"
    //
    // The idea behind this code is to use a Parallel.For along with a thread local storage StringBuilder object per thread.
    // Don't need any final method to execute after each partition has completed.
    // No example on how to do this that I can find.

    int StrCount = str1.Count(); // str1, str2, and str3 guaranteed to be equal in size and > 0.
    var RetStr = new string[StrCount];
    Parallel.For<StringBuilder>(0, StrCount, () => new StringBuilder(200), (i, j, sb1) =>
    {
        sb1.Clear();
        sb1.Append(str1.ElementAt(i)).Append(' ').Append(str2.ElementAt(i)).Append(' ').Append(str3.ElementAt(i));
        RetStr[i] = sb1.ToString();
        return sb1; // Problem #1 solved.  Signature of function requires return value.
    }, (x) => x = null); // Problem #2 solved.  Replaces (x) => 0 above.
    return RetStr;
}

因此,正如 Jon Skeet 在评论中指出的那样,第一个问题是我的 lambda 方法无法 return 一个值。因为我没有使用 return 值,所以我没有输入 - 至少一开始是这样。当我输入 return 语句时,编译器显示 "ElementAt" 静态方法的另一个错误 - 如上文 EDIT.

所示

事实证明,编译器标记为问题的 "ElementAt" 错误与问题完全无关。这让我想起了我在 C++ 的日子,当时编译器的帮助还不如 C# 编译器。将错误的行识别为错误在 C# 中很少见 - 但从这个例子可以看出,它确实发生了。

第二个问题是行 (x) => 0)。这一行是函数中的第 5 个参数,在所有工作完成后由每个线程调用。我最初尝试将其更改为 (x) => x.Clear。这最终生成了错误消息:

Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement

"ElementAt" 错误仍然存​​在。因此,根据这个线索,我决定 (x) => 0 可能是导致真正问题的原因 - 减去错误消息。由于此时工作已完成,我将其更改为将 StringBuffer 对象设置为 null,因为不再需要它。神奇的是,所有 "ElementAt" 错误都消失了。之后它构建并 运行 正确。

Parallel.For 提供了一些不错的功能,但我认为最好建议 Microsoft 重新访问其中的一些功能。任何时候线路导致问题,都应该被标记为问题。这至少需要解决。

如果 Microsoft 可以为 Parallel.For 提供一些额外的覆盖方法,允许 void 被 returned,并接受第 5 个参数的 null 值,那也很好。我实际上尝试为此发送一个 NULL 值,然后它就建立了。但是,因此出现了运行次异常。更好的想法是在不需要调用 "thread completion" 方法时为 4 个参数提供覆盖。

这是您自己的 For 重载的样子

    public static ParallelLoopResult For<TLocal>(int fromInclusive, int toExclusive, Func<TLocal> localInit, Func<int, ParallelLoopState, TLocal, TLocal> body)
    {
        return Parallel.For(fromInclusive, toExclusive, localInit, body, localFinally: _ => { });
    }

        static void StringBuilderFor(int count, Action<int, ParallelLoopState, StringBuilder> body)
    {
        Func<int, ParallelLoopState, StringBuilder, StringBuilder> b = (i, j, sb1) => { body(i, j, sb1); return sb1; };
        For(0, count, () => new StringBuilder(200), b);
    }

您也可以通过使用 LINQ 和 AsParallel() 而不是显式并行来避免整个问题。

        int StrCount = str1.Count(); // str1, str2, and str3 guaranteed to be equal in size and > 0.
        var RetStr = from i in Enumerable.Range(0, StrCount)
                     let sb1 = new StringBuilder(200)
                     select (sb1.Append(str1.ElementAt(i)).Append(' ').Append(str2.ElementAt(i)).Append(' ').Append(str3.ElementAt(i))).ToString();
        return RetStr.AsParallel().ToArray();

这可能不是那么快,但可能要简单得多。