【问题标题】:Create a copy of IEnumerable<T> to modify collection from different threads?创建 IEnumerable<T> 的副本以修改来自不同线程的集合?
【发布时间】:2016-03-25 10:43:00
【问题描述】:

我正在使用一个线程方数据模型,它使用它的自定义数据模型。数据模型的层次结构如下:
型号
---表格(表格类型)
-----行(行类型)
-------细胞(细胞类型)

Table 与 DataTable 一样具有属性 Rows,我必须在多个任务中访问此属性。现在我需要表中的一行,该行具有指定值的列值。

为此,我创建了一个具有 lock 语句的方法,使其只能从一个线程访问一次。

public static Row GetRowWithColumnValue(Model model, string tableKey, string indexColumnKey, string indexColumnValue)
{
    Row simObj = null;
    lock (syncRoot)
    {
        SimWrapperFromValueFactory wrapperSimSystem = new SimWrapperFromValueFactory(model, tableKey, indexColumnKey);
        simObj = wrapperSimSystem.GetWrapper(indexColumnValue);
    }
    return simObj;
}

为了创建表中某一列的查找,我创建了一个方法,该方法总是尝试创建行的副本以避免集合修改异常:

Private Function GetTableRows(table As Table) As List(Of Row)
    Dim rowsList As New List(Of Row)(table.Rows)  'Case 1
    'rowsList.AddRange(table.Rows) 'Case 2
    ' Case 3
    'For i As Integer = 0 To table.Rows.Count - 1
    'rowsList.Add(table.Rows.ElementAt(i))
    'Next
    Return rowsList
End Function

但其他线程可以修改表(例如添加、删除行或更新任何行中的列值)。我得到以下“集合修改异常”:

at System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource resource)
   at System.Collections.Generic.List`1.Enumerator.MoveNextRare()
   at System.Collections.Generic.List`1.InsertRange(Int32 index, IEnumerable`1 collection)

我无法将此第三方库修改为并发集合和多个项目之间共享的相同数据模型。

问题:我正在寻找一种解决方案,该解决方案允许我允许此集合上的多个读者,或者它在另一个线程中修改.. 是否有可能获取集合的副本而不会出现异常??

在SO线程下引用但没有找到确切的解决方案:
Lock vs. ToArray for thread safe foreach access of List collection
Can ToArray() throw an exception?
Is returning an IEnumerable<> thread-safe?

【问题讨论】:

  • 如果我理解正确,您可以创建一个包含行的新列表并使用该行。 var rows = new List&lt;Row&gt;(GetTableRows());
  • 您不能“锁定”没有内置支持的集合(通过SyncRoot 或类似方法)。此外,即使您成功拍摄了数据快照(通过重试完整副本,直到最终无一例外地获得一个),您如何确保另一个线程没有通过修改行/单元格值来产生损坏?没有表本身所做的并发检查。这是一个棘手的情况。
  • @Domysee:我已经用 3 个实现对其进行了测试,我目前正在按照您的建议进行操作,但它也在内部使用 GetEnumerator 方法,这会导致问题。
  • @NiranjanKala,table.Rows 到底是什么?是List(Of Row) 吗?如果是这样,我建议使用CopyTo 方法,因为它绕过枚举器并且只访问2 个字段:_items_count,每个字段都访问一次,因此竞争的概率远低于涉及GetEnumerator 的任何内容。如果您想彻底,您还可以打破反射并检查操作前后的_version字段,以确保在您复制时列表没有被修改。如果您觉得这听起来很明智,请告诉我,我会将其编造成答案。
  • 当然请记住,这只会解决表级别的竞争问题(即保护您免受另一个线程添加、删除或交换行的影响)。如果行和单元格不是不可变的并且可以被另一个线程改变,那么您可能仍然不走运,因为您永远无法获得快照数据一致性保证。

标签: c# multithreading multitasking


【解决方案1】:

最简单的解决方案是重试异常,如下所示:

private List<Row> CopyVolatileList(IEnumerable<Row> original)
{
    while (true)
    {
        try
        {
            List<Row> copy = new List<Row>();

            foreach (Row row in original) {
                copy.Add(row);
            }

            // Validate.
            if (copy.Count != 0 && copy[copy.Count - 1] == null) // Assuming Row is a reference type.
            {
                // At least one element was removed from the list while were copying.
                continue;
            }

            return copy;
        }
        catch (InvalidOperationException)
        {
            // Check ex.Message?
        }

        // Keep trying.
    }
}

最终你会得到一个没有抛出异常并且数据完整性验证通过的运行。

或者,你可以潜得很深(我的意思是非常、非常深)。

免责声明:永远不要在生产中使用它。除非你绝望了,真的别无选择。

因此,我们确定您正在使用自定义集合 (TableRowCollection),该集合最终使用 List&lt;Row&gt;.Enumerator 来遍历行。这强烈表明您的收藏由List&lt;Row&gt; 支持。

首先,您需要获得对该列表的引用。您的收藏不会公开公开,因此您需要稍作调整。您将需要使用反射来查找并获取支持列表的值。我建议在调试器中查看您的TableRowCollection。它将向您显示非公开成员,您将知道要反映什么。

如果找不到您的List&lt;Row&gt;,请仔细查看TableRowCollection.GetEnumerator() - 特别是GetEnumerator().GetType()。如果返回List&lt;Row&gt;.Enumerator,那么宾果游戏:我们可以从中获取支持列表,如下所示:

List<Row> list;

using (IEnumerator<Row> enumerator = table.GetEnumerator())
{
    list = (List<Row>)typeof(List<Row>.Enumerator)
        .GetField("list", BindingFlags.Instance | BindingFlags.NonPublic)
        .GetValue(enumerator);
}

如果上述获取List&lt;Row&gt; 的方法均失败,则无需进一步阅读。还是放弃吧。

如果你成功了,既然你有 List&lt;Row&gt; 的支持,我们将不得不在 Reference Source 中寻找 List&lt;T&gt;

我们看到的是使用了 3 个字段:

private T[] _items;
private int _size; // Accessible via "Count".
private int _version;

我们的目标是将索引在零和_size - 1 之间的项目从_items 数组复制到一个新数组中,并在_version 更改之间进行。

关于线程安全的观察:List&lt;T&gt; 不使用锁,没有一个字段被标记为volatile_version 通过++ 递增,而不是Interlocked.Increment。长话短说,这意味着不可能读取所有 3 个字段值并自信地说我们正在查看稳定的数据。我们必须反复读取字段值,以便有点确信我们正在查看一个合理的快照(我们永远不会 100% 有信心,但您可能会选择满足于“好够了”)。

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;

private Row[] CopyVolatileList(List<Row> original)
{
    while (true)
    {
        // Get _items and _size values which are safe to use in tandem.
        int version = GetVersion(original); // _version.
        Row[] items = GetItems(original); // _items.
        int count = original.Count; // _size.

        if (items.Length < count)
        {
            // Definitely a torn read. Copy will fail.
            continue;
        }

        // Copy.
        Row[] copy = new Row[count];

        Array.Copy(items, 0, copy, 0, count);

        // Stabilization window.
        Thread.Sleep(1);

        // Validate.
        if (version == GetVersion(original)) {
            return copy;
        }

        // Keep trying.
    }
}

static Func<List<Row>, int> GetVersion = CompilePrivateFieldAccessor<List<Row>, int>("_version");
static Func<List<Row>, Row[]> GetItems = CompilePrivateFieldAccessor<List<Row>, Row[]>("_items");

static Func<TObject, TField> CompilePrivateFieldAccessor<TObject, TField>(string fieldName)
{
    ParameterExpression param = Expression.Parameter(typeof(TObject), "o");
    MemberExpression fieldAccess = Expression.PropertyOrField(param, fieldName);

    return Expression
        .Lambda<Func<TObject, TField>>(fieldAccess, param)
        .Compile();
}

注意重新稳定窗口:它越大,您就越有信心没有处理撕裂的读取(因为该列表正在修改所有 3 个字段)。我已经确定了我在测试中不能失败的最小值,我在一个线程上的紧密循环中调用了CopyVolatileList,并使用另一个线程将项目添加到列表中,删除它们或以随机间隔清除列表在 0 到 20 毫秒之间。

如果您移除稳定窗口,您偶尔会在数组末尾获得一个包含未初始化元素的副本,因为在您复制时其他线程已移除一行 - 这就是需要它的原因。

您显然应该在构建副本后尽最大努力对其进行验证(至少检查数组末尾的未初始化元素,以防稳定窗口失败)。

祝你好运。

【讨论】:

  • 感谢您提供详细信息,但我会尝试获取列表项
猜你喜欢
  • 2011-10-07
  • 2018-11-20
  • 2016-04-20
  • 1970-01-01
  • 1970-01-01
  • 2014-02-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多