C#中的线程 -- 同步基础(线程状态,同步上下文)

内容预告:

  • 线程入门(线程概念,创建线程)
  • 同步基础(同步本质,线程安全,线程中断,线程状态,同步上下文)
  • 使用线程(后台任务,线程池,读写锁,异步代理,定时器,本地存储)
  • 高级话题(非阻塞线程,扶起和恢复)

线程状态:可以通过ThreadState查看线程的状态。

关于线程的状态,有三层意思:

  •  运行、阻塞、终止。如上图所示
  •  前台、后台(ThreadState.Background)
  •  暂停、申请暂停(ThreadState.SuspendRequested 和 ThreadState.Suspended)

所以线程的状态可以是这三层中各占其一,比如:SuspendRequested, Background, WaitSleepJoin

 

等待句柄:

Win32 API有很多线程同步的结构,在.NET里是通过EventWaitHandle, Mutex 和Semaphore暴露的。这些类都是WaitHandle的派生类。通过命名线程,可以跨进程的工作,而不是只在线程内工作。

EventWaitHandle有两个子类,AutoResetEvent 和ManualResetEvent,两者的不同在于构造函数的参数不同。

AutoResetEvent 就像一个门禁,刷一次门卡进一个人。Auto的意思是当有刷卡的时候自动开门,人进去以后自动关门。可以通过调用WaitOne函数让一个线程在门口等,可以通过Set函数插入门卡。如果有很多线程在门外等,就排成一个队列。WaitOne可以接收一个超时参数,如果时间结束会返回一个false。

复制代码
class BasicWaitHandle {
static EventWaitHandle wh = new AutoResetEvent (false);
static void Main() {
new Thread (Waiter).Start();
Thread.Sleep (1000); // Wait for some time...
wh.Set(); // OK - wake it up
}
static void Waiter() {
Console.WriteLine ("Waiting...");
wh.WaitOne(); // Wait for notification
Console.WriteLine ("Notified");
}
}
复制代码

输出:Waiting... (pause) Notified.

创建一个跨进程的EventWaitHandle

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

生产者、消费者模型:

另一个常见的场景是:一个队列中有一个后台工作进程任务。这个队列叫做生产者/消费者队列,生产者将任务加入队,消费者将任务移出队。
生产者消费者队列可以扩展的,可以创建多个消费者,每个消费者都在一个单独的线程上服务于同一个队列。下面的例子中,单个AutoResetEvent用来当任务运行完成时给工作线程发信号:

复制代码
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!
复制代码

注意:我们在这个例子中当ProducerConsumerQueue析构时显式地关闭了等待句柄,因为我们可以在程序的生命周期里创建和销毁很多这个类的实例。

ManualResetEvent:是AutoResetEvent的变种,区别在于在线程在WaitOne后被允许通过门禁后不能自动重置。要手动Set开门,Reset关门。

Mutex:和lock的功能一样,只是可以跨进程。一个常用的功能是保证程序同时只有一个实例在运行:

复制代码
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没有宿主,任何线程都可以释放一个信号量,在Mutex和lock上只有获得资源的线程才能释放信号量。下面的例子里,10个线程执行一个带Sleep的循环语句,一个信号量确保不超过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 and SignalAndWait

除了Set和WaitOne之外,WaitHandle类还有几个静态函数来解决复杂的同步场景,WaitAny, WaitAll 和 SignalAndWait 函数有助于等待多个句柄(可以是多个类型的)。SignalAndWait也许最有用,当在另一个WaitHandle调用Set时,它在WaitHandle上调用WaitOne,这是一个原子操作。可以用在一对EventWaitHandles上来建立两个线程,AutoResetEvent和ManualResetEvent都有这个麻烦,第一个线程:

WaitHandle.SignalAndWait (wh1, wh2);

第二个线程:

WaitHandle.SignalAndWait (wh1, wh2);

WaitHandle.WaitAny等待每一个等待句柄数组中的句柄。WaitHandle.WaitAll等待所有给定的句柄。用门禁的例子来比喻的话,这些函数就像在门禁前排队。


同步上下文:

相比手动加锁,我们也可以通过代码自动加锁,可以从ContextBoundObject类派生然后添加Synchronization特性,CLR就会自动加锁:

复制代码
using System;
using System.Threading;
using System.Runtime.Remoting.Contexts;
[Synchronization]
public class AutoLock : ContextBoundObject {
public void Demo() {
Console.Write ("Start...");
Thread.Sleep (1000); // We can't be preempted here
Console.WriteLine ("end"); // thanks to automatic locking!
}
}
public class Test {
public static void Main() {
AutoLock safeInstance = new AutoLock();
new Thread (safeInstance.Demo).Start(); // Call the Demo
new Thread (safeInstance.Demo).Start(); // method 3 times
safeInstance.Demo(); // concurrently.
}
}
复制代码

输出:

Start... end
Start... end
Start... end

CLR保证同时只有一个线程执行safeInstance代码,它通过创建一个同步对象,锁定safeInstance的所有函数和字段,这个safeInstance对象叫做同步上下文。

它是如何运转的?一个ContextBoundObject可以认为是一个远程对象,意思是所有函数调用都被拦截,要使拦截生效,我们通过AutoLock来看,CLR实际上返回了一个代理,一个和AutoLock对象具有相同方法和属性的对象,做为一个中介,通过中介发生自动锁定。

但是自动锁定不能限制住静态对象,以及ContextBoundObject的派生类(比如一个Windows Form)。

复制代码
[Synchronization]
public class AutoLock : ContextBoundObject {
public void Demo() {
Console.Write ("Start...");
Thread.Sleep (1000);
Console.WriteLine ("end");
}
public void Test() {
new Thread (Demo).Start();
new Thread (Demo).Start();
new Thread (Demo).Start();
Console.ReadLine();
}
public static void Main() {
new AutoLock().Test();
}
}
复制代码

你可能感兴趣的:(.NET与C#基础)