考虑以下代码块
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
。
我不确定叫它们Set
和Get
是否明智,这些名字很令人困惑。也许你已经简化了你的例子(顺便说一下,这使得谈论它变得更加困难)。
你有一个类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