【问题标题】:C# Passing array of string element into a Task.RunC#将字符串元素数组传递到Task.Run
【发布时间】:2018-10-01 11:12:16
【问题描述】:

试图将字符串数组的元素传递给在 Task.Run 中调用的函数。任何人都知道这里的错误是什么?

这里的代码不起作用,它的行为就像 ProcessElem 永远不会被调用一样。

string[] arr = message.Split(new string[] {"\n"}, StringSplitOptions.None);

for (int i = 0; i < arr.Length; i++) {
    if(arr[i] != "") {
       var t = Task.Run(() => this.ProcessElem(arr[i]));
    }
 }

但是下面的代码可以工作

string[] arr = message.Split(new string[] {"\n"}, StringSplitOptions.None);

for (int i = 0; i < arr.Length; i++) {
    if(arr[i] != "") {
       var tmp = arr[i];
       var t = Task.Run(() => this.ProcessElem(tmp));
    }
 }

我对 C# 的处理方式非常陌生,但似乎这两种模式都不安全,因为调用 Task.Run() 的函数可能会在 ProcessElem 函数执行之前返回,如果字符串是通过引用传递的,那么它们将在调用 ProcessElem 之前被销毁。

如果是这种情况,将字符串传递到 ProcessElem 的最佳方法是什么?

另外,为什么第一个版本实际上没有“调用”ProcessElem?我在 ProcessElem 的顶部有一个打印语句,它只在第二个版本中打印。

【问题讨论】:

  • 什么是“打印声明”?您能否向我们展示“ProcessElem”或显示该问题的精简版本。而且,如果您在 WinForms、WPF、ASP.NET、控制台应用程序中运行什么样的环境......?
  • 这是一个控制台应用程序。 ProcessElem 只是打印传递给它的字符串。 public void ProcessElem(string str) { Console.WriteLine(str);}

标签: c# .net thread-safety task


【解决方案1】:

欢迎来到captured variables

Task.Run(() => this.ProcessElem(arr[i]))

这基本上意味着:

  1. 执行我的 lambda 操作:() =&gt; this.ProcessElem(arr[i])

  2. 在找到/创建线程后运行它。 一段时间后

但是,只涉及一个变量,i,它是在您的 lambda 操作范围之外定义的,它没有被复制,same 变量只是被捕获并引用。

当线程开始执行时,i 的值很可能已经改变。通常,循环在线程执行其工作之前结束。

这意味着到那时,i 等于 arr.Length 并且所有线程都尝试访问 arr[arr.length],这显然会导致 IndexOutOfRangeException

当您执行var tmp = arr[i]; 时,您将在每次循环迭代中创建一个新变量,复制循环变量并在您的 lambda 中捕获该副本,这就是它起作用的原因。

【讨论】:

    【解决方案2】:

    问题的根源在于实际的“协程”在 C# 中是如何工作的

    i 不是作为当前值传递,而是作为ref i 传递,这意味着您的Action 在执行时总是会收到当前的i 值。

    很有可能,您运行此代码并且任务不会并行执行。这意味着,执行的特定任务将获得i 的当前值,在大多数简单的情况下,它将作为退出条件提供:arr.Length + 1

    证明:

    for (int i = 0; i < arr.Length; i++)
    {
        if (arr[i] != "")
        {
            var j = i;
            var t = Task.Run(() => ProcessElem(arr[j]));
            tasklist.Add(t);
        }
    }
    

    可以正常工作(除非您的ProcessElem 方法有问题:P)

    关于字符串破坏,除非你有一些实现IDisposable的对象,否则你应该可以将它传递给一些lambda。 它会一直存在,直到实际的 lambda 被删除(因为它将保留对对象的一些引用,例如在这种情况下为 arr

    【讨论】:

    • 所以,当我执行 Task.Run(() => this.ProcessElem(arr[i])) 时,'i' 的值通过引用传入,因此它的值可能是 arr.Length (因为协程都可以在同一个线程中运行),这将是一个超出范围的索引?我了解协同例程的工作原理,但直觉上我认为 arr[i] 的值是传递给 ProcessElem 的值(在任务 ProcessElem 排队到 ThreadPool 时进行评估)。不是对 i 的引用,它将用于计算 arr[i] 的值
    【解决方案3】:

    您的问题是一个古老的问题,它是 lamdas 的工作原理,并且有据可查。

    但是,假设您只是创建和等待一堆任务,那么保存您自己的代码、麻烦和任务创建,只需使用 TPL Parallel.ForAsParallel

    Parallel.For(0, arr.Length, (i) => ProcessElem(arr[i]));
    

    或者

    arr.AsParallel().ForAll(ProcessElem);
    

    或者如果你真的不想要空字符串

    arr.Where(x => !string.IsNullOrEmpty(x))
       .AsParallel()
       .ForAll(ProcessElem);
    

    【讨论】:

      猜你喜欢
      • 2015-12-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多