在C#中的循环中使用lock语句

Rod*_*ley 14 c# multithreading loops locking

让我们尝试使用示例类SomeThread,我们试图阻止在Running属性设置为false之后调用DoSomething方法,并且由OtherThread类调用Dispose,因为如果在Dispose方法之后调用它们,则世界将以我们结束知道.

感觉因为循环,有可能发生邪恶的事情.在调用DoSomething方法之前,在启动下一个循环和锁定之前,可以将Running更改为false,并在它到达锁定之前调用Disposed.在这种情况下,生活不会很好.

当我在一个简单易于维护的方法中使用循环时,我正在研究如何处理这个问题.为了记录,我确实考虑过Double Lock Check图案,但它似乎并不适合C#.

警告:这是一个简化的示例,试图让您更容易关注循环和锁定问题.如果我没有详细说明某些地方请告诉我,我会尽力填写任何细节.

public class SomeThread : IDisposable
{
    private object locker = new object();
    private bool running = false;

    public bool Running 
    { 
        get
        {
            lock(locker)
            {
                return running;
            }
        }
        set
        {
            lock(locker)
            {
                running = value;
            }
        }
    }

    public void Run()
    {
        while (Running)
        {
            lock(locker)
            {
                DoSomething1();
                DoSomething2();
            }
        }
    }

    private void DoSomething1()
    {
        // something awesome happens here
    }

    private void DoSomething2()
    {
        // something more awesome happens here
    }

    public void Dispose()
    {
        lock (locker)
        {   
            Dispose1();
            Dispose2();
        }
    }

    private void Dispose1()
    {
        // something awesome happens here
    }

    private void Dispose2()
    {
        // something more awesome happens here
    }

}

public class OtherThread
{
    SomeThread st = new SomeThread();

    public void OnQuit()
    {
        st.Running = false;
        st.Dispose();

        Exit();
    }
}
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 46

退后一步.

首先开始编写解决方案之前指定所有理想和不良特性.一些立刻想到的:

  • "工作"在线程W上完成."UI"在线程U上完成.
  • 这项工作是在"工作单位"中完成的.对于"短"的某些定义,每个工作单元的持续时间都很短.让我们调用执行工作M()的方法.
  • W在循环中连续完成工作,直到U告诉它​​停止.
  • 完成所有工作后,U调用清理方法D().
  • D()必须在M()运行之前或运行时运行.
  • 必须在线程U上的D()之后调用Exit().
  • 你绝不能阻止"漫长"的时间; 它可以阻止"短暂"的时间.
  • 没有死锁,等等.

这总结了问题空间吗?

首先,我注意到乍一看似乎问题是U必须是D()的调用者.如果W是D()的调用者,那么你不必担心; 你只是发信号通知W突破循环,然后W会在循环后调用D().但这只会将一个问题换成另一个问题; 大概在这种情况下,在U调用Exit()之前,U必须等待W调用D().因此将D()的调用从U移动到W实际上并不会使问题更容易.

你已经说过你不想使用双重检查锁定.您应该知道,从CLR v2开始,已知双重检查的锁定模式是安全的.内存模型保证在v2中得到了加强.因此,使用双重检查锁定可能是安全的.

更新:您要求提供以下信息:(1)为什么在v2中双重检查锁定安全但在v1中不安全?(2)为什么我使用狡猾的词"可能"?

要理解为什么双重检查锁定在CLR v1内存模型中是不安全的,但在CLR v2内存模型中是安全的,请阅读:

http://web.archive.org/web/20150326171404/https://msdn.microsoft.com/en-us/magazine/cc163715.aspx

我说"可能",因为正如Joe Duffy明智地说:

一旦你冒险甚至略微超出了几个"有福"的无锁操作的范围[...],你就会打开自己的最恶劣的竞争条件.

我不知道你是否打算正确使用双重检查锁定,或者你是否计划在双重检查锁定上编写自己聪明的,破碎的变体,实际上在IA64机器上死得很厉害.因此,它可能适合您,如果您的问题实际上适合双重检查锁定并且您正确编写代码.

如果您关心这一点,您应该阅读Joe Duffy的文章:

http://www.bluebytesoftware.com/blog/2006/01/26/BrokenVariantsOnDoublecheckedLocking.aspx

http://www.bluebytesoftware.com/blog/2007/02/19/RevisitedBrokenVariantsOnDoubleCheckedLocking.aspx

这个问题有一些很好的讨论:

.NET中双重检查锁定需要volatile修饰符

可能最好找到除双重检查锁定之外的其他机制.

有一种机制可以等待一个正在关闭的线程完成 - thread.Join.您可以从UI线程加入工作线程; 当工作线程关闭时,UI线程再次唤醒并执行处理.

更新:添加了一些关于加入的信息.

"加入"基本上意味着"线程U告诉线程W关闭,U进入睡眠直到发生".退出方法简述:

// do this in a thread-safe manner of your choosing
running = false; 
// wait for worker thread to come to a halt
workerThread.Join(); 
// Now we know that worker thread is done, so we can 
// clean up and exit
Dispose(); 
Exit();   
Run Code Online (Sandbox Code Playgroud)

假设您出于某种原因不想使用"加入".(也许工作线程需要继续运行才能执行其他操作,但是您仍然需要知道何时使用对象完成.)我们可以使用等待句柄构建我们自己的机制,就像Join一样.你现在需要的是两个锁定机制:一个允许U向W发送一个信号,表示"现在停止运行",然后另一个在W完成对M()的最后一次调用时等待.

在这种情况下我会做的是:

  • 使线程安全标志"运行".使用您熟悉的任何机制使其线程安全.我个人会从一个致力于它的锁开始; 如果您稍后决定可以使用无锁互锁操作,那么您可以在以后随时执行此操作.
  • 使AutoResetEvent充当dispose上的门.

所以,简要草图:

UI线程,启动逻辑:

running = true
waithandle = new AutoResetEvent(false)
start up worker thread
Run Code Online (Sandbox Code Playgroud)

UI线程,退出逻辑:

running = false; // do this in a thread-safe manner of your choosing
waithandle.WaitOne(); 

// WaitOne is robust in the face of race conditions; if the worker thread
// calls Set *before* WaitOne is called, WaitOne will be a no-op.  (However,
// if there are *multiple* threads all trying to "wake up" a gate that is
// waiting on WaitOne, the multiple wakeups will be lost. WaitOne is named
// WaitOne because it WAITS for ONE wakeup. If you need to wait for multiple
// wakeups, don't use WaitOne.

Dispose();
waithandle.Close();
Exit();    
Run Code Online (Sandbox Code Playgroud)

工作线程:

while(running) // make thread-safe access to "running"
    M();
waithandle.Set(); // Tell waiting UI thread it is safe to dispose
Run Code Online (Sandbox Code Playgroud)

请注意,这取决于M()很短的事实.如果M()需要很长时间,那么你可以等待很长时间退出应用程序,这看起来很糟糕.

那有意义吗?

真的,你不应该这样做.如果要在处置正在使用的对象之前等待工作线程关闭,只需加入它即可.

更新:提出了一些其他问题:

没有超时等待是个好主意吗?

实际上,请注意在我的例子中使用Join和我的WaitOne示例,我不会在放弃之前使用等待特定时间的变体.相反,我呼吁我的假设是工作线程干净而迅速地关闭.这是正确的做法吗?

这取决于!这取决于工作线程的行为有多糟糕以及它在行为不端时的行为.

如果你可以保证工作的持续时间很短,无论什么"短"意味着你,那么你不需要超时.如果你不能保证,那么我会建议先重写代码,以便您可以保证; 如果你知道代码会在你提出要求时迅速终止,那么生活就会变得容易多了.

如果你做不到,那么正确的做法是什么?这种情况的假设是工人行为不端,并且在被要求时不会及时终止.所以现在我们必须问自己"工人在设计,车辆敌意方面是否会变慢?"

在第一种情况下,工人只是做一些需要很长时间的事情,无论出于何种原因,都不能被打断.这里做什么是正确的?我不知道.这是一个可怕的情况.据推测,工人不会很快关闭,因为这样做很危险或不可能.在那种情况下,当超时超时时你打算做什么?你有一些危险或不可能关闭的东西,它没有及时关闭.你的选择似乎是(1)什么都不做,(2)做一些危险的事情,或者(3)做一些不可能的事情.选择三可能是出局.选择一个相当于永远等待,我们已经拒绝了.这留下了"做一些危险的事".

了解正确的做法是为了尽量减少对用户数据的伤害,取决于造成危险的确切情况; 仔细分析,了解所有情景,并找出正确的事情.

现在假设工作者应该能够快速关闭,但不会因为它有错误.显然,如果可以的话,修复bug.如果你无法解决这个问题 - 或许它是你不拥有的代码 - 那么再次,你是一个可怕的修复.您必须了解在处理您现在正在另一个线程上使用的资源之前,不等待已经错误且因此无法预测的代码完成的后果.而且你必须知道在一个有缺陷的工作者线程仍忙于做天堂时终止应用程序的后果是什么只知道操作系统状态是什么.

如果代码是敌对的并且正在积极抵制被关闭那么你已经输了.你不能通过正常方式停止线程,你甚至无法线程中止它.无法保证中止恶意线程实际终止它; 你愚蠢地开始在你的进程中运行的恶意代码的所有者可以在finally块或其他约束区域中完成所有工作,这可以防止线程中止异常.

最好的办法是永远不要陷入这种状况; 如果你有你认为是敌对的代码,要么根本不运行它,要么在它自己的进程中运行它,并终止进程,而不是当事情发生严重时的线程.

简而言之,对于"如果花费太长时间我该怎么办?"这个问题没有一个好的答案.如果发生这种情况并且没有简单的答案,那么你处境很糟糕.最好努力确保你不要在第一时间进入它; 只运行合作,良性,安全的代码,当被问到时,它总是干净利落地自行关闭.

如果工人抛出异常怎么办?

好的,那又怎么样呢?再次,最好不要首先处于这种情况; 编写工作程序代码,使其不会抛出.如果你不能这样做,那么你有两个选择:处理异常,或者不处理异常.

假设您没有处理异常.从我认为CLR v2开始,工作线程中的未处理异常会关闭整个应用程序.原因是,在过去会发生什么事情,你会启动一堆工作线程,他们都会抛出异常,你最终会得到一个正在运行的应用程序,没有工作线程,没有工作,并且不告诉用户它.最好强制代码的作者处理由于异常导致工作线程失效的情况; 这样做可以有效地隐藏错误,并且可以轻松编写易碎的应用程序.

假设您确实处理了异常.怎么办?有些东西引发了异常,根据定义,这是一个意外的错误条件.您现在不知道任何数据是否一致,或者您的任何子系统中是否维护了任何程序不变量.你接下来打算怎么办?在这一点上你几乎没有什么安全可做的.

问题是"在这种不幸的情况下,对用户最有利的是什么?" 这取决于应用程序正在做什么.完全有可能在这一点上做的最好的事情就是简单地关闭并告诉用户意外失败的事情.这可能比试图混淆并可能使情况变得更糟,比如在尝试清理时意外破坏用户数据.

或者,最好的做法是尽最大努力保持用户的数据,整理尽可能多的状态,并尽可能正常终止.

基本上,你的问题都是"当我的子系统不能表现自己时我该怎么办?" 如果您的子系统不可靠,要么使它们可靠,要么制定有关如何处理不可靠子系统的策略,并实施该策略.这是我所知道的一个模糊的答案,但那是因为处理一个不可靠的子系统本身就是一个非常糟糕的情况.你如何处理它取决于它不可靠性的性质,以及这种不可靠性对用户有价值数据的影响.

  • @Creepy Gnome,不客气.我的选择总是使用显式锁定,直到性能测试表明锁定太慢.制作无锁bool标志并不难,特别是在其整个生命周期中只会从真到假的标志,但除非我有充分的理由,否则我不会这样做.无锁代码让我非常非常紧张. (3认同)

Ano*_*on. 6

Running再次检查锁内:

while (Running)
{
    lock(locker)
    {
        if(Running) {
            DoSomething1();
            DoSomething2();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您甚至可以将其重写为一个while(true)...break,这可能是更可取的.