15

我有一个多个类使用的 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;
    }
}
4

9 回答 9

30

通过“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 上)中途,另一个线程可以从您下方跳入并更改它。

如果 2 个线程从不访问任何共同的东西,那没关系,但一旦他们这样做,你需要防止它们互相踩到对方的脚趾。在 .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在 read 和return. 为了使这个假设真正正确,您需要在value该代码块的持续时间内锁定。

所以你会这样做:

lock( value )
{
     if(value == 0) 
     ... // all your code here
     return value;
}

然而

在 .NET 中,您只能锁定引用类型。Int32 是一个值类型,所以我们不能锁定它。
我们通过引入一个“虚拟”对象来解决这个问题,并将它锁定在我们想要锁定“值”的任何地方

这就是本·谢尔曼所指的。

于 2008-09-03T20:49:28.723 回答
8

正如 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;
        }
    }
}
于 2008-09-03T20:40:00.540 回答
2

您的线程可能会在该方法的中间停止并将控制权转移到不同的线程。您需要围绕该代码的关键部分...

private static object _lockDummy = new object();


...

lock(_lockDummy)
{
   //do stuff
}
于 2008-09-03T20:32:55.020 回答
2

这就是我所想的。但是,我正在寻找细节......'Toggle()'不是静态方法,但它是静态属性的成员(使用'Instance'时)。这就是让它在线程之间共享的原因吗?

如果您的应用程序是多线程的,并且您可以预见多个线程将访问该方法,这使得它在线程之间共享。因为你的类是一个单例,你知道不同的线程将访问相同的对象,所以要注意你的方法的线程安全。

以及这通常如何适用于单身人士。我必须在课堂上的每个方法中解决这个问题吗?

正如我上面所说,因为它是一个单例,你知道不同的线程可能会同时访问同一个对象。这并不意味着您必须让每个方法都获得锁。如果您注意到同时调用可能导致类的损坏状态,那么您应该应用@Thomas 提到的方法

于 2008-09-03T20:58:08.860 回答
2

我可以假设单例模式将我原本可爱的线程安全类暴露给常规静态成员的所有线程问题吗?

不,您的课程根本不是线程安全的。单例与它无关。

(我开始意识到调用静态对象的实例成员会导致线程问题)

这也与此无关。

你必须这样想:在我的程序中是否有可能让 2 个(或更多)线程同时访问这段数据?

您通过单例或静态变量或将对象作为方法参数传递来获取数据的事实并不重要。归根结底,这只是 PC RAM 中的一些位和字节,重要的是多个线程是否可以看到相同的位。

于 2008-09-03T21:03:02.967 回答
1

我还将向 MyClass 添加一个受保护的构造函数,以防止编译器生成公共默认构造函数。

于 2008-09-03T20:41:41.130 回答
1

我在想,如果我转储单例模式并强制每个人获取该类的新实例,它会缓解一些问题......但这并不能阻止其他人初始化该类型的静态对象并传递它。 ..或从多个线程中分离出来,所有线程都从同一个实例访问'Toggle()'。

答对了 :-)

我现在明白了。这是一个艰难的世界。我希望我没有重构遗留代码:(

不幸的是,多线程很难,你必须对事情非常偏执:-)在这种情况下,最简单的解决方案是坚持使用单例,并在值周围添加一个锁,就像在示例中一样。

于 2008-09-03T21:12:48.327 回答
0

引用:

if(value == 0) { value = 1; }
if(value == 1) { value = 0; }
return value;

value永远是0...

于 2008-09-03T20:32:20.280 回答
0

好吧,我实际上对 C# 不太了解……但我对 Java 还不错,所以我会给出答案,希望两者足够相似以至于它会很有用。如果没有,我道歉。

答案是,不,这不安全。一个线程可以与另一个线程同时调用 Toggle(),尽管使用此代码不太可能,但 Thread1 可能会value在 Thread2 检查它和设置它的时间之间进行设置。

要修复,只需制作 Toggle() synchronized。它不会阻塞任何东西或调用任何可能产生另一个可以调用 Toggle() 的线程的东西,所以这就是你保存它所要做的一切。

于 2008-09-03T20:36:47.273 回答