System.ArgumentException 锁定时

System.ArgumentException when locking

在我的实际应用程序中,我需要迭代集合,但它可以从其他线程更改。所以我需要复制集合来迭代它。我将这个问题重现为一个小例子,但显然我对锁和线程缺乏理解导致 System.ArgumentException。用锁尝试了不同的东西,但结果是一样的。

class Program
{
    static List<int> list;
    static void Main(string[] args)
    {
        list = new List<int>();
        for (int i = 0; i < 1000000; i++)
        {
            list.Add(i);
            if (i == 1000)
            {
                Thread t = new Thread(new ThreadStart(WorkThreadFunction));
                t.Start();
            }
        }
    }

    static void WorkThreadFunction()
    {
        lock (list)
        {
            List<int> tmp = list.ToList();  //Exception here!
            Console.WriteLine(list.Count);
        }
    }
}

选项 1:

这是您的代码的修改版本:

class Program
{
    static List<int> list;
    static void Main(string[] args)
    {
        list = new List<int>();
        for (int i = 0; i < 1000000; i++)
        {
            lock (list) //Lock before modification
            {
                list.Add(i);
            }
            if (i == 1000)
            {
                Thread t = new Thread(new ThreadStart(WorkThreadFunction));
                t.Start();
            }

        }

        Console.ReadLine();
    }

    static void WorkThreadFunction()
    {
        lock (list)
        {
            List<int> tmp = list.ToList();  //Exception here!
            Console.WriteLine(list.Count);
        }
    }
}

这里发生的是您的 list 在转换为另一个列表集合时被修改(其中发生参数异常)。因此,为避免出现这种情况,您需要如上所示锁定列表。

选项 2:(否 lock

使用并发集合删除 lock:

using System.Collections.Concurrent;

//Change this line
static List<int> list;
//To this line
static ConcurrentBag<int> list;

并删除所有 lock 语句。

我发现您的算法存在一些问题,也许您应该重构它。在使用 locksConcurrentBag class 的情况下,您应该意识到将整个集合复制到新集合中只是为了枚举是非常庞大且非常耗时的操作,在此期间您可以'有效地使用集合。

lock (list)
{
    // VERY LONG OPERATION HERE
    List<int> tmp = list.ToList();  //Exception here!
    Console.WriteLine(list.Count);
}

你真的不应该 lock 收集这么长的时间 - 在 for 循环结束时你有很多 Threads 相互阻塞。您必须为此方法使用 TPL classes,不应直接使用 Threads

您可以选择的另一种情况是实施一些 optimistic lock-free algorithm with double check for the collection version, or even lock-free and wait-free algorithm with storing the snapshot of the collection and checking for it inside your methods for the collection access. Additional information can be found here

我认为您提供的信息不足以向您建议解决问题的正确方法。

尝试了 Joel 的建议。 ConcurrentBag 非常慢。锁定每百万次迭代似乎效率低下。看起来事件等待句柄在这种情况下很好(比我的电脑上的锁少 3 时间)。

 class Program
{
    static List<int> list;
    static ManualResetEventSlim mres = new ManualResetEventSlim(false);

    static void Main(string[] args)
    {
        list = new List<int>();
        for (int i = 0; i < 10000000; i++)
        {
            list.Add(i);

            if (i == 1000)
            {
                Thread t = new Thread(new ThreadStart(WorkThreadFunction));
                t.Start();
                mres.Wait();
            }
        }
    }

    static void WorkThreadFunction()
    {
        List<int> tmp = list.ToList();
        Console.WriteLine(list.Count);
        mres.Set();
    }
}