如何安全地写入同一个List

时间:2018-03-15 19:19:39

标签: c# .net visual-studio-2017

我有public static List<MyDoggie> DoggieList;

在我的应用程序中,

DoggieList被多个进程追加并写入。

我们经常遇到这个例外:

  

收藏被修改;枚举操作可能无法执行

假设有多个类写入DoggieList,我们如何解决此异常?

请注意,此设计不是很好,但此时我们需要在生产中快速修复它。

我们如何安全地从多个线程对此列表执行突变?

我知道我们可以做类似的事情:

lock(lockObject)
{
   DoggieList.AddRange(...)
}

但是,我们可以针对相同的DoggieList从多个班级执行此操作吗?

3 个答案:

答案 0 :(得分:1)

使用lock防止并发读数的缺点。

不需要更改集合类型的有效解决方案是使用ReaderWriterLockSlim

private static readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

使用以下扩展方法:

public static class ReaderWriterLockSlimExtensions
{
    public static void ExecuteWrite(this ReaderWriterLockSlim aLock, Action action)
    {
        aLock.EnterWriteLock();
        try
        {
            action();
        }
        finally
        {
            aLock.ExitWriteLock();
        }
    }

    public static void ExecuteRead(this ReaderWriterLockSlim aLock, Action action)
    {
        aLock.EnterReadLock();
        try
        {
            action();
        }
        finally
        {
            aLock.ExitReadLock();
        }
    }
}

可以通过以下方式使用:

_lock.ExecuteWrite(() => DoggieList.Add(new Doggie()));

_lock.ExecuteRead(() =>
{
    // safe iteration
    foreach (MyDoggie item in DoggieList)
    {
        ....
    }
})

最后,如果您想基于此构建自己的集合:

public class SafeList<T>
{
    private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
    private readonly List<T> _list = new List<T>();

    public T this[int index]
    {
        get
        {
            T result = default(T);

            _lock.ExecuteRead(() => result = _list[index]);

            return result;
        }
    }

    public List<T> GetAll()
    {
        List<T> result = null;

        _lock.ExecuteRead(() => result = _list.ToList());

        return result;
    }

    public void ForEach(Action<T> action) => 
      _lock.ExecuteRead(() => _list.ForEach(action));

    public void Add(T item) => _lock.ExecuteWrite(() => _list.Add(item));

    public void AddRange(IEnumerable<T> items) => 
      _lock.ExecuteWrite(() => _list.AddRange(items));
}

这个列表是完全安全的,多个线程可以并行添加或获取项目而不会出现任何并发问题。此外,多个线程可以并行获取项目而不会相互锁定,只有在写入时才会有1个单线程可以处理集合。

请注意,此集合未实现IEnumerable<T>,因为您可能会获得一个枚举器并忘记处理它,这会使列表锁定在读取模式。

答案 1 :(得分:1)

你也可以创建自己的类并封装锁定的东西,你可以尝试如下,

你可以添加你想要的方法,如addRange,Remove等。

class MyList { 

  private object objLock = new object(); 
  private List<int> list = new List<int>();


  public void Add(int value) {
    lock (objLock) {
      list.Add(value);
    }
  }

  public int Get(int index) {
   int val = -1;
   lock (objLock) {
      val = list[0];
    }
    return val;
  }

  public void GetAll() {
   List<int> retList = new List<int>();
   lock (objLock) {
      retList = new List<T>(list);
    }
    return retList;
  }
}

好东西:并发收集非常详细:http://www.albahari.com/threading/part5.aspx#_Concurrent_Collections

利用并发集合ConcurrentBag类也可以解决与多线程更新相关的问题

示例

using System.Collections.Concurrent;
using System.Threading.Tasks;

public static class Program
{
    public static void Main()
    {
        var items = new[] { "item1", "item2", "item3" };
        var bag = new ConcurrentBag<string>();
        Parallel.ForEach(items, bag.Add);
    }
}

答案 2 :(得分:0)

制作DoggieList类型的ConcurrentStack,然后使用pushRange方法。它是线程安全的。

using System.Collections.Concurrent;

var doggieList = new ConcurrentStack<MyDoggie>();
doggieList.PushRange(YourCode)