它实际上是线程安全的(纯粹作为Count 上的实现细节问题),但是:
代码的线程安全 sn-ps 不是线程安全应用程序。您可以将不同的线程安全操作组合成非线程安全操作。事实上,很多非线程安全的代码都可以分解成更小的部分,所有这些部分本身都是线程安全的。
由于您希望的原因,它不是线程安全的,这意味着进一步扩展它不会是线程安全的。
这段代码是线程安全的:
public void CallToMethodInOtherClass(List<int> list)
{
//note we've no locks!
int i = list.Count;
//do something with i but don't touch list again.
}
使用任何列表调用它,它会根据该列表的状态为i 提供一个值,而不管其他线程在做什么。它不会损坏list。它不会给i 一个无效值。
所以虽然这段代码也是线程安全的:
public void CallToMethodInOtherClass(List<int> list)
{
Console.WriteLine(list[93]); // obviously only works if there's at least 94 items
// but that's nothing to do with thread-safety
}
这段代码不是线程安全的:
public void CallToMethodInOtherClass(List<int> list)
{
lock(locker)//same as in the question, different locker to that used elsewhere.
{
int i = list.Count;
if(i > 93)
Console.WriteLine(list[93]);
}
}
在进一步讨论之前,我描述为线程安全的两个位并未被 List 规范所承诺。保守的编码会假设它们不是线程安全的,而不是依赖于实现细节,但我将依赖于实现细节,因为它以一种重要的方式影响了如何使用锁的问题:
因为在list 上运行的代码没有首先获取locker 上的锁,所以不会阻止该代码与CallToMethodInOtherClass 并发运行。现在,虽然list.Count 是线程安全的,list[93] 是线程安全的,* 两者的组合我们依赖第一个来确保第二个工作不是线程安全的。因为锁外的代码会影响list,所以代码可以在Count 之间调用Remove 或Clear,以确保list[93] 可以工作,而list[93] 会被调用。
现在,如果我们知道 list 只会被添加到,那很好,即使同时发生调整大小,我们最终都会得到 list[93] 的值。如果有东西正在写入list[93],并且它是 .NET 将自动写入的一种类型(int 就是这样一种类型),我们最终会得到旧的或新的,就像我们' d 正确锁定我们会得到旧的或新的,这取决于哪个线程先锁定。 再次重申,这是一个实现细节,而不是指定的承诺,我这样说只是为了指出给定的线程安全性如何仍然导致非线程安全代码。
将其移向真实代码。我们不应该假设 list.Count 和 list[93] 是线程安全的,因为我们没有被保证它们会是线程安全的,而且这可能会改变,但即使我们确实有这个承诺,这两个承诺加起来也不会成为一个承诺它们在一起是线程安全的。
重要的是使用相同的锁来保护可能相互干扰的代码块。因此,请考虑以下保证线程安全的变体:
public class ThreadSafeList
{
private readonly object locker = new object();
private List<int> myList = new List<int>();
public void Add(int item)
{
lock(locker)
myList.Add(item);
}
public void Clear()
{
lock(locker)
myList.Clear();
}
public int Count
{
lock(locker)
return myList.Count;
}
public int Item(int index)
{
lock(locker)
return myList[index];
}
}
保证这个类在它所做的所有事情上都是线程安全的。在不依赖任何实现细节的情况下,这里没有任何方法会破坏状态或给出不正确的结果,因为另一个线程正在处理同一个实例。以下代码仍然不起作用:
// (l is a ThreadSafeList visible to multiple threads.
if(l.Count > 0)
Console.WriteLine(l[0]);
我们已经保证每个调用的线程安全 100%,但是我们没有保证组合,我们不能保证组合。
我们可以做两件事。我们可以为组合添加一个方法。对于专门为多线程使用而设计的许多类,以下内容很常见:
public bool TryGetItem(int index, out int value)
{
lock(locker)
{
if(l.Count > index)
{
value = l[index];
return true;
}
value = 0;
return false;
}
}
这使得计数测试和项目检索成为保证线程安全的单个操作的一部分。
或者,通常我们需要做的是,我们在操作分组的地方发生锁定:
lock(lockerOnL)//used by every other piece of code operating on l
if(l.Count > 0)
Console.WriteLine(l[0]);
当然,这会使ThreadSafeList 内的锁变得多余,而且只是浪费精力、空间和时间。这是大多数类不为其实例成员提供线程安全的主要原因 - 因为您无法有意义地保护类内部对成员的调用组,除非线程安全承诺,否则尝试这样做是浪费时间非常详细,并且非常有用。
回到问题中的代码:
CallToMethodInOtherClass 中的锁定应该被移除,除非OtherClass 有自己的内部锁定原因。它不能做出有意义的承诺,即它不会以非线程安全的方式组合,并且向程序添加更多锁只会增加分析它以确保没有死锁的复杂性。
对CallToMethodInOtherClass 的调用应该受到与该类中其他操作相同的锁的保护:
public void MethodeB()
{
lock(locker)
CallToMethodInOtherClass(myList);
}
那么只要CallToMethodInOtherClass 没有将myList 存储在稍后其他线程可以看到的地方,CallToMethodInOtherClass 不是线程安全的也没关系,因为唯一可以访问的代码myList 提供了自己的保证,不会与myList 上的其他操作同时调用它。
两个重要的事情是:
当某事被描述为“线程安全”时,请知道它的承诺是什么,因为有不同类型的承诺属于“线程安全”,它本身就意味着“我赢了'不要让这个对象进入一个荒谬的状态”,虽然它是一个重要的构建块,但它本身并不是很多。
锁定操作的组,每个组使用相同的锁,这将影响相同的数据,并保护对对象的访问,以便不可能有另一个线程不玩这个。
*这是一个非常有限的线程安全定义。在List<T> 上调用list[93],其中T 是一种将被原子读写的类型,我们不知道它实际上是否至少有94 个项目同样安全,无论是否有其他线程在其上运行.当然,在任何一种情况下它都可以抛出ArgumentOutOfRangeException这一事实并不是大多数人认为的“安全”,但我们对多线程的保证与一个线程相同。我们通过在单线程而不是在多线程情况下检查Count 获得了更强的保证,这导致我将其描述为不是线程安全的;虽然该组合仍然不会破坏状态,但它可能会导致我们向自己保证不会发生的异常。