代码之家  ›  专栏  ›  技术社区  ›  chiccodoro

C语言中的线程安全异步代码#

  •  4
  • chiccodoro  · 技术社区  · 14 年前

    几周前我问了下面的问题。现在,当回顾我的问题和所有答案时,一个非常重要的细节跃入我的眼帘:在我的第二个代码示例中,不是吗 DoTheCodeThatNeedsToRunAsynchronously() 在主(UI)线程中执行?计时器不只是等待一秒钟,然后将事件发布到主线程吗?这就意味着需要异步运行的代码根本不是异步运行的?!

    原始问题:


    我最近多次遇到一个问题,并用不同的方法解决了它,总是不确定它是否是线程安全的:我需要异步执行一段C代码。( )

    这段代码作用于由主线程代码提供的对象。 我将向您介绍两种我尝试过的方法(简化)和 :

    1. 实现我想要的最好的方法是什么?这是两种方法中的一种还是另一种?
    2. 是两种方法之一
    3. 第一种方法创建一个线程并将其传递给构造函数中的对象。我应该这样通过这个物体吗?
    4. 第二种方法使用的计时器不提供这种可能性,所以我只使用匿名委托中的局部变量。这是安全的还是理论上可能的,变量中的引用在被委托代码求值之前发生变化?( 无论何时使用匿名委托,这都是一个非常普通的问题 ). 在Java中,必须将局部变量声明为 最终的

    方法1:线程

    new Thread(new ParameterizedThreadStart(
        delegate(object parameter)
        {
            Thread.Sleep(1000); // wait a second (for a specific reason)
    
            MyObject myObject = (MyObject)parameter;
    
            DoTheCodeThatNeedsToRunAsynchronously();
            myObject.ChangeSomeProperty();
    
        })).Start(this.MyObject);
    

    这种方法有一个问题:我的主线程可能会崩溃,但由于僵尸线程的存在,进程仍然存在于内存中。


    方法2:定时器

    MyObject myObject = this.MyObject;
    
    System.Timers.Timer timer = new System.Timers.Timer();
    timer.Interval = 1000;
    timer.AutoReset = false; // i.e. only run the timer once.
    timer.Elapsed += new System.Timers.ElapsedEventHandler(
        delegate(object sender, System.Timers.ElapsedEventArgs e)
        {
            DoTheCodeThatNeedsToRunAsynchronously();
            myObject.ChangeSomeProperty();
        });
    
    DoSomeStuff();
    myObject = that.MyObject; // hypothetical second assignment.
    

    局部变量 myObject 这就是我在问题4中所说的。我添加了第二个作业作为示例。想象一下计时器过去了 之后 第二次分配,代表代码是否会操作 this.MyObject that.MyObject ?

    7 回复  |  直到 14 年前
        1
  •  2
  •   Heinzi    14 年前

    实现我想要的最好的方法是什么?这是两种方法中的一种还是另一种?

    两个看起来都不错,但是。。。

    这两种方法中有一种不是线程安全的吗(我担心两者都是…)为什么?

    …它们不是线程安全的 MyObject.ChangeSomeProperty() 是线程安全的。

    第一种方法创建一个线程并将其传递给构造函数中的对象。我应该这样通过这个物体吗?

    对。使用闭包(就像在第二种方法中一样)也很好,还有一个额外的优点,那就是你不需要进行强制转换。

    第二种方法使用的计时器不提供这种可能性,所以我只使用匿名委托中的局部变量。这是安全的还是理论上可能的,变量中的引用在被委托代码求值之前发生变化?(每当使用匿名代理时,这是一个非常普通的问题)。

    当然,如果你加上 myObject = null; timer.Elapsed this.MyObject 不会影响线程中捕获的变量。


    那么,如何让这个线程安全呢?问题是 myObject.ChangeSomeProperty(); myObject . 基本上有两种解决方案:

    :执行 myObject.ChangeSomeProperty() 在主界面中。这是最简单的解决方案,如果 ChangeSomeProperty 速度很快。你可以用 Dispatcher Control.Invoke (WinForms)跳回UI线程,但最简单的方法是使用 BackgroundWorker

    MyObject myObject = this.MyObject;
    var bw = new BackgroundWorker();
    
    bw.DoWork += (sender, args) => {
        // this will happen in a separate thread
        Thread.Sleep(1000);
        DoTheCodeThatNeedsToRunAsynchronously();
    }
    
    bw.RunWorkerCompleted += (sender, args) => {
        // We are back in the UI thread here.
    
        if (args.Error != null)  // if an exception occurred during DoWork,
            MessageBox.Show(args.Error.ToString());  // do your error handling here
        else
            myObject.ChangeSomeProperty();
    }
    
    bw.RunWorkerAsync(); // start the background worker
    

    方案2 ChangeSomeProperty() 通过使用 lock

        2
  •  5
  •   JaredPar    14 年前

    这些代码是否安全与 MyObject 实例。在这两种情况下,您都在共享 myObject 前台线程和后台线程之间的变量。没有什么可以阻止前台线程修改 我的对象 后台线程正在运行时。

    我的对象 . 然而,如果你没有具体计划,那么它肯定是一个不安全的操作。

        3
  •  4
  •   Stephen Cleary    14 年前

    Task 对象,并重新构造代码以使后台任务 它的计算值,而不是改变某些共享状态。

    a blog entry BackgroundWorker , Delegate.BeginInvoke , ThreadPool.QueueUserWorkItem Thread ),各有利弊。

    1. 实现我想要的最好的方法是什么?这是两种方法中的一种还是另一种? 任务 对象而不是特定的 或计时器回调。请参阅我的博客文章,了解原因,但总结如下: 支架 returning a result callbacks on completion , proper error handling universal cancellation system 在.NET中。
    2. 这两种方法中有一种不是线程安全的吗(我担心两者都是…)为什么? 正如其他人所说,这完全取决于 MyObject.ChangeSomeProperty 是线程安全的。在处理异步系统时,当每个异步操作都执行时,更容易推断线程安全性 结果 .
    3. 就我个人而言,我更喜欢使用lambda绑定,这是更安全的类型(无需强制转换)。
    4. 第二种方法使用的计时器不提供这种可能性,所以我只使用匿名委托中的局部变量。这是安全的还是理论上可能的,变量中的引用在被委托代码求值之前发生变化? 变量 ,而不是 ,所以答案是肯定的:在代理使用引用之前,引用可能会更改。如果引用可能更改,则通常的解决方案是创建一个单独的局部变量,该变量仅由lambda表达式使用,

    MyObject myObject = this.MyObject;
    ...
    timer.AutoReset = false; // i.e. only run the timer once.
    var localMyObject = myObject; // copy for lambda
    timer.Elapsed += new System.Timers.ElapsedEventHandler(
      delegate(object sender, System.Timers.ElapsedEventArgs e)
      {
        DoTheCodeThatNeedsToRunAsynchronously();
        localMyObject.ChangeSomeProperty();
      });
    // Now myObject can change without affecting timer.Elapsed
    

    ReSharper等工具将尝试检测lambdas中绑定的局部变量是否会更改,并在检测到这种情况时发出警告。

    任务 )会像这样:

    var ui = TaskScheduler.FromCurrentSynchronizationContext();
    var localMyObject = this.myObject;
    Task.Factory.StartNew(() =>
    {
      // Run asynchronously on a ThreadPool thread.
      Thread.Sleep(1000); // TODO: review if you *really* need this   
    
      return DoTheCodeThatNeedsToRunAsynchronously();   
    }).ContinueWith(task =>
    {
      // Run on the UI thread when the ThreadPool thread returns a result.
      if (task.IsFaulted)
      {
        // Do some error handling with task.Exception
      }
      else
      {
        localMyObject.ChangeSomeProperty(task.Result);
      }
    }, ui);
    

    注意,由于UI线程是调用 ,该方法不必是线程安全的。当然, DoTheCodeThatNeedsToRunAsynchronously 仍然需要线程安全。

        4
  •  3
  •   Philip Rieck    14 年前

    “线程安全”是一个狡猾的野兽。使用这两种方法,问题是线程正在使用的“MyObject”可能会被多个线程修改/读取,从而使状态看起来不一致,或者使线程的行为与实际状态不一致。

    例如,说你的 MyObject.ChangeSomeproperty() 必须在 MyObject.DoSomethingElse() ,或者它抛出。无论采用哪种方法,都无法阻止任何其他线程调用 DoSomethingElse() ChangeSomeProperty() 饰面。

    碰巧被两个线程调用,并且它(在内部)更改状态,线程上下文切换可能在第一个线程正在工作时发生,最终结果是两个线程之后的实际新状态为“错误”。

    然而,就其本身而言,两种方法都不是固有的线程不安全的,它们只需要确保更改的状态是序列化的,并且访问状态总是给出一致的结果。

    就我个人而言,我不会使用第二种方法。如果“僵尸”线程有问题,请设置 IsBackground

        5
  •  3
  •   Kiril    14 年前

    您的第一次尝试相当不错,但是即使在应用程序退出之后,线程仍然存在,因为您没有设置 IsBackground true ... 以下是您的代码的简化(和改进)版本:

    MyObject myObject = this.MyObject;
    Thread t = new Thread(()=>
        {
            Thread.Sleep(1000); // wait a second (for a specific reason)
            DoTheCodeThatNeedsToRunAsynchronously();
            myObject.ChangeSomeProperty();
        });
    t.IsBackground = true;
    t.Start();
    

    如果程序在上有争用,可能会遇到并发问题 MyObject .

    Java有 final 关键字和C#有一个对应的关键字 readonly ,但两者都不是 也不是 只读 确保要修改的对象的状态在线程之间保持一致。这些关键字所做的唯一一件事就是确保不更改对象所指向的引用。如果两个线程在同一对象上有读/写争用,那么您应该在该对象上执行某种类型的同步或原子操作,以确保线程安全。

    更新

    好的,如果你修改了 myObject 指的是,那么你的争论现在开始了 我的对象 . 我确信我的答案与您的实际情况不完全吻合,但根据您提供的示例代码,我可以告诉您将会发生什么:

    你会 确保修改了哪个对象: 可能是的 that.MyObject this.MyObject . 不管您使用的是Java还是C,这都是正确的。调度器可以安排线程/计时器在第二次分配之前、之后或期间执行。如果你指望的是一个特定的执行顺序,那么你必须这样做 以确保执行顺序。通常是这样 某物 ManualResetEvent , Join 或者别的什么。

    下面是一个连接示例:

    MyObject myObject = this.MyObject;
    Thread task = new Thread(()=>
        {
            Thread.Sleep(1000); // wait a second (for a specific reason)
            DoTheCodeThatNeedsToRunAsynchronously();
            myObject.ChangeSomeProperty();
        });
    task.IsBackground = true;
    task.Start();
    task.Join(); // blocks the main thread until the task thread is finished
    myObject = that.MyObject; // the assignment will happen after the task is complete
    

    这是一个 手动复位事件

    ManualResetEvent done = new ManualResetEvent(false);
    MyObject myObject = this.MyObject;
    Thread task = new Thread(()=>
        {
            Thread.Sleep(1000); // wait a second (for a specific reason)
            DoTheCodeThatNeedsToRunAsynchronously();
            myObject.ChangeSomeProperty();
            done.Set();
        });
    task.IsBackground = true;
    task.Start();
    done.WaitOne(); // blocks the main thread until the task thread signals it's done
    myObject = that.MyObject; // the assignment will happen after the task is done
    

    当然,在这种情况下,生成多个线程是毫无意义的,因为您不允许它们同时运行。避免这种情况的一种方法是不更改对的引用 我的对象 在你开始线程之后,你就不需要了 加入 WaitOne 手动复位事件 .

    我的对象 ? 这是for循环的一部分吗?for循环启动多个线程来执行多个异步任务?

        6
  •  2
  •   Dan Bryant    14 年前

    Monitor.Pulse AutoResetEvent

    ThreadPool.QueueUserWorkItem 最好用于短期运行的任务。如果应用程序死机,线程池线程也不会挂断应用程序,只要没有死锁阻止非后台线程死机。

        7
  •  1
  •   STW    14 年前

    线程方法的选择在很大程度上取决于 DoTheCodeThatNeedsToRunAsynchronously() 做。

    不同的.NET线程方法适用于不同的需求。一个非常重要的问题是这个方法是否会很快完成,或者需要一些时间(是短期的还是长期的?)。

    一些.NET线程机制,如 ThreadPool.QueueUserWorkItem() ,供短命线程使用。它们通过使用“回收的”线程避免了创建线程的开销——但是它将回收的线程数量是有限的,因此长时间运行的任务不应该占用线程池的线程。

    要考虑的其他选项包括:

    • ThreadPool.QueueUserWorkItem() 是一种在线程池线程上启动和忘记小任务的方便方法

    • System.Threading.Tasks.Task

    • Delegate.BeginInvoke() Delegate.EndInvoke() ( BeginInvoke() 将异步运行代码,但确保 EndInvoke() ThreadPool 我相信。

    • System.Threading.Thread 如您的示例所示。线程提供了最多的控制,但也比其他方法更昂贵,因此它们非常适合于长时间运行的任务或面向细节的多线程处理。

    总的来说,我个人的偏好是 Delegate.BeginInvoke()/EndInvoke()