创建IEnumerable <t>的副本以修改来自不同线程的集合?

时间:2016-03-25 10:43:00

标签: c# multithreading multitasking

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

Table的属性Rows与DataTable类似,我必须在多个任务中访问此属性。现在我需要表中的一行,其列值为指定值。

为此,我创建了一个具有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;
}

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

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

但是其他线程可以修改表(例如,添加,删除行或更新任何行中的列值)。我收到以下&#34;收集修改后的例外&#34;:

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)

我无法将此第三方库修改为并发集合,并且这个数据模型在多个项目之间共享。

问:我打猎,让我允许在此集合多个读者的解决方案也许是它在另一个线程修改..是否有可能获得集合的副本没有得到例外?? < / p>

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

1 个答案:

答案 0 :(得分: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<Row>.Enumerator来遍历行。这有力地表明您的收藏集由List<Row>支持。

首先,您需要获得对该列表的引用。您的收藏品不会公开曝光,因此您需要进行一些调整。您将需要使用Reflection来查找并获取支持列表的值。我建议您在调试器中查看TableRowCollection。它将向您展示非公开成员,您将知道要反映的内容。

如果找不到List<Row>,请仔细查看TableRowCollection.GetEnumerator() - 具体为GetEnumerator().GetType()。如果返回List<Row>.Enumerator,那么bingo:我们可以从中获取支持列表,如下所示:

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<Row>的方法失败,则无需进一步阅读。你不妨放弃。

如果您成功了,现在您已获得支持List<Row>,我们必须List<T>查看private T[] _items; private int _size; // Accessible via "Count". private int _version;

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

_size - 1

我们的目标是将索引在零到_items之间的项从_version数组复制到一个新数组中,并在List<T>次更改之间执行此操作。

观察线程安全:volatile不使用锁定,没有任何字段标记为_version++通过Interlocked.Increment递增,而不是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个字段值并自信地说我们正在查看稳定数据。我们必须反复阅读字段值,以便有点确信我们正在查看合理的快照(我们永远不会100%自信,但您可能会选择满足于“好”足够“)。

CopyVolatileList

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

如果你删除了稳定窗口,你偶尔会在数组的末尾得到一个带有未初始化元素的副本,因为另一个线程在你复制时删除了一行 - 这就是为什么需要它。

显然,你应该尽可能地验证副本(至少在稳定窗口失败的情况下检查数组末尾未初始化的元素)。

祝你好运。