阵列元素的多线程交换

时间:2012-08-29 21:59:44

标签: c# arrays multithreading concurrency locking

我必须通过并行交换随机索引元素来重排数组。 我的问题是如何防止其他线程读取和写入当前由另一个线程交换的元素。我不想在一个线程交换时锁定整个数组。

我想让几个线程在同一时间交换不同的元素。

我试过这样的事情:

        object[] lockArray = new object[array.Length];
        for (int i = 0; i < array.Length; i++)
            lockArray[i] = new object();

        for (int i = 0; i < thredCount; i++)
        {
            Thread t = new Thread(th => Shuffle.Shuflle(array,lockArray));
            t.Start();
        }

        public class Shuffle
        {
            public static void Shuflle(char[] array,object [] lockArray)
            {
                    for (int count = array.Length - 1; count > 1; count--)
                    {
                        Random rand = new Random();
                        int y = rand.Next(count) + 1;

                        lock (lockArray[count])
                        {
                            lock (lockArray[y])
                            {
                                char temp = array[count];
                                array[count] = array[y];
                                array[y] = temp;
                            }
                        }


                    }
            }
        }

在数组中有数字作为从0到9的字符, 结果是重新排序的数字。 但有时我会得到一个加倍的结果。 138952469. 9现在在混洗数组中加倍,并且缺少7个。

请帮我诊断问题。

3 个答案:

答案 0 :(得分:4)

根本不使用锁:

private void OptimisticalSwap(object[] arr, int i, int j, object sentinel, SpinWait spinWait)
{
  Interlocked.Increment(ref nSwap);
  if(i == j) return;
  var vi = ExchangeWithSentinel(arr, i, sentinel, spinWait);
  var vj = ExchangeWithSentinel(arr, j, sentinel, spinWait);
  Interlocked.Exchange(ref arr[i], vj);
  Interlocked.Exchange(ref arr[j], vi);
}

private object ExchangeWithSentinel(object[] arr, int i, object sentinel, SpinWait spinWait)
{
  spinWait.Reset();
  while(true) {
    var vi = Interlocked.Exchange(ref arr[i], sentinel);
    if(vi != sentinel) return vi;
    spinWait.SpinOnce();
  }
}

sentinel只是一些虚拟对象,在所有进行交换的线程之间共享,用于“保留”交换位置。

var sentinel = new object();

我的笔记本电脑(i7)上的运行结果:

Run 0 took 272ms (nSwap=799984, nConflict=300)
Run 1 took 212ms (nSwap=799984, nConflict=706)
Run 2 took 237ms (nSwap=799984, nConflict=211)
Run 3 took 206ms (nSwap=799984, nConflict=633)
Run 4 took 228ms (nSwap=799984, nConflict=350)

nConflict是交换未能保留位置的次数。与交换总数相比,它相当低,所以我针对没有冲突的情况优化了例程,只发生冲突时调用SpinUntil。

我测试的整个代码:

[TestClass]
  public class ParallelShuffle
  {
    private int nSwap = 0;
    private int nConflict = 0;
    [TestMethod]
    public void Test()
    {
      const int size = 100000;
      const int thCount = 8;
      var sentinel = new object();
      var array = new object[size];

      for(int i = 0; i < array.Length; i++)
        array[i] = i;

      for(var nRun = 0; nRun < 10; ++nRun) {
        nConflict = 0;
        nSwap = 0;
        var sw = Stopwatch.StartNew();
        var tasks = new Task[thCount];
        for(int i = 0; i < thCount; ++i) {
          tasks[i] = Task.Factory.StartNew(() => {
            var rand = new Random();
            var spinWait = new SpinWait();
            for(var count = array.Length - 1; count > 1; count--) {
              var y = rand.Next(count);
              OptimisticalSwap(array, count, y, sentinel, spinWait);
            }
          }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
        }

        Task.WaitAll(tasks);

        //Console.WriteLine(String.Join(", ", array));
        Console.WriteLine("Run {3} took {0}ms (nSwap={1}, nConflict={2})", sw.ElapsedMilliseconds, nSwap, nConflict, nRun);
        // check for doubles:
        var checkArray = new bool[size];
        for(var i = 0; i < array.Length; ++i) {
          var value = (int) array[i];
          Assert.IsFalse(checkArray[value], "A double! (at {0} = {1})", i, value);
          checkArray[value] = true;
        }
      }
    }


   private void OptimisticalSwap(object[] arr, int i, int j, object sentinel, SpinWait spinWait)
    {
      Interlocked.Increment(ref nSwap);
      if(i == j) return;
      var vi = ExchangeWithSentinel(arr, i, sentinel, spinWait);
      var vj = ExchangeWithSentinel(arr, j, sentinel, spinWait);
      Interlocked.Exchange(ref arr[i], vj);
      Interlocked.Exchange(ref arr[j], vi);
    }

    private object ExchangeWithSentinel(object[] arr, int i, object sentinel, SpinWait spinWait)
    {
      spinWait.Reset();
      while(true) {
        var vi = Interlocked.Exchange(ref arr[i], sentinel);
        if(vi != sentinel) return vi;
        spinWait.SpinOnce();
      }
    }

答案 1 :(得分:3)

出于好奇,你为什么要允许多个线程并行交换?交换不应该花费很长时间,因此在交换期间锁定整个数组可能比锁定单个元素的任何尝试都要快得多。

那就是说,如果你真的想做并行改组,你最好这样做:

  1. 使用单独的线程对阵列的每一半进行随机播放(它们不会发生冲突)。
  2. 对阵列进行“完美洗牌”(即交错双方的元素;有关详细信息,请参阅Faro Shuffle。)
  3. 再次对阵列的每一半进行随机播放。
  4. 如果你想做四个线程值得改组,你可以将它概括为四个部分。也就是说,'交换'必须是一个非常慢的操作,以便从中获得任何性能上的好处。

答案 2 :(得分:0)

一种简单的方法是使用Fisher-Yates算法创建映射列表,然后锁定源阵列上的所有操作,同时使用映射来执行转置en-mass。但是,这将消耗额外的资源,将花费更长的时间,并且只会略微减少阵列的易失性时间。你也可以像这样建立一个新的结果。

public static IEnumerable<T> Shuffle<T>(
    this IEnumerable<T> source,
    Random random = null)
{
    random = random ?? new Random();
    var list = source.ToList();

    for (int i = list.Length; i > 1; i--)
    {
        // Pick random element to swap.
        int j = random.Next(i); // 0 <= j <= i-1;

        // Swap.
        T tmp = list[j];
        list[j] = list[i - 1];
        list[i - 1] = tmp;
    }

    return list;
}

并做

var shuffler = new Task<char[]>(() => return array.Shuffle().ToArray());
array = shuffler.Result;

如果你想在多个线程上进行随机播放,你需要一个不同的算法和一个大的源来使它值得。