【问题标题】:String.Concat and thread safety?String.Concat 和线程安全?
【发布时间】:2013-01-12 09:45:52
【问题描述】:

asking this question 之后,我还有一个问题。

我得到了两个很好的答案,但我仍然无法找到 这实际上是如何发生的:(故障情况怎么会发生):

我将从一个例子开始:

public void Do(string [] g)
{
   g=null;     //<========
}

void Main()
{
    var t=new string[3];
    t[0]="1";   t[1]="1";   t[2]="1";
    Do( t);
    Console.WriteLine ( t.Length);
}

注释行可以在不同的线程中执行,但每个线程都有其自己的 g 变量!
(请记住,我无法将项目添加到数组中。因为数组长度是在创建时创建的)

无论我将如何处理 函数 Do -(无论在哪个线程中),Console.Writeline 结果将始终为 3(除非使用 ref)。

让我们看看真正的代码:

    public static string Concat(params string[] values)
  #1  {
  #2      if (values == null)
  #3       {
  #4            throw new ArgumentNullException("values");
  #5       }
  #6        int totalLength = 0;
  #7        string[] strArray = new string[values.Length];
  #8        for (int i = 0; i < values.Length; i++)
  #9          {
  #10            string str = values[i];
  #11            strArray[i] = (str == null) ? Empty : str;
  #12            totalLength += strArray[i].Length;
  #13            if (totalLength < 0)
  #14            {
  #15                throw new OutOfMemoryException();
  #16            }
  #17         }
  #18        return ConcatArray(strArray, totalLength);
  #19   }

我的说法是:一旦我在 #1 上,在线程 X 中,这个线程将永远拥有长度为 3 的数组。

如果另一个线程想要销毁/更改数组长度(我无法理解如何,因为数组有固定长度,他所能做的就是让它@ 987654329@) - 它将有一个不同的指针地址副本。

我一定是错过了什么。

  • 我错过了什么?

  • 其他线程可以执行哪些代码会导致错误?(假设我们不复制数组)。

【问题讨论】:

  • @ilya 行号对于我的问题和答案参考是必需的。请不要删除它们。
  • 3 分钟后投反对票的问题真的很愚蠢,因为问题需要至少 5 分钟才能明白。
  • 问题不在于如果有人更改了局部变量 g 的值。问题是如果有人更改了g[0] 的值怎么办?这不会改变数组 t 的长度,但会改变数组 t 中的所有字符串的总长度

标签: c# .net string multithreading


【解决方案1】:

我在原始线程中查看了 Jon Skeet 和 Eric Lippert 的答案,我相信您误解了他们的答案。他们担心,如果您不将字符串复制到新数组中,那么在 ConcatString 第一次评估输入并执行连接之间,有人会过来将 {"1","2","3"} 更改为 {"a very very", "long string", "(but much longer, yes?"}

请注意,第 12 行处理的不是输入数组的长度,而是最终输出字符串的长度。所有这些检查都处理连接的结果,而不是输入数组。

【讨论】:

  • 没错。数组的长度不能改变,但是数组中的元素可以改变,影响字符串的总长度。
【解决方案2】:

其他线程可以执行哪些代码会导致错误?

string[] data = { "1", "2", "3" };
ThreadPool.QueueUserWorkItem( () => { data[0] = "one"; } );
string total = String.Concat(data);

如果没有中间数组,就会出现竞争条件,线程可能会在data[0] 之后将其长度添加到totalLength之前(可能是不安全的) 致电ConcatArray()。然后低级方法会将 5 个字符复制到大小为 3 的缓冲区。

【讨论】:

    【解决方案3】:

    数组引用被传递到方法中,因此每个线程(在这种情况下)都有自己的引用 g。每个线程都可以更改自己的引用指向的内容(例如 g = null),但是当您访问 g 中的项目时,例如 g[0],两个线程将访问同一个项目。

    所以,问题是g[0] = null,而不是g = null

    【讨论】:

    • 但是如果你看答案(在链接中),他们都说复制的原因是改变长度而不是内容!!!的数组。而不是内容
    • @RoyiNamir - Jon Skeet 谈论的是totalLength,它是结果 的长度,而不是valuesstrArray。它来源于valuescontent
    • 数组不是通过引用传递的。数组引用是按值传递的(默认情况下所有参数也是如此)。这是一个微妙但重要的区别。
    【解决方案4】:

    以下几行确保了数组内容在其他线程中被修改时的数据一致性

      #11            strArray[i] = (str == null) ? Empty : str;
      #12            totalLength += strArray[i].Length;
      #18        return ConcatArray(strArray, totalLength);
    

    比赛仍然存在,但在第号线之前。 11、但是如果使用原始数组(以防数据损坏)错误的信息可能会传递给ConcatArray函数。

    【讨论】:

    • stackoverflow.com/a/9910626/859154 他说的是数组的长度而不是值。
    • 可能我不清楚,根据数据一致性,我指的是ConcatArray函数调用中的totalLength参数,会导致输出错误(可能是异常)
    【解决方案5】:

    John Skeet 和 Eric Lippert 的意思是,如果在计算长度后数组内容可能发生变化,那么总字符串长度的计算会导致不同的结果。因为 concat 操作是在非托管代码缓冲区中完成的,所以如果之后的字符串长度确实发生了变化,则会发生溢出。

    为了说明问题,这里复制了他们所讨论的问题:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            static string[] arr = new string[] { "1", "2", "3" };
            static string[] arr2 = new string[] { "1111", "2222222", "2222222222222222222222" };
    
            [MTAThread]
            public static void Main()
            {
                ThreadStart[] funcs = Enumerable.Range(0, Environment.ProcessorCount * 4).Select(i => new ThreadStart(MutateArray)).ToArray();
                foreach (var func in funcs)
                    new Thread(func).Start();
                Console.ReadLine();
            }
    
            static void MutateArray()
            {
                Random rand = new Random();
                while (true)
                {
                    int i = rand.Next(arr.Length);
                    // swap array contents with contents from another array so arr will always
                    // contain a mixture of arr and arr2 without knowing at which point of time which contents
                    // are inside it.
                    lock (arr)
                    {
                        string tmp = arr[i];
                        arr[i] = arr2[i];
                        arr2[i] = tmp;
                    }
    
                    Do(arr); 
    
                }
            }
    
            static void Do(string[] g)
            {
                AllocateBufferWithRightLength(g, StrLen(g));
            }
    
            static void AllocateBufferWithRightLength(string[] g, int strLen)
            {
                int newLen = StrLen(g);
                if (strLen != newLen)
                {
                    throw new Exception("Invariant broken");
                }
            }
    
            static int StrLen(string[] g)
            {
                int strLen = 0;
                foreach (var str in g)
                {
                    if (str != null)
                        strLen += str.Length;
                }
                Thread.Sleep(1);
                return strLen;
            }
    
    
    
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-06-09
      • 2014-03-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多