代码之家  ›  专栏  ›  技术社区  ›  Rodney S. Foley

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

  •  14
  • Rodney S. Foley  · 技术社区  · 15 年前

    让我们将示例类somethread作为一个例子,在我们试图阻止DoSomething方法在running属性设置为false之后被调用,并且其他线程类调用Dispose,因为如果在Dispose方法is之后调用它们,世界将如我们所知结束。

    感觉好像有机会因为循环而发生一些邪恶的事情。在开始下一个循环的时候,在调用dosometing方法之前获取锁之前,可以将running更改为false,并在命中锁之前释放call。在这种情况下,生活是不好的。

    在使用简单易维护方法中的循环时,我正在寻找处理这一问题的方法。对于记录,我确实考虑了双重锁检查模式,但似乎不建议使用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();
        }
    }
    
    3 回复  |  直到 8 年前
        1
  •  44
  •   Community CDub    8 年前

    后退一步。

    首先指定所有需要和不需要的特性 之前 你开始写一个解决方案。一些立刻想到:

    • “工作”在线程W上完成。“UI”在线程U上完成。
    • 这项工作是以“工作单位”的形式完成的。每个工作单元的持续时间都是“短”的,对于某些“短”的定义来说。让我们调用执行该工作的方法m()。
    • 这项工作是由w在一个循环中连续完成的,直到u命令它停止为止。
    • 当所有的工作完成后,u调用一个清理方法d()。
    • d()不能在m()运行之前或运行时运行。
    • 在线程U上,exit()必须在d()之后调用。
    • 你不能长时间阻塞,短时间阻塞是可以接受的。
    • 没有死锁等等。

    这总结了问题空间吗?

    首先,我注意到 似乎 乍一看,问题是u必须是d()的调用方。如果w是d()的调用者,那么就不必担心了;您只需要向w发出信号,使其脱离循环,然后w会在循环之后调用d()。但这只是将一个问题换成另一个问题;在这种情况下,您必须等待w调用d(),然后才能调用exit()。因此,将对d()的调用从u移动到w实际上并不能使问题更简单。

    你说过你不想使用双重检查锁定。您应该知道,从clr v2开始,双重检查的锁定模式是安全的。V2增强了内存模型保证。所以使用双重检查锁定可能是安全的。

    更新:您询问了有关(1)为什么在v2而不是v1中双重检查锁定安全的信息?为什么我用黄鼠狼的词“可能”?

    要了解为什么双重检查锁定在clr v1内存模型中不安全,但在clr v2内存模型中安全,请阅读以下内容:

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

    我说“可能”,因为正如乔·达菲明智地说:

    一旦你到外面去冒险 少数“有福”的界限 无锁实践[…]您是 向最坏的人敞开心扉 比赛条件。

    我不知道您是否计划正确地使用双重检查锁定,或者您是否计划编写自己的巧妙的、中断的双重检查锁定变体,实际上在IA64机器上会死机。因此,它将 可能 为你工作,如果你的问题真的可以接受双重检查锁定 你写的代码是正确的。

    如果你关心这个,你应该读乔·达菲的文章:

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

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

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

    The need for volatile modifier in double checked locking in .NET

    也许最好是找到一些其他机制,而不是双重检查锁定。

    有一种机制可以等待正在关闭的一个线程完成——thread.join。您可以从UI线程连接到工作线程;当工作线程关闭时,UI线程将再次唤醒并进行释放。

    更新:添加了一些有关join的信息。

    “join”的基本意思是“线程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();   
    

    假设您出于某种原因不想使用“join”。(可能工作线程需要继续运行才能执行其他操作,但您仍然需要知道使用对象何时完成。)我们可以构建自己的机制,其工作方式与使用等待句柄进行联接类似。你现在需要的是 锁定机制:一个让你向W发送一个信号,表示“现在停止运行”,另一个则表示 等待 当w结束对m()的最后一个调用时。

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

    • 设置线程安全标志“运行”。使用任何你觉得合适的机制来保证它的安全。我个人会从一个专门用于它的锁开始;如果您稍后决定可以对它进行无锁联锁操作,那么您以后总是可以这样做。
    • 生成一个autoresetEvent作为释放上的入口。

    所以,简单的草图:

    UI线程,启动逻辑:

    running = true
    waithandle = new AutoResetEvent(false)
    start up worker thread
    

    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();    
    

    工作者线程:

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

    注意,这取决于m()是短的。如果m()需要很长时间,那么您可以等待很长时间来退出应用程序,这似乎很糟糕。

    这有道理吗?

    其实,你不应该这样做。如果要在释放工作线程正在使用的对象之前等待其关闭,只需将其联接。

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

    不超时等待是个好主意吗?

    事实上,请注意,在我的join示例和waitone示例中,我不使用在放弃之前等待特定时间的变量。相反,我指出我的假设是工作线程会快速而干净地关闭。这是正确的做法吗?

    这要看情况!这取决于工作线程的行为有多糟糕,以及当它行为不端时它在做什么。

    如果你能保证工作持续时间很短,不管“短”对你意味着什么,那么你不需要超时。如果您不能保证,那么我建议您首先重写代码,以便 可以 保证;如果你知道代码在你要求的时候会很快终止,生活就会变得容易得多。

    如果你不能,那你该怎么做?这种情况的假设是,工人行为不端,在被要求时不会及时终止。所以现在我们要问自己“是工人吗? 设计慢 ,请 婴儿车 怀有敌意的 ?“

    在第一种情况下,工人只是做了一些需要很长时间的事情,无论出于什么原因,都不能被打断。在这里做什么是正确的?我不知道。这是一个可怕的情况。可能工人没有很快关闭,因为这样做是危险的或不可能的。在这种情况下,当超时超时时,您要做什么????你有一些危险的或者不可能关闭的东西,而且它没有及时关闭。你的选择似乎是:(1)什么都不做,(2)做一些危险的事情,或(3)做一些不可能的事情。第三种选择可能会被淘汰。选择一等于永远等待,因为我们已经拒绝了。这就意味着“做一些危险的事情”。

    了解正确的操作以尽量减少对用户数据的伤害取决于造成危险的具体情况;仔细分析,了解所有情况,并找出正确的操作。

    现在假设工人应该能够快速关闭,但不是因为它有一个bug。很明显,如果可以的话,修复这个bug。如果您不能修复这个bug——也许它是在您不拥有的代码中——那么,您再次陷入了一个糟糕的修复中。您必须了解不等待已经出现错误的结果,因此在处理您知道它正在另一个线程上使用的资源之前,不可预知的代码会完成。当一个有缺陷的工作线程仍在忙着工作时,您必须知道终止应用程序的后果是什么,天堂只知道操作系统状态是什么。

    如果代码是 怀有敌意的 而且是 积极抵制关闭 那么你已经输了。你不能用正常的方法终止线程,甚至不能中止线程。无法保证中止一个敌对线程实际上会终止它;您在进程中愚蠢地开始运行的敌对代码的所有者可以在finally块或其他约束区域中完成其所有工作,从而防止线程中止异常。

    最好的办法是一开始就不要陷入这种情况;如果你有你认为是敌对的代码,要么根本不要运行它,要么在它自己的进程中运行它,然后终止 过程 不是 线程 当情况恶化时。

    简而言之,“如果时间太长,我该怎么做?”这个问题没有很好的答案。你在A 可怕的 如果发生这种情况,就没有简单的答案。最好努力工作,以确保你一开始不会接触到它;只运行合作的、良性的、安全的代码,当被要求时,它总是以干净和快速的方式关闭自己。

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

    好吧,如果有呢?同样,最好不要在这种情况下出现;编写工作者代码,使其不抛出。如果您不能这样做,那么您有两个选择:处理异常,或者不处理异常。

    假设您不处理异常。至于clr v2,工作线程中未处理的异常会关闭整个应用程序。原因是,在过去会发生的事情是,你会启动一组工作线程,它们都会抛出异常,最后你会得到一个运行中的应用程序,没有留下工作线程,没有做任何工作,也没有告诉用户。最好是强制代码的作者处理工作线程因异常而停机的情况;使用旧方法可以有效地隐藏错误并使编写脆弱的应用程序变得容易。

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

    问题是“在这种不幸的情况下,什么对用户最好?”这取决于应用程序正在做什么。在这一点上,最好的做法是简单地主动关闭并告诉用户意外的失败。这可能比在试图清理时不小心破坏了用户数据,可能会使情况变得更糟要好。

    或者,完全有可能的是,最好的做法是真诚地努力保存用户的数据,尽可能地整理状态,并尽可能正常地终止。

    基本上,您的两个问题都是“当我的子系统不工作时,我该怎么做?”如果你的子系统不可靠,或者 使它们可靠 对如何处理不可靠的子系统制定策略,并实现该策略 . 这是一个模糊的答案,我知道,但那是因为处理一个不可靠的子系统是一个固有的可怕的情况。如何处理它取决于它的不可靠性的性质,以及这种不可靠性对用户宝贵数据的影响。

        2
  •  6
  •   Anon.    15 年前

    检查 Running 再次进入锁内:

    while (Running)
    {
        lock(locker)
        {
            if(Running) {
                DoSomething1();
                DoSomething2();
            }
        }
    }
    

    你甚至可以把它改写为 while(true)...break 这可能更可取。

        3
  •  0
  •   Moose    15 年前

    而不是使用 bool 对于 Running ,为什么不使用状态为的枚举 Stopped ,请 Starting ,请 运行 Stopping ?

    这样,当 运行 设置为 停止 做你的处理。完成后, 运行 设置为 停止 . 当onquit()看到 正在运行 设置为 停止 ,它将继续前进并退出。

    编辑:这里有代码,快速和肮脏,没有测试等。

    public class SomeThread : IDisposable 
    { 
        private object locker = new object(); 
        private RunState running = RunState.Stopped;
    
        public enum RunState
        {
            Stopped,
            Starting,
            Running,
            Stopping,
        }
    
        public RunState Running  
        {  
            get 
            { 
                lock(locker) 
                { 
                    return running; 
                } 
            } 
            set 
            { 
                lock(locker) 
                { 
                    running = value; 
                } 
            } 
        } 
    
        public void Run() 
        { 
            while (Running == RunState.Running) 
            { 
                lock(locker) 
                { 
                    DoSomething1(); 
                    DoSomething2(); 
                } 
            } 
    
            Dispose();
    
        } 
    
        private void DoSomething1() 
        { 
            // something awesome happens here 
        } 
    
        private void DoSomething2() 
        { 
            // something more awesome happens here 
        } 
    
        public void Dispose() 
        { 
            lock (locker) 
            {    
                Dispose1(); 
                Dispose2(); 
            } 
            Running = RunState.Stopped;
        } 
    
        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 = SomeThread.RunState.Stopping; 
            while (st.Running == SomeThread.RunState.Stopping) 
            {                 
                // Do something while waiting for the other thread.
            }  
    
            Exit(); 
        } 
    }