C#多线程实践——同步系统

  lock语句(即Monitor.Enter / Monitor.Exit)多用于当对一段代码或资源实施排他访问的线程同步场合, 但在需要传输信号给等待的工作线程使其开始任务执行等复杂应用场景下实现同步比较复杂。 .NET framework提供了EventWaitHandle, Mutex 和 Semaphore类用于构建丰富的同步系统,例如MutexEventWaitHandle提供唯一的信号功能时,会成倍提高lock的效率。这三个类都依赖于WaitHandle类,虽然它们的功能不尽相同,但都可以绕过操作系统进程工作,而不是只能在当前进程里绕过线程。

  EventWaitHandle有两个子类:AutoResetEvent 和 ManualResetEvent(不涉及到C#中的事件或委托)。两个类的不同点是在于用不同的参数调用基类的构造函数。性能方面,使用Wait Handles花费系统开销在微秒级别,不会在使用它们的上下文中产生太大影响。AutoResetEvent在WaitHandle中是最有用的的类,它连同lock 语句是一个主要的同步结构。

AutoResetEvent

  AutoResetEvent就像一个用票通过的旋转门:插入一张票,让正确的人通过。类名字里的“auto”实际上就是旋转门自动关闭或“重新安排”后来的人让其通过。一个线程等待或阻止通过在门上调用WaitOne方法(直到等到这个“one”,门才开) ,票的插入则由调用Set方法。如果由许多线程调用WaitOne,在门前便形成了队列,一张票可能来自任意某个线程——换言之,任何(非阻止)线程要通过AutoResetEvent对象调用Set方法来释放一个被阻止的的线程。

    也就是调用WaitOne方法的所有线程会阻塞到一个等待队列,其他非阻塞线程通过调用Set方法来释放一个阻塞。然后AutoResetEvent继续阻塞后面的线程。

    如果Set调用时没有任何线程处于等待状态,那么句柄保持打开直到某个线程调用了WaitOne 。这个行为避免了在线程起身去旋转门和线程插入票(哦,插入票是非常短的微秒间的事,真倒霉,你将必须不确定地等下去了!)间的竞争。但是在没人等的时候重复地在门上调用Set方法不会允许在一队人都通过,在他们到达的时候:仅有下一个人可以通过,多余的票都被“浪费了"。

    WaitOne 接受一个可选的超时参数——当等待以超时结束时这个方法将返回false,WaitOne在等待整段时间里也通知离开当前的同步内容,为了避免过多的阻止发生。

    Reset作用是关闭旋转门,也就是无论此时是否已经set过,都将阻塞下一次WaitOne——它应该是开着的。

    AutoResetEvent可以通过2种方式创建,第一种是通过构造函数:

EventWaitHandle wh = new AutoResetEvent (false);

如果布尔参数为真,Set方法在构造后立刻被自动的调用,也就是说第一个WaitOne会被放行,不会被阻塞,另一个方法是通过它的基类EventWaitHandle:

EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto);

EventWaitHandle的构造器也允许创建ManualResetEvent(用EventResetMode.Manual定义).

    在Wait Handle不在需要时候,你应当调用Close方法来释放操作系统资源。但是,如果一个Wait Handle将被用于程序(就像这一节的大多例子一样)的生命周期中,你可以发点懒省略这个步骤,它将在程序域销毁时自动的被销毁。

    接下来这个例子,一个线程开始等待直到另一个线程发出信号。

class BasicWaitHandle 

{

    static EventWaitHandle wh = new AutoResetEvent (false);

    static void Main() 

    {

        new Thread (Waiter).Start();

        Thread.Sleep (1000);                  // 等一会...

        wh.Set();                             // OK ——唤醒它

    }

 

    static void Waiter() 

    {

        Console.WriteLine ("Waiting...");

            wh.WaitOne();                        // 等待通知

        Console.WriteLine ("Notified");

    }

}

 

Waiting... (pause) Notified.

 

创建跨进程的EventWaitHandle

    EventWaitHandle的构造器允许以“命名”的方式进行创建,它有能力跨多个进程。名称是个简单的字符串,可能会无意地与别的冲突!如果名字使用了,你将引用相同潜在的EventWaitHandle,除非操作系统创建一个新的,看这个例子:

EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto,"MyCompany.MyApp.SomeName");

如果有两个程序都运行这段代码,他们将彼此可以发送信号,等待句柄可以跨这两个进程中的所有线程。

 

任务确认

   设想我们希望在后台完成任务,但又不在每次我们得到任务时再创建一个新的线程。我们可以通过一个轮询的线程来完成:等待一个任务,执行它,然后等待下一个任务。这是一个普遍的多线程方案。也就是在创建线程上切分内务操作,任务执行被序列化,在多个工作线程和过多的资源消耗间排除潜在的不想要的操作。 我们必须决定要做什么,但是,如果当新的任务来到的时候,工作线程已经在忙之前的任务了,设想这种情形下我们需选择阻止调用者直到之前的任务被完成。像这样的系统可以用两个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 == null) return;             // Gracefully exit

      Console.WriteLine (task);

    }

  }

}

 

 

ah

ahh

ahhh

ahhhh

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

 

生产者/消费者队列

   另一个普遍的线程方案是在后台工作进程从队列中分配任务。这叫做生产者/消费者队列:在工作线程中生产者入列任务,消费者出列任务。这和上个例子很像,除了当工作线程正忙于一个任务时调用者没有被阻止之外。

   生产者/消费者队列是可缩放的,因为多个消费者可能被创建——每个都服务于相同的队列,但开启了一个分离的线程。这是一个很好的方式利用多处理器的系统来限制工作线程的数量一直避免了极大的并发线程的缺陷(过多的内容切换和资源连接)。

   在下面例子里,一个单独的AutoResetEvent被用于通知工作线程,它只有在用完任务时(队列为空)等待。一个通用的集合类被用于队列,必须通过锁

控制它的访问以确保线程安全。工作线程在队列为null任务时结束:

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 == null) return;

        }

      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.

  }

}

Performing task: Hello 
Performing task: Say 1 
Performing task: Say 2 
Performing task: Say 3 
... 
... 
Performing task: Say 9 
Goodbye!

注意我们明确的关闭了Wait Handle在ProducerConsumerQueue被销毁的时候,因为在程序的生命周期中我们可能潜在地创建和销毁许多这个类的实例。

 

 ManualResetEvent

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

    你可以用一个布尔字段"gateOpen" (用 volatile 关键字来声明)与"spin-sleeping" – 方式结合——重复地检查标志,然后让线程休眠一段时间的方式,来模拟这个过程。

   ManualResetEvent有时被用于给一个完成的操作发送信号,又或者一个已初始化正准备执行工作的线程。

 

互斥(Mutex)

   Mutex提供了与C#的lock语句同样的功能,这使它大多时候变得的冗余了。它的优势在于它可以跨进程工作——提供了一计算机范围的锁而胜于程序范围的锁。

   Mutex是相当快的,而lock 又要比它快上数百倍,获取Mutex需要花费几微秒,获取lock需花费数十纳秒(假定没有阻止)。

   对于一个Mutex类,WaitOne获取互斥锁,当被抢占后时发生阻止。互斥锁在执行了ReleaseMutex之后被释放,就像C#的lock语句一样,Mutex只

能从获取互斥锁的这个线程上被释放。

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

  }

}

Mutex有个好的特性是,如果程序结束时而互斥锁没通过ReleaseMutex首先被释放,CLR将自动地释放Mutex。

 

Semaphore

   Semaphore就像一个夜总会:它有固定的容量,这由保镖来保证,一旦它满了就没有任何人可以再进入这个夜总会,并且在其外会形成一个队列。然后,当人一个人离开时,队列头的人便可以进入了。构造器需要至少两个参数——夜总会的活动的空间,和夜总会的容量。

   Semaphore 的特性与Mutex 和 lock有点类似,除了Semaphore没有“所有者”——它是不可知线程的,任何在Semaphore内的线程都可以调用Release,而Mutex 和 lock仅有那些获取了资源的线程才可以释放它。

   在下面的例子中,10个线程执行一个循环,在中间使用Sleep语句。Semaphore确保每次只有不超过3个线程可以执行Sleep语句:

class SemaphoreTest 

{

  static Semaphore s = new Semaphore (3, 3);  // Available=3; Capacity=3

  

  static void Main() 

{

    for (int i = 0; i < 10; i++) new Thread (Go).Start();

  }

  

  static void Go()

 {

    while (true) {

      s.WaitOne();

      Thread.Sleep (100);   // Only 3 threads can get here at once

      s.Release();

    }

  }

}

WaitAny, WaitAll 和 SignalAndWait

 

    除了Set 和 WaitOne方法外,在类WaitHandle中还有一些用来创建复杂的同步过程的静态方法。

    WaitAny, WaitAll 和 SignalAndWait使跨多个可能为不同类型的等待句柄变得容易。

   SignalAndWait可能是最有用的了:他在某个WaitHandle上调用WaitOne,并在另一个WaitHandle上自动地调用Set。你可以在一对EventWaitHandle上装配两个线程,而让它们在某个时间点“相遇”,这马马虎虎地合乎规范。AutoResetEvent 或 ManualResetEvent都无法使用这个技巧。第一个线程像这样:

   WaitHandle.SignalAndWait (wh1, wh2);

   同时第二个线程做相反的事情:

   WaitHandle.SignalAndWait (wh2, wh1);

    WaitHandle.WaitAny等待一组等待句柄任意一个发出信号,WaitHandle.WaitAll等待所有给定的句柄发出信号。与票据旋转门的例子类似,这些方法可能同时地等待所有的旋转门——通过在第一个打开的时候(WaitAny情况下),或者等待直到它们所有的都打开(WaitAll情况下)。

   WaitAll 实际上是不确定的值,因为这与单元模式线程——从COM体系遗留下来的问题,有着奇怪的联系。WaitAll 要求调用者是一个多线程单元——刚巧是单元模式最适合——尤其是在 Windows Forms程序中,需要执行任务像与剪切板结合一样庸俗!

    幸运地是,在等待句柄难使用或不适合的时候,.NET framework提供了更先进的信号结构——Monitor.Wait 和 Monitor.Pulse。

你可能感兴趣的:(多线程)