stand on the shoulders of giants

Threading in C# - Synchronization

1. Synchronization

Simple Blocking Methods

Construct

Purpose

Sleep

Blocks for a given time period.

Join

Waits for another thread to finish.

Locking Constructs

Construct

Purpose

Cross-Process?

Speed

lock

Ensures just one thread can access a resource, or section of code.

no

fast

Mutex

Ensures just one thread can access a resource, or section of code.
Can be used to prevent multiple instances of an application from starting

yes

moderate

Semaphore

Ensures not more than a specified number of threads can access a resource, or section of code.

yes

moderate

(Synchronization Contexts are also provided, for automatic locking).

Signaling Constructs

Construct

Purpose

Cross-Process?

Speed

EventWaitHandle

Allows a thread to wait until it receives a signal from another thread.

yes

moderate

Wait and Pulse*

Allows a thread to wait until a custom blocking condition is met.

no

moderate

Non-Blocking Synchronization Constructs*

Construct

Purpose

Cross-Process?

Speed

Interlocked*

To perform simple non-blocking atomic operations.

yes (assuming shared memory)

very fast

volatile*

To allow safe non-blocking access to individual fields outside of a lock.

very fast

a. Blocking

当一个线程通过上面所列的方式处于等待或暂停的状态,被称为被阻止。一旦被阻止,线程立刻放弃它被分配的CPU时间,将它的ThreadState属性添加为WaitSleepJoin状态,
判断方法:if ((worker.ThreadState & ThreadState.WaitSleepJoin) > 0)
停止阻止在任意四种情况下发生:

  • 阻止的条件已得到满足
  • 操作超时(如果timeout被指定了)
  • 通过Thread.Interrupt中断了
  • 通过Thread.Abort放弃了


    Thread.Sleep在阻止方法中是唯一的暂停汲取Windows Forms程序的Windows消息的方法,任何对主UI线程的阻止都将使程序失去相应。因此一般避免这样使用.
    你可以通过Join方法阻止线程直到另一个线程结束:Join方法也接收一个使用毫秒或用TimeSpan类的超时参数,当Join超时是返回false,如果线程已终止,则返回true 。Join所带的超时参数非常像Sleep方法,实际上下面两行代码几乎差不多:
    Thread.Sleep (1000);
    Thread.CurrentThread.Join (1000);

    b. Locking and Thread Safety

    锁实现互斥的访问,被用于确保在同一时刻只有一个线程可以进入特殊的代码片段。

     

    class ThreadSafe {
      
    static object locker = new object();
      
    static int val1, val2;
     
      
    static void Go() {
        
    lock (locker) {
          
    if (val2 != 0) Console.WriteLine (val1 / val2);
          val2 
    = 0;
        }
      }
    }

     

    lock(locker)   lock是锁,locker是同步对象。

    在同一时刻只有一个线程可以锁定同步对象(在这里是locker),任何竞争的的其它线程都将被阻止,直到这个锁被释放。如果有大于一个的线程竞争这个锁,那么他们将形成称为“就绪队列”的队列,以先到先得的方式授权锁。
    一个等候竞争锁的线程被阻止,ThreadStateWaitSleepJoin状态。

    C#的lock 语句实际上是调用Monitor.EnterMonitor.Exit,中间夹杂try-finally语句的简略版,下面是实际发生在之前例子中的Go方法: 

    Monitor.Enter (locker);
    try {
      
    if (val2 != 0) Console.WriteLine (val1 / val2);
      val2 
    = 0;
    }
    finally { Monitor.Exit (locker); }  

     

    Monitor 也提供了TryEnter方法来实现一个超时功能——也用毫秒或TimeSpan,如果获得了锁返回true,反之没有获得返回false

    i. Choosing the synchronization object

    任何对所有有关系的线程都可见的对象都可以作为同步对象,但要服从一个硬性规定:它必须是引用类型。也强烈建议同步对象最好私有在类里面(比如一个私有实例字段)防止无意间从外部锁定相同的对象。
    同步对象可以兼对象和保护两种作用。比如下面List
    class ThreadSafe {
      List <string> list = new List <string>();
      void Test() {
       lock (list) {
          list.Add ("Item 1");
          ...

    但是,A dedicated field is commonly used (such as locker, 还是专门用一个字段好。

    用对象或类本身的类型作为一个同步对象,是不好的,因为潜在的可以在公共范围访问这些对象。即:
    lock (this) { ... }
    or:
    lock (typeof (Widget)) { ... }    // For protecting access to statics

    ii. Nested Locking

    线程可以重复锁定相同的对象, 

    static object x = new object();
     
    static void Main() {
      
    lock (x) {
         Console.WriteLine (
    "I have the lock");
         Nest();
         Console.WriteLine (
    "I still have the lock");
      }
      Here the 
    lock is released.
    }
     
    static void Nest() {
      
    lock (x) {
         
      }
      Released the 
    lock? Not quite!
    }
  •  

    死锁 DeadLock

    看下面这段代码:

    public class Foo {
        
    public void CallBar() {
            
    lock (this) {
                Bar myBar 
    = new Bar ();
                myBar.BarWork(
    this);
            }
        }

        
    // This will be called back on a worker thread
        public void FooWork() {
            
    lock (this) {
                
    // do some work
                •••
            }
        }
    }

    public class Bar {
        
    public void BarWork(Foo myFoo) {
            
    // Call Foo on different thread via delegate.
            MethodInvoker mi = new MethodInvoker(
                myFoo.FooWork);
            IAsyncResult ar 
    = mi.BeginInvoke(nullnull);
            
    // do some work
            •••
            
    // Now wait for delegate call to complete (DEADLOCK!)
            mi.EndInvoke(ar);
        }
    }

    如果Foo‘s CallBar方法被调用,代码就会陷入停顿grind to a halt.

    CallBar 方法将获得 Foo 对象上的锁,并直到 BarWork 返回后才释放它。BarWork 使用异步委托调用,在线程池线程中调用 Foo 对象的 FooWork 方法。接下来,它会在调用委托的 EndInvoke 方法前执行一些其他操作。EndInvoke 将等待工作线程完成,但工作线程却被阻塞在 FooWork 中。它也试图获取 Foo 对象的锁,但锁已被 CallBar 方法持有。所以,FooWork 会等待 CallBar 释放锁,但 CallBar 也在等待 BarWork 返回。不幸的是,BarWork 将等待 FooWork 完成,所以 FooWork 必须先完成,它才能开始。结果,没有线程能够进行下去。

    这就是一个死锁的例子,其中有两个或更多线程都被阻塞以等待对方进行。这里的情形和标准死锁情况还是有些不同,后者通常包括两个锁。

    避免这种情况的最简单方法是,
    当持有一个对象锁时,不要等待跨线程调用完成(像上例,CallBar方法持有对象锁时,等待跨线程调用myBar.BarWork()调用完成)。
    要确保这一点,应该避免在锁语句中调用 Invoke 或 EndInvoke。其结果是,当持有一个对象锁时,不要再等待其他线程完成某操作。要坚持这个规则,说起来容易做起来难。

    在检查代码的 BarWork 时,它在锁语句的作用域内并不明显,因为在该方法中并没有锁语句。其实原因是 BarWork 调用自 Foo.CallBar 方法的锁语句。
    造成了BarWork也在锁范围内,这意味着只有确保正在调用的函数并不拥有锁时,调用 Control.Invoke 或 EndIn-voke 才是安全的。这句话的意思就是说,调用BarWork的函数CallBar方法拥有锁,再调用EndInvoke是不安全的, EndInvoke将等待工作线程完成,工作线程却被阻塞在 FooWork 中。它也试图获取 Foo 对象的锁,但锁已被 CallBar 方法持有。
    对于非私有方法而言,确保这一点并不容易,所以最佳规则是,根本不调用 Control.Invoke 和 EndInvoke。这就是为什么“启动后就不管launch and leave”的编程风格更可取的原因,也是为什么 Control.BeginInvoke 解决方案通常比 Control.Invoke 解决方案好的原因。

    c. Thread Safety

    线程安全的代码是指在面对任何多线程情况下,这代码都没有不确定的因素。
    如何实现: 1. blocking  2. 减少在线程间交互的可能性
    线程安全带来性能损失,程序复杂,不易维护。因此线程安全经常只在需要实现的地方来实现。.NET framework方面,几乎所有非初始类型的实例都不是线程安全的。

    两个线程同时为相同的List增加条目,然后枚举它: 

     

    class ThreadSafe {
      
    static List <string> list = new List <string>();
     
      
    static void Main() {
        
    new Thread (AddItems).Start();
        
    new Thread (AddItems).Start();
      }
     
      
    static void AddItems() {
        
    for (int i = 0; i < 100; i++)
          
    lock (list)
            list.Add (
    "Item " + list.Count);
     
        
    string[] items;
        
    lock (list) items = list.ToArray();
        
    foreach (string s in items) Console.WriteLine (s);
      }
    }

     

     

    在这种情况下,我们锁定了list对象本身,这个简单的方案是很好的。
    如果我们有两个相关的list,也许我们就要锁定一个共同的目标——可能是单独的一个字段,如果没有其它的list出现,显然锁定它自己是明智的选择。

    枚举.NET的集合也不是线程安全的,在枚举的时候另一个线程改动list的话,会抛出异常。在这个例子中,我们首先将项目复制到数组当中,这就避免了使用锁因为有潜在的耗时。

    这里的一个有趣的假设:想象如果List实际上为线程安全的,如何解决呢?代码会很少!举例说明,我们说我们要增加一个项目到我们假象的线程安全的list里,如下:
    if (!myList.Contains (newItem)) myList.Add (newItem);
    即使List是线程安全的,这个语句也不是,在判断有无和增加新的之间,线程彼此会发生抢占,所以整个if语句仍然需要放在lock内。内置的线程安全,显而易见是浪费时间!
    这就解释了为什么.NET类一般都不是线程安全的。

    一个遍及.NET framework一个普遍模式——静态成员是线程安全的,而一个实例成员则不是。

    d. Interrput & Abort

    一个被阻止的线程可以通过两种方式被提前释放:

    • 通过 Thread.Interrupt   打断,是打断当前阻止状态,恢复running
    • 通过 Thread.Abort

    这必须通过另外活动的线程实现,等待的线程是没有能力对它的被阻止状态做任何事情的。

    在一个被阻止的线程上调用Interrupt 方法,抛出ThreadInterruptedException异常,如下:

     

    class Program {
      
    static void Main() {
        Thread t 
    = new Thread (delegate() {
          
    try {
            Thread.Sleep (Timeout.Infinite);
          }
          
    catch (ThreadInterruptedException) {
            Console.Write (
    "Forcibly ");
          }
          Console.WriteLine (
    "Woken!");
        });
     
        t.Start();
        t.Interrupt();
      }
    }

     

    输出: Forcibly Woken!

    中断一个线程仅仅释放它的当前的等待状态:它并不结束这个线程。

    在Interrupt 与 Abort 之间最大不同在于它们调用一个非阻止线程所发生的事情。Interrupt,线程继续工作直到下一次阻止发生,Abort在线程当前所执行的位置(可能甚至不在你的代码中)抛出异常。终止一个非阻止的线程会带来严重的后果。

    E. Thread State

    [ThreadState Diagram]

    ThreadState是按位组合零或每个状态层的成员!一个简单的ThreadState例子
    Unstarted
    Running
    WaitSleepJoin
    Background, Unstarted
    SuspendRequested, Background, WaitSleepJoin

    你必须用按位与非操作符来代替. 但是ThreadState.Running潜在的值为0 ,因此下面的测试不工作:
    if ((t.ThreadState & ThreadState.Running) > 0) ...
    IsAlive可能不是你想要的,它在被阻止或挂起的时候返回true(只有在线程未开始或已结束时它才为false)。
    这个方法可以返回你想要的:

     

    public static ThreadState SimpleThreadState (ThreadState ts)
    {
      
    return ts & (ThreadState.Aborted | ThreadState.AbortRequested |
                   ThreadState.Stopped 
    | ThreadState.Unstarted |
                   ThreadState.WaitSleepJoin);
    }

     

     

    f. Wait Handles

    lock对一段代码或资源实施排他访问时,有些同步任务是笨拙的或难以实现的,比如说传输信号给等待的工作线程开始任务。
    有时它显得有些笨拙,不合适。线程除了等待锁被释放,什么也不能做。
    Win32 API拥有丰富的同步系统,这在.NET framework以EventWaitHandle, MutexSemaphore类展露出来。
    EventWaitHandle提供唯一的信号功能。EventWaitHandle有两个子类:AutoResetEvent 和 ManualResetEvent(不涉及到C#中的事件或委托)。
    AutoResetEventWaitHandle中是最有用的的类,它连同lock 语句是一个主要的同步结构。

    AutoResetEvent就像地铁闸机一样,插入一张票,一人通过。Auto在这里指的是当一个人通过后,已经打开的旋转门自动关闭或者Reset。
    一个线程调用WaitOne方法,等待或阻止(直到等到这个“one”,门才开) ,票的插入则由调用Set方法。如果由许多线程调用WaitOne,在门前便形成了队列,一张票可能来自任意某个线程——换言之,任何(非阻止)线程,如果它可以访问AutoResetEvent对象,则调用Set方法可以释放一个被阻止的的线程。

    AutoResetEvent可以通过2种方式创建,第一种是通过构造函数:
    EventWaitHandle wh = new AutoResetEvent (false);
    如果布尔参数为真,Set方法在构造后立刻被自动的调用,另一个方法是通过它的基类EventWaitHandle
    EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto);

    一个线程开始等待直到另一个线程发出信号:

     

    Code

     

     

    g. 任务确认

    设想我们希望不在每次我们得到任务时再创建一个新的线程。我们可以通过一个轮询的线程来完成:等待一个任务,执行它,然后等待下一个任务。
    这是一个普遍的多线程方案。也就是把创建多个线程的开支,分成各个任务序列执行。
    以减少在多个工作线程的交互和过多的资源消耗。

    工作线程正在忙之前的任务,有新的任务到来,我们需选择阻止调用者直到之前的任务被完成。像这样的系统可以用两个AutoResetEvent对象实现:一个“ready”AutoResetEvent,当准备好的时候,它被工作线程调用Set方法;和“go”AutoResetEvent,当有新任务的时候,它被调用线程调用Set方法。
    在下面的例子中,一个简单的string字段被用于决定任务(使用了volatile关键字声明,来确保两个线程都可以看到相同版本):

     

    class AcknowledgedWaitHandle {
      
    static EventWaitHandle ready = new AutoResetEvent (false);
      
    static EventWaitHandle go = new AutoResetEvent (false);
      
    static volatile string task;
     
      
    static void Main() {
        
    new Thread (Work).Start();
     
        
    // Signal the worker 5 times
        for (int i = 1; i <= 5; i++) {
          ready.WaitOne();                
    // First wait until worker is ready
          task = "a".PadRight (i, 'h');   // Assign a task
          go.Set();                       // Tell worker to go!
        }
     
        
    // Tell the worker to end using a null-task
        ready.WaitOne(); task = null; go.Set();
      }
     
      
    static void Work() {
        
    while (true) {
          ready.Set();                          
    // Indicate that we're ready
          go.WaitOne();                         // Wait to be kicked off
          if (task == nullreturn;             // Gracefully exit
          Console.WriteLine (task);
        }
      }
    }

     

    Result:
    ah
    ahh
    ahhh
    ahhhh

    注意我们要给task赋null来告诉工作线程退出。
    先调用ready.WaitOne,这和在工作线程上调用Interrupt Abort 效果是一样的。
    因为在调用ready.WaitOne后我们就知道工作线程的确切位置,就是在go.WaitOne语句之前,因此避免了中断任意代码的复杂性。调用 InterruptAbort需要我们在工作线程中捕捉异常。

    这段话的意思是如果我们用Interrupt,代码是这样的(对前面的例子): 

     

    static void Main()
            {
                Thread t 
    = new Thread(Waiter);
                t.Start();
                Thread.Sleep(
    1000);                  // Wait for some time
                
                t.Interrupt();                      
    //like notify

                Console.ReadLine();
            }

            
    static void Waiter()
            {
                
    try
                {
                    Console.WriteLine(
    "Waiting");
                    Thread.Sleep(Timeout.Infinite);
                }
                
    catch (ThreadInterruptedException)
                {
                    Console.WriteLine(
    "Notified");
                }

            }

     

     

    H. Producer/Consumer Queue

    除了上面的Acknowledgement方式,还有另一种Producer/Consumer队列的方式:
    有一个background worker负责处理队列中的任务,Producer负责任务入队,Consumer负责任务出队。
    和Acknowledgement不同的是,当Worker忙的时候,caller不需要block,前面的例子caller需要Ready.WaitOne()等待worker完成任务。caller只要负责入队,别的可以不管。

    生产者/消费者队列是可扩展的,可以定义multiple consumers – each servicing the same queue, but on a separate thread. 这是很好利用多处理器的方法。

    a single AutoResetEvent is used to signal the worker. 队列里是empty的时候,worker等待

     

    using System;
    using System.Threading;
    using System.Collections.Generic;
     
    class ProducerConsumerQueue : IDisposable {
      EventWaitHandle wh 
    = new AutoResetEvent (false);
      Thread worker;
      
    object locker = new object();
      Queue
    <string> tasks = new Queue<string>();
     
      
    public ProducerConsumerQueue() {
        worker 
    = new Thread (Work);
        worker.Start();
      }
     
      
    public void EnqueueTask (string task) {
        
    lock (locker) tasks.Enqueue (task);
        wh.Set();
      }
     
      
    public void Dispose() {
        EnqueueTask (
    null);     // Signal the consumer to exit.
        worker.Join();          // Wait for the consumer's thread to finish.
        wh.Close();             // Release any OS resources.
      }
     
      
    void Work() {
        
    while (true) {
          
    string task = null;
          
    lock (locker)
            
    if (tasks.Count > 0) {
              task 
    = tasks.Dequeue();
              
    if (task == nullreturn;
            }
          
    if (task != null) {
            Console.WriteLine (
    "Performing task: " + task);
            Thread.Sleep (
    1000);  // simulate work
          }
          
    else
            wh.WaitOne();         
    // No more tasks - wait for a signal
        }
      }
    }
    Here
    's a main method to test the queue:

    class Test {
      
    static void Main() {
        
    using (ProducerConsumerQueue q = new ProducerConsumerQueue()) {
          q.EnqueueTask (
    "Hello");
          
    for (int i = 0; i < 10; i++) q.EnqueueTask ("Say " + i);
          q.EnqueueTask (
    "Goodbye!");
        }
        
    // Exiting the using statement calls q's Dispose method, which
        
    // enqueues a null task and waits until the consumer finishes.
      }
    }

     

     

    为了便于理解,我们简化一下。

     

    //ProducerConsumerQueue 
    public ProducerConsumerQueue() {
          worker 
    = new Thread (Work);
    }

    public void go(){
          worker.Start();
    }

    //Main
    ProducerConsumerQueue qp = new ProducerConsumerQueue();
    qp.go();      
    qp.EnqueueTask(
    "First Task");      
    qp.Dispose();

     

     

    qp.go(), worker线程进入wh.waitone()等待状态;
    qp.EnqueueTask("First Task"); caller就是Producer入队"First Task", 然后wh.Set()唤醒Worker线程;
    worker线程就是Consumer让任务First Task出队,执行任务。任务执行完毕,再次进入等待状态;
    qp.Dispose() 中的EnqueueTask (null); 负责Signal the consumer to exit. 

    I. ManualResetEvent

    ManualResetEventAutoResetEvent的一种形式,它的不同之处在于:在线程WaitOne的调用而等待,set通过的时候,它不会自动地reset,这个过程就像大门一样——调用Set打开门,允许任何数量的WaitOne的线程通过;调用Reset关闭大门,可能会引起一系列的“等待者”直到下次门打开。

  • J. Mutex

    Mutex提供了与C#的lock语句同样的功能。它的优势在于它可以跨进程工作——提供了计算机范围的锁而胜于程序范围内的锁。

    Mutex在跨进程的普遍用处是确保在同一时刻只有一个程序的的实例在运行,下面演示如何使用:

    class OneAtATimePlease
        {
            
    // Use a name unique to the application (eg include your company URL)
            static Mutex mutex = new Mutex(false"oreilly.com OneAtATimeDemo");

            
    static void Main()
            {
                
    // Wait 5 seconds if contended – in case another instance
                
    // of the program is in the process of shutting down.
                if (!mutex.WaitOne(TimeSpan.FromSeconds(5), false))
                {
                    Console.WriteLine(
    "Another instance of the app is running. Bye!");                
                    
    return;
                }
                
    try
                {
                    Console.WriteLine(
    "Running - press Enter to exit");
                    Console.ReadLine();
                }
                
    finally { mutex.ReleaseMutex(); }
            }
        }

     

    WaitOne 方法获得exclusive lock, 如果存在竞争,则Blocking. ReleaseMutex method负责释放排他锁. 
    a Mutex 只能由获得它的同一个线程释放。

    过程是这样的,
    启动一个程序,waitone方法获得一个排它锁;
    再启动一个程序,此时存在竞争,因为设置了5秒所以第二个程序会“竞争”锁5秒,失败,return关闭。
    设置5秒是为了防止有的程序正在关闭,如果没有这个设置,第二个立即退出,第一个也退出,最后没有程序在运行了。

  • posted @ 2009-06-05 17:41  DylanWind  阅读(2120)  评论(1编辑  收藏  举报