单例成员的线程安全使用
-
09-06-2019 - |
题
我有一个多个类使用的 C# 单例类。是否通过访问 Instance
到 Toggle()
方法线程安全?如果是,通过什么假设、规则等?如果没有,为什么 和 我该如何修复它?
public class MyClass
{
private static readonly MyClass instance = new MyClass();
public static MyClass Instance
{
get { return instance; }
}
private int value = 0;
public int Toggle()
{
if(value == 0)
{
value = 1;
}
else if(value == 1)
{
value = 0;
}
return value;
}
}
解决方案
通过“Instance”访问“Toggle()”类是线程安全的吗?如果是,通过什么假设、规则等?如果没有,为什么以及如何解决?
不,它不是线程安全的。
基本上,两个线程都可以运行 Toggle
同时起作用,所以可能会发生这种情况
// thread 1 is running this code
if(value == 0)
{
value = 1;
// RIGHT NOW, thread 2 steps in.
// It sees value as 1, so runs the other branch, and changes it to 0
// This causes your method to return 0 even though you actually want 1
}
else if(value == 1)
{
value = 0;
}
return value;
您需要根据以下假设进行操作。
如果有 2 个线程正在运行,它们可以并且将会在任何时候随机地相互交错和交互。您可能正在写入或读取 64 位整数或浮点(在 32 位 CPU 上),另一个线程可以跳入并从您下面将其更改。
如果这两个线程从不访问任何共同的东西,那也没关系,但是一旦它们访问了,您就需要防止它们互相踩到对方的脚趾。在 .NET 中执行此操作的方法是使用锁。
您可以通过考虑以下事项来决定锁定什么内容以及锁定位置:
对于给定的代码块,如果 something
从我下面变了,这有关系吗?如果是的话,你需要锁定它 something
在代码的持续时间内,这很重要。
再次查看您的示例
// we read value here
if(value == 0)
{
value = 1;
}
else if(value == 1)
{
value = 0;
}
// and we return it here
return value;
为了使其返回我们期望的结果,我们假设 value
读取和读取之间不会发生变化 return
. 。为了使这个假设真正正确,您需要锁定 value
在该代码块的持续时间内。
所以你会这样做:
lock( value )
{
if(value == 0)
... // all your code here
return value;
}
然而
在 .NET 中,您只能锁定引用类型。Int32是一个值类型,所以我们不能锁定它。
我们通过引入“虚拟”对象并锁定来解决这个问题 那 无论我们想在哪里锁定“价值”。
这是什么 本·谢尔曼 指的是.
其他提示
正如 Ben 指出的那样,最初的实现不是线程安全的
使其线程安全的一个简单方法是引入lock语句。例如。像这样:
public class MyClass
{
private Object thisLock = new Object();
private static readonly MyClass instance = new MyClass();
public static MyClass Instance
{
get { return instance; }
}
private Int32 value = 0;
public Int32 Toggle()
{
lock(thisLock)
{
if(value == 0)
{
value = 1;
}
else if(value == 1)
{
value = 0;
}
return value;
}
}
}
和我想的一样。但是,我正在寻找细节...'toggle()'不是静态方法,而是静态属性的成员(使用'instance')。那是什么使它在线程之间共享?
如果您的应用程序是多线程的,并且您可以预见多个线程将访问该方法,这使得它在线程之间共享。因为您的类是单例,所以您知道不同的线程将访问相同的对象,因此请注意方法的线程安全性。
这通常适用于单身人士。我必须在课堂上的每种方法中解决这个问题吗?
正如我上面所说,因为它是单例,所以你知道不同的线程可能会同时访问同一个对象。这并不意味着您必须让每个方法都获得锁。如果您注意到同时调用可能会导致类的状态损坏,那么您应该应用@Thomas提到的方法
我是否可以假设单例模式将我原本可爱的线程安全类暴露给常规静态成员的所有线程问题?
不。你的类根本就不是线程安全的。单例与此无关。
(我正在了解调用静态对象的实例成员会导致线程问题的事实)
这也与此无关。
你必须这样思考:我的程序中是否可以有2个(或更多)线程同时访问这段数据?
通过单例、静态变量或将对象作为方法参数传递来获取数据这一事实并不重要。归根结底,这只是 PC RAM 中的一些位和字节,重要的是多个线程是否可以看到相同的位。
您的线程可能会在该方法的中间停止并将控制权转移到另一个线程。您需要围绕该代码的关键部分......
private static object _lockDummy = new object();
...
lock(_lockDummy)
{
//do stuff
}
我还将向 MyClass 添加一个受保护的构造函数,以防止编译器生成公共默认构造函数。
我在想,如果我放弃单例模式并强制每个人都获得该类的新实例,这会缓解一些问题......但这并不能阻止其他人初始化该类型的静态对象并将其传递......或者通过分拆多个线程,所有线程都从同一实例访问“Toggle()”。
答对了 :-)
我现在明白了。这是一个艰难的世界。我希望我没有重构遗留代码:(
不幸的是,多线程很难,您必须对事情非常偏执:-)在这种情况下,最简单的解决方案是坚持使用单例,并在该值周围添加锁,就像示例中一样。
嗯,其实我不太了解 C#...但我擅长 Java,所以我会给出答案,希望两者足够相似,从而有用。如果没有,我深表歉意。
答案是,不,这不安全。一个线程可以与另一个线程同时调用 Toggle(),并且 Thread1 可以设置(尽管此代码不太可能) value
在 Thread2 检查它和设置它之间。
要修复,只需使用 Toggle() synchronized
. 。它不会阻塞任何东西,也不会调用任何可能产生另一个可以调用 Toggle() 的线程的东西,所以这就是你所要做的保存它。
引用:
if(value == 0) { value = 1; }
if(value == 1) { value = 0; }
return value;
value
永远是0...