返回Linq.对象(IEnumerable)从锁内-它是线程安全的吗?



考虑以下代码块

public class Data
{
public bool Init { get; set; }
public string Value {get; set; }
}
public class Example
{
private Object myObject = new Object();
private List<Data> myList = new List<Data>
{
new Data { Init = true, Value = "abc" },
new Data { Init = false, Value = "def" },
};
public IEnumerable<string> Get()
{
lock(this.myObject)
{
return this.myList.Where(i => i.Init == true).Select(i => i.Value);
}
}
public void Set(string value)
{
lock (this.myObject)
{
this.myList.Add(new Data { Init = false, Value = value });
}
}
}

如果多个线程正在调用Get()-该方法是线程安全的吗?

另外-在linq查询中调用.ToList()将使其线程安全吗?

return this.myList.Where(i => i.Init == true).Select(i => i.Value).ToList()

注意这里不能锁定:

public void Set(string value)
{
this.myList.Add(new Data { Init = false, Value = value });
}

所以在任何情况下都不是线程安全的。

假设您只是忘记这样做-它仍然不安全,因为Get返回"lazy"IEnumerable。它保存一个对myList的引用,并且只有在返回时才枚举IEnumerable本身。所以你正在泄漏对myList的引用,你试图用锁保护,锁语句之外的任意代码。

你可以这样测试:

var example = new Example();
var original = example.Get();
example.Clear(); // new method which clears underlying myList
foreach (var x in original)
Console.WriteLine(x);

这里调用Get,然后清空myList,然后枚举从Get得到的结果。人们可能天真地认为original将包含我们拥有的原始2项,但它不会包含任何内容,因为只有当我们枚举original时才会对其进行评估,并且在那个时间点- list已经被清除并且是空的。

如果使用

public IList<string> Get()
{
lock(this.myObject)
{
return this.myList.Where(i => i.Init == true).Select(i => i.Value).ToList();
}
}

那么它就"安全"了。现在你还不"懒"IEnumerable,但List<>的一个新实例与副本的值,你在myList。请注意,在这里将返回类型更改为IList是一个好主意,否则调用方可能会支付额外的开销(例如调用ToArray或ToList来创建副本),而在这种情况下则没有必要。

您必须意识到枚举序列 (= IEnumerable)和枚举序列本身(List, Array等)之间的差异。

你有一个类Example,它内部在成员MyList中保存一个List<Data>。每个Data至少有一个字符串属性"Value"。

类的例子有方法来提取Value,并添加新的元素到MyList

我不确定叫它们SetGet是否明智,这些名字很令人困惑。也许你已经简化了你的例子(顺便说一下,这使得谈论它变得更加困难)。

你有一个类Example的对象和两个线程,它们都可以访问这个对象。您担心,当一个线程枚举序列的元素时,另一个线程正在添加序列的元素。

您的Get方法返回"枚举"的"可能性"。在您从Get返回之后,并且在锁被处置之后,还没有枚举序列。

这意味着,当您开始枚举序列时,Data不再被锁定。如果您曾经从从数据库获取的数据中返回过IEnumerable,那么您可能已经看到了相同的问题:在开始枚举之前,与数据库的连接已被解除。

解决方案1:返回枚举数据:低效

您已经提到了一个解决方案:在返回之前枚举List中的数据。这样,从Get返回后,属性MyList将不再被访问,因此不再需要锁:

public IEnumerable<string> GetInitializedValues()
{
lock(this.MyList)
{
return this.MyList
.Where(data => data.Init == true)
.Select(data => data.Value)
.ToList();
}
}
也就是说:锁定MyList,它是一个Data序列。在这个序列中只保留那些具有属性Init的真值的数据。从每个剩余的Data中,取属性value的值并将它们放入List中。释放锁并返回List。

如果调用者不需要完整的列表,这是不有效的。

// Create an object of class Example which has a zillion Data in MyList:
Example example = CreateFilledClassExample();
// I only want the first element of MyList:
string onlyOneString = example.GetInitializedValues().FirstOrDefault();

GetInitializedValues创建一个包含无数元素的列表,并返回它。调用者只接受第一个初始化值,并丢弃列表的其余部分。真是浪费处理能力。

解决方案2:使用yield return:只枚举必须枚举的

关键字yield表示:返回序列的下一个元素。在调用者处置IEnumerator

之前,保持所有内容活动。
public IEnumerable<string> GetInitializedValues()
{
lock(this.MyList)
{
IEnumerable<string> initializedValues = this.MyList
.Where(data => data.Init == true)
.Select(data => data.Value);
foreach (string initializedValue in initializedValues)
{
yield return initializedValue;
}
}
}

因为yield在锁内,所以锁保持活动状态,直到您释放枚举数:

List<string> someInitializedValues = GetInitializedValues()
.Take(3)
.ToList();

这个是保存的,只枚举前三个元素。

在它的内部会这样做:

List<string> someInitializedValues = new List<string>();
IEnumerable<string> enumerableInitializedValues = GetInitializedValues();
// MyList is not locked yet!
// Create the enumerator. This is Disposable, so use using statement:
using (IEnumerator<string> initializedValuesEnumerator = enumerableInitializedValues.GetEnumerator())
{
// start enumerating the first 3 elements (remember: we used Take(3)
while (initializedValuesEnumerator.MoveNext() && someInitializedValues.Count < 3)
{
// there is a next element, fetch it and add the fetched value to the list
string fetchedInitializedValue = initializedValuesEnumerator.Current;
someInitializedValues.Add(fetchedInitializedValue);
}
// the enumerator is not disposed yet, MyList is still locked.
}
// the enumerator is disposed. MyList is not locked anymore

最新更新