C# 多线程总结
1 创建线程
1.1 异步委托方式
使用异步委托创建的线程,都是由.Net线程池维护的。
线程池中的线程总是后台线程。
为了方便起见,接下来使用的共通委托方法如下1:
static int TakesAWhile(int data, int ms) { Console.WriteLine("TakesAWhile started"); Thread.Sleep(ms); Console.WriteLine("TakesAWhile completed"); return ++data; }
1.1.1 IAsyncResult.IsCompleted
根据IAsyncResult.IsCompleted判断异步委托是否执行完成。
EndInvoke获取返回值。
TakesAWhileDelegate dl = TakesAWhile; IAsyncResult ar = dl.BeginInvoke(1, 3000, null, null); while (!ar.IsCompleted) { Console.Write("."); Thread.Sleep(50); } int result = dl.EndInvoke(ar); Console.WriteLine("result: {0}", result);
1.1.2 IAsyncResult.AsyncWaitHandle
使用WaitHandle,可指定异步调用的超时时间进行后续处理。
TakesAWhileDelegate dl = TakesAWhile; IAsyncResult ar = dl.BeginInvoke(1, 3000, null, null); if (!ar.AsyncWaitHandle.WaitOne(200, false)) { Console.WriteLine("Thread not invoked."); } if (ar.AsyncWaitHandle.WaitOne(3000, false)) { int result = dl.EndInvoke(ar); Console.WriteLine("result: {0}", result); }
1.1.3 AsyncCallBack
通过传入回调函数,进行后续处理
- 分支一:单独定义回调方法
static void Main(string[] args) { TakesAWhileDelegate dl = TakesAWhile; dl.BeginInvoke(1, 3000, TakesAWhileCompleted, dl); //必须程序主线程一直存在才会执行回调方法,所以使用了如下for循环(说明了异步委托所创建的线程是一个后台线程) for (int i = 0; i < 100; i++ ) { Console.Write("."); Thread.Sleep(50); } } //定义回调方法 static void TakesAWhileCompleted(IAsyncResult ar) { if (ar == null) { throw new ArgumentNullException("ar"); } TakesAWhileDelegate dl = ar.AsyncState as TakesAWhileDelegate; Trace.Assert(dl != null, "Invalid object type"); int result = dl.EndInvoke(ar); Console.WriteLine("result: {0}", result); }
- 分支二:使用lambada表达式
TakesAWhileDelegate dl = TakesAWhile; dl.BeginInvoke(1, 3000, //这是个回调函数,使用lambada表达式的话,代码不够清晰。 ar => { int result = dl.EndInvoke(ar);//lambda表达式可使用该作用域外部的变量dl Console.WriteLine("result: {0}", result); }, null); //必须程序主线程一直存在才会执行回调方法 for (int i = 0; i < 100; i++) { Console.Write("."); Thread.Sleep(50); }
1.2 Thread类
1.2.1 无参数线程方法
var t1 = new Thread(() => Console.WriteLine("running in a thread, id {0}", Thread.CurrentThread.ManagedThreadId)); t1.Start(); Console.WriteLine("This is a main thread, id {0}", Thread.CurrentThread.ManagedThreadId);
1.2.2 有参数线程方法
public struct Data { public string Message; } static int TakesAWhile(int data, int ms) { var d = new Data { Message = "Info" }; var t2 = new Thread((object obj) => { Data data = (Data)obj; Console.WriteLine("running in a thread, id {0}, Data {1}", Thread.CurrentThread.ManagedThreadId, data.Message); }); t2.Start(d); Console.WriteLine("This is a main thread, id {0}", Thread.CurrentThread.ManagedThreadId); }
1.2.3 后台线程
Thread类默认创建的是前台线程,设定IsBackground属性可转为后台线程
var t1 = new Thread( () => { Console.WriteLine("branch thread Start, id {0}", Thread.CurrentThread.ManagedThreadId); Thread.Sleep(3000); Console.WriteLine("branch thread End"); }) { Name = "NewBKThread", IsBackground = true }; t1.Start(); Thread.Sleep(50);//为了使后台线程的情况下,能打出branch thread Start, id Console.WriteLine("This is a main thread, id {0}", Thread.CurrentThread.ManagedThreadId);
- IsBackground = true 结果:
branch thread Start, id 3
This is a main thread, id 1
- IsBackground = false 结果:
branch thread Start, id 3
This is a main thread, id 1
branch thread End
1.2.4 关于线程优先级
可以通过Thread.Priority属性调整线程的 基本 优先级。实际线程调度器会动态调整优先级
频繁使用CPU的线程的优先级会动态调低,等待资源(等待磁盘IO完成等)的线程会动态调高优先级。
以便在下次等待结束时获得CPU资源。2
1.2.5 线程状态
通过属性Thread.ThreadState获取当前线程状态
运行Thread.Start()后,状态为Unstarted。
系统线程调度器选择了运行该线程后,状态为Running。
调用Thread.Sleep(),状态为WaitSleepJoin。
停止另一个线程,调用Thread.Abort()。接到中止命令的线程中会抛出ThreadAbortException。3
涉及的状态有AbortRequested、Aborted。
继续停止的线程,调用Thread.ResetAbort()。线程将会在抛出ThreadAbortException后的语句后继续进行。
等待线程的结束,调用ThreadInstance.Join()。
该调用会停止 当前 线程,当前线程状态设为WaitSleepJoin。
等待加入的线程处理完成,再继续当前线程的处理。
1.3 线程池
超出最大线程数时,QueueUserWorkItem会等待获取线程资源时再调用。
static void Main(string[] args) { ThreadPool.SetMinThreads(3, 3);//创建线程池时启动的最小线程数 ThreadPool.SetMaxThreads(10, 10);//最大线程数 for (int i = 0; i < 5; i++ ) { ThreadPool.QueueUserWorkItem(JobForAThread); } Thread.Sleep(3000);//由于是后台线程,需要使主线程等一会,否则程序直接退出 } static void JobForAThread(object state) { for (int i = 0; i < 3; i++) { Console.WriteLine("loop {0}, running inside pooled thread {1}", i, Thread.CurrentThread.ManagedThreadId); } }
使用线程池的限制:
- 其中的所有线程只能是后台线程。
- 无法设置线程的优先级或名称。
- 关键点 适用于耗时较短的任务。长期运行的线程,应使用Thread类创建。
2 同步问题
2.1 lock关键字
只能锁定引用类型,锁定值类型等于锁定了一个副本,没有意义,编译器也不允许你这么做。
使用锁定需要时间,并不总是必须。可以创建类的两个版本,一个同步版本,一个异步版本。
2.1.1 将实例成员设为线程安全的
lock(this) { //一次只有一个线程能访问相同实例的该语句块 }
因为该实例对象也可用于外部访问,这样做会导致外部访问时也得等待该同步语句块执行完成。正确的做法:
private object syncRoot = new object(); public void DoSomething() { lock (object) { //Do something } } private static object syncRoot = new object();//可用于锁定类静态成员
2.1.2 lock关键字由编译器解析为Monitor类
lock (obj) { };
等价于:
Monitor.Enter(obj); try { } finally { Monitor.Exit(obj); }
与lock关键字的区别:
- 可添加一个等待解锁的超时时间,使用TryEnter传递超时值。
bool lockTaken = Monitor.TryEnter(obj, 500); if (lockTaken) { try { } finally { Monitor.Exit(obj); } } else { //didn't get the lock, do something else }
2.1.3 更快速的Interlocked类
仅用于简单的针对变量赋值的同步问题
lock(this) { if (someState == null) { someState = newState; } }
等价于(可用于单件模式的GetInstance):
Interlocked.CompareExchange<SomeState>(ref someState, newState, null);//第一个参数和第三个参数比较,如果相等,替换为第二个参数的值
public int State { get { lock (this) { return ++state; } } }
等价于:
public int State { get { return Interlocked.Increment(ref state); } }
2.2 WaitHandle
WaitHandle是一个抽象基类。用于等待某个信号量。
Mutex、EventWaitHandle、Semaphore类都从WaitHandle派生。
2.3 Mutex类
提供进程之间的同步访问。创建一个进程之间能共享的以字符串命名的互斥锁。
构造函数的一种形式如下:
bool created; Mutex mutex = new Mutex(false, "IFFileMutex", out created);
其中,第一个参数定义了该互斥体的所有权是否应属于调用线程。
第二个参数是互斥体名字,操作系统能识别该字符串,以此实现各进程之间的同步。
第三个参数,如果系统中已存在该命名的互斥体返回false,否则返回true。
Mutex mutex = Mutex.OpenExisting("IFFileMutex");//打开系统中已存在的互斥体 if(mutex.WaitOne(500))//500为等待超时时间 { try { //synchronized region } finally { mutex.ReleaseMutex(); } }
2.4 Semaphore类
信号量可以同时由多个线程使用,是计数的互斥体。一般用于受数量限制的访问资源(如DB连接资源)。
2.5 Event类
系统级的资源同步方式,比之Mutex,多了个Reset方法,
重置nonsignaled的状态(等同于互斥体的锁定状态),释放所有等待的线程。
Set方法:将事件设为signaled状态,使其他等待的线程得以继续,类似锁的Release方法。
Waitone方法:等待事件被设为signaled状态。
Reset方法:将事件设为nonsignaled状态,并且阻塞所有等待的线程。
2.5.1 AutoResetEvent
Reset方法会在某一线程Waitone成功后,自动重置为nonsignaled。
达到的效果:一次只能一个线程继续处理。
2.5.2 ManualResetEvent
需手动调用Reset方法重置为nonsignaled。
达到的效果:多个线程都能继续进行处理。
2.6 ReaderWriterLockSlim类(.Net 3.5引入)
如果没有Writer锁定资源,就允许多个Reader访问资源,但只能有一个
Writer锁定该资源(所有访问中的Reader都必须先释放锁)。
比之.Net 1.0版本 ReaderWriterLock类,重新设计为防止死锁,提供更好的性能。
- EnterReadLock 进入锁定,另一个方法TryEnterReadLock允许指定一个超时时间。ExitReadLock释放锁定
- EnterUpgradableReadLock 用于读取锁定需要改为写入锁定的情况。
- EnterWriteLock 获得多资源的写入锁定。仅一个线程能获取锁定,在这之前还必须释放所有的读取锁定。
3 Timer类
.Net提供了几个Timer类,比较如下:
命名空间 | 说明 |
---|---|
System.Threading | 提供了Timer的核心功能,在构造函数中传入回调的委托。 |
System.Timer | 继承Component,可在设计界面拖入,提供了基于事件的机制(非委托)。 |
System.Windows.Forms | 为单线程环境设计的(创建和回调在同一个线程中执行),执行回调方法时UI会假死,不宜执行耗时较长的代码。该Timer时间精度55ms。 |
System.Web.UI | 是一个AJAX扩展,可以用于Web页面 |
4 总结
类 | 目的 | 参考开销4 | 是否跨进程? |
---|---|---|---|
lock(Monitor) | 保证单个进程内只有一个线程能够获取同步资源 | 20ns | No |
Mutex | 保证只有一个线程能够获取同步资源 | 1000ns | Yes |
Semaphore | 可指定可获取同步资源的线程数 | 1000ns | Yes |
ReaderWriterLock | 允许多个Reader访问同步资源 | 100ns | No |
AutoResetEvent | 当信号被设为signaled状态时,允许单个线程进入同步资源块 | 1000ns | Yes |
ManualResetEvent | 当信号被设为signaled状态时,允许所有等待线程进入同步资源块 | 1000ns | Yes |
ReaderWriterLockSlim | 可锁定多个Reader访问资源以及单个Writer修改资源 | 40ns | No |
- 注:一些Slim类(如ManualResetEventSlim),比之旧版本,通常拥有更好的性能。参考 MSDN。
几条规则:
- 尽量使同步要求最低,尝试避免共享状态。
- 类的静态成员应是线程安全的。
- 实例成员不需要是线程安全的。为了最佳性能,最好在类的外部处理同步问题。
完整代码示例:MultiThreadDemo.rar
5 推荐阅读
Footnotes:
1 例子参照《C# 高级编程(第7版)》
2 给线程指定较高的基本优先级时,需注意。这有可能会降低其他线程的运行概率。
3 可以捕捉该异常完成线程的资源清理任务。
4 该时间测自CPU Intel Core i7 860的环境,参考Threading in C#