14.并发与异步 - 1.线程处理Thread -《果壳中的c#》
2017-07-24 22:46
477 查看
14.2.1 创建一个线程
实例化一个Thread对象,然后调用它的Start方法,就可以创建和启动一个新的线程。最简单的
Thread构造方法是接受一个
ThreadStart代理:一个无参方法,表示执行开始位置。
//System.Threading.ThreadStart 委托,它表示此线程开始执行时要调用的方法 public Thread(ThreadStart start);
示例:
static void Main(string[] args) { Thread t = new Thread(WriteY); //创建一个新线程 t.Start(); //启动线程 WriteY //同时,主线程也会执行。 for (int i = 0; i < 1000; i++) Console.Write("x"); Console.Read(); } static void WriteY() { for (int i = 0; i < 1000; i++) Console.Write("y"); }
线程启动后,
IsAlive属性变为
True,直到线程停止。
当
Thread的构造函数接收的代理执行完毕,线程会停止。
停止后,线程无法再启动。
每个线程都有一个
Name属性,可用于调试。只能设置一次,修改线程名称会抛出异常。
静态属性
Thread.CurrentThread可以返回当前执行的线程:
Console.Write(Thread.CurrentThread.Name);
14.2.2 联合与休眠
等待另一个线程结束时,可以调用另一个现成的Join方法:
static void Main(string[] args) { Thread t = new Thread(Go); t.Start(); t.Join(); Console.WriteLine("线程 t 已经结束"); Console.Read(); } static void Go() { for (int i = 0; i < 1000; i++) Console.Write("y"); }
打印1000次“y”,然后再接着打印“线程 t 已经结束”。调用
Join时,可以指定一个超时时间。然后,它会在线程结束时返回true,或者超时时返回false。
Thread.Sleep(TimeSpan.FromHours(1));//休眠1小时
调用Thread.Sleep(0),会马上放弃线程当前时间片,自动将CPU交给其他线程。Thread.Yield()方法也有相同的效果,但是它只会将资源交给同一处理器上运行的线程。
14.2.3 阻塞
线程阻塞是指线程由于特定原因暂停执行,如Sleeping或执行
Join后等待另一个线程停止。阻塞的线程会立刻交出(yield)它的处理器时间片,然后从这时开始不再消耗处理器时间,直至阻塞条件结束。使用线程的
ThreadState属性,可以测试线程的阻塞状态:
1.I/O密集与计算密集
如果一个操作将大部分时间用于等待一个条件的发生,那么就称为I/O密集(I/O-bound)操作。相反,如果一个操作将大部分时间用于执行CPU秘籍操作,那么就称为计算密集(compute-bound)操作。
2.阻塞与自旋
I/O密集操作可以以两种方式执行:同步等待当前线程的操作完成(如Console.ReadLine、Thread.Sleep或Thread.Join),或者异步执行,然后在将来操作完成时触发一个回调函数。
异步等待的I/O密集操作会将大部分时间花费在线程阻塞上。它们可能在一个定期循环中自旋:
while(DateTime < nextStartTime) Thread.Sleep(100);
14.2.4 本地状态与共享状态
CLR会给每一个线程分配独立的内存堆,从而保证本地变量的隔离。下例定义一个方法,其中包含一个局部(本地)变量,然后同时在主线程和新创建的线程上调用这个方法:static void Main(string[] args) { new Thread(Go).Start(); //在 新线程 上调用GO Go(); //在 主线程 调用GO Console.Read(); } static void Go() { for (int cycles = 0; cycles < 5; cycles++) { Console.Write('?'); } }
每一个线程的内存堆会创建cycles变量副本,所以输出结果为10个问号。
如果线程拥有一个对象实例的通用引用,那么这些线程就共享相同的数据:
class ThreadTest { bool _done; static void Main(string[] args) { ThreadTest tt = new ThreadTest(); new Thread(tt.GO).Start(); tt.GO(); Console.Read(); } void GO() { if (!_done) { _done = true; Console.WriteLine("Done"); } } }
因为这两个线程都在同一个
ThreadTest实例上调用GO(),所以它们共享_done域。因此,“Done”只会打印一次,而不会打印两次。
其它方式:
编译器会将Lambda表达式或匿名代理捕获的局部变量转为域,所以它们也可以共享。
静态域是在线程之间共享数据的另一种方法。
14.2.5 锁与线程安全
class ThreadSafe { static bool _done; static readonly object _locker = new object(); static void Main(string[] args) { new Thread(Go).Start(); Go(); Console.Read(); } static void Go() { lock (_locker) { if (_done) { Console.WriteLine("Done"); _done = true; } } } }
结果:(什么都没有)
当两个线程同时争夺一个锁时(它可以是任意引用类型的对象,这里是_locker),其中一个线程会等待(或阻塞),直到锁释放。这个例子保证一次只会一个线程进入它的代码块,因此“Done”只会打印一次。
在复杂多线程环境中,采用这种方式来保护代码就是具有线程安全性。
锁并不是解决线程安全的万能法宝 —— 人们很容易在访问域时忘记锁,而且锁本身也存在一些问题(如死锁)。
14.2.6 传递数据到线程
给线程启动方法传递一些参数。最简单是使用Lambda表达式,然后用指定参数调用这个方法:static void Main() { Thread t = new Thread(() => Print("Hello from t!")); t.Start(); } static void Print(string message) { Console.WriteLine(message); }
这种方法可以给这个方法传递任意数量的参数。甚至可以将整个实现过程封装在一个多语句Lambda表达式中
new Thread (() => { Console.WriteLine ("I'm running on another thread!"); Console.WriteLine ("This is so easy!"); }).Start()
Lambda表达式与捕获的变量
在线程启动之后,一定要注意小心修改捕捉的变量。for (int i = 0; i < 10; i++) new Thread(() => Console.Write(i)).Start();
这段代码输出结果不确定,下面是一种常见的:
问题是,在整个循环过程中,变量i都指向同一块内存地址。因此,每次线程调用Console.Write处理变量时,这个变量的值可能发生了变化!解决方法是使用临时变量:
for (int i = 0; i < 10; i++) { int temp = i; new Thread (() => Console.Write (temp)).Start(); }
变量temp是每个循环过程的局部变量,因此,每一个线程都会获取完全不同的内存地址。
String text = "t1"; Thread t1 = new Thread(()=>Console.WriteLine(text)); text = "t2"; Thread t2 = new Thread(() => Console.WriteLine(text)); t1.Start();t2.Start();
结果:t2 t2
由于两个Lambda表达式补货同一个text变量,所以t2会打印两次。
14.2.7 异常处理
线程创建时任何生效的try/catch/finally语句块在线程执行后都与线程无关。static void Main(string[] args) { try { new Thread(Go).Start(); } catch (Exception) { //代码永远不会运行到这里 Console.WriteLine("exception"); } } static void Go() { throw null; } //抛出异常
解决方法 是将异常处理移动到
Go方法内:
static void Main(string[] args) { new Thread(Go).Start(); } static void Go() { //throw null; try { throw null;//下面补货到异常 NullReferemceException } catch (Exception ex) { //通常是记录异常,并且/或者发信号给另一个线程,告诉它们捕捉到了异常 Console.WriteLine("exception"); } }
结果: exception
集中式异常处理
WPF、Metro和Windows窗体应用程序都支持订阅全局异常处理事件,分别是Application.DispatcherUnhandledException和
Application.ThreadException。如果通过消息循环调用的程序中出现未处理异常(相当于Application激活时运行在主线程上的所有代码)。就会触发这些异常。这非常适合记录日志和报告缺陷(但是它不会触发非UI线程的未处理异常)。处理这些事件可防止程序意外关闭,但必须选择重启应用。
AppDomain.CurrentDomain.UnhandledException可以触发任意线程的任意未处理异常,CLR 2.0开始会在事件执行完后关闭应用程序。在程序配置文件中添加下面代码,可防止应用程序关闭:
<configuration> <runtime> <legacyUnhandledExceptionPolicy enabled="1" /> </runtime> </configuration>
14.2.8 前台线程与后台线程
默认显示创建的线程为前台线程,使用线程的IsBackground属性。
前台线程:只有所有的前台线程都关闭才能完成程序关闭。(主线程一直是前台线程)
后台线程:只要所有的前台线程都结束,后台线程自动结束(CLR会强制结束所有仍运行的后台线程,却不会抛出异常)。
14.2.9 线程优先级
线程的Priority属性可以确定它与其他激活线程的相对执行时间长短,public enum ThreadPriority { Lowest = 0, BelowNormal = 1, Normal = 2, AboveNormal = 3, Highest = 4, }
如果希望一个线程拥有比其他进程的线程更高的优先级,还必须使用
System.Diagnostics的
Process类,提高进程本身优先级:
worker.Priority = ThreadPriority.Highest; //进程 using (Process p = Process.GetCurrentProcess()) { p.PriorityClass = ProcessPriorityClass.High; }
14.2.10 发送信号
有时候,一个线程需要等待其他线程的通知,这就是发送信号(signaling)。最简单的发送信号结构是ManualResetEvent。在一个
ManualResetEvent上调用
WaitOne,可以阻塞当前线程,使之一直等待另一个线程通过调用
Set“打开”信号。
下面例子启动一个线程,等待
ManualResetEvent到达,它会保持阻塞2秒钟,直至主线程发送信号:
static void Main(string[] args) { //通知一个或多个正在等待的线程已发生事件,如果为 true,则将初始状态设置为终止 var signal = new ManualResetEvent(false); new Thread(() => { Console.WriteLine("等待 signal.."); signal.WaitOne(); //阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号 signal.Dispose(); //释放由 System.Threading.WaitHandle 类的当前实例使用的所有资源。 Console.WriteLine("开始signal"); }).Start(); Thread.Sleep(2000); signal.Set(); //打开信号 ---(将事件状态设置为终止状态,允许一个或多个等待线程继续) Console.Read(); }
调用
Set后,信号仍然保持打开;调用
Reset,就可以再次将它关闭。
14.2.12 同步上下文
System.ComponentModel命名空间中有一个抽象类
SynchronizationContext。它实现了线程编列的一般化。
WPF、Metro和Windows窗体都定义和实例化了
SynchronizationContext的子类,当运行在UI线程时,它通过静态属性
SynchronizationContext.Current获得。
Framework2.0引入了
BanckgroundWoker类,他使用
SynchronizationContext类简化富客户端应用程序的工作者线程。
BanckgroundWoker增加了相同的
Tasks和异步功能,它也使用
SynchronizationContext。
14.2.13 线程池
无论何时启动一个线程,都需要一定时间(几百毫秒)用于创建新的局部变量堆。线程池(thread pool)预先创建一组可回收线程,因此可以缩短这个过载时间。要实现高效的并行编程和细致的并发性,必须使用线程池。
考虑:
1. 由于不能设置池化线程的Name,因此会增加代码调试难度。
2. 池化线程通常都是后台线程。
3. 池化线程阻塞会影响性能。
1.进入线程池
在池化线程运行代码最简单的方法是使用Task.Run:Task.Run(() => Console.WriteLine("Hello from the thread pool"));
Framework 4.0之前不支持任务,所以改为调用
ThreadPool.QueueUserWorkItem
ThreadPool.QueueUserWorkItem(notUsed => Console.WriteLine("Hello"));
2.线程池整洁性
线程池还有一个功能,既保证计算密集作业的临时过载不会引起CPU超负荷。CLR能将任务进行排序,并且控制任务启动数量,从而避免线程池超负荷。
阻塞是很很麻烦的,因为它会让CLR错误地人为它占用了大量CPU。CLR能检测并补偿(往池中注入更多线程),但这可能使线程池受到后续超负荷的影响。此外,这样会增加延迟,一位内CLR会限制注入新线程的速度,特别是应用程序生命周期的前期。
如果想提高CPU利用率,那么一定要报保证线程池整洁性。
相关文章推荐
- 14.并发与异步 - 3.C#5.0的异步函数 -《果壳中的c#》
- C# 集合-并发处理-锁OR线程 (转载)
- 如何处理线程并发时产生的线程安全问题(Runnable和Thread)
- C# 线程知识--使用ThreadPool执行异步操作
- C#线程执行超时处理与并发线程数控制实例
- 14.并发与异步 - 2.任务Task -《果壳中的c#》
- C#并发处理-锁OR线程安全?
- Android异步任务处理之Thread线程
- C# 使用 Task 替换 ThreadPool ,异步监测所有线程(任务)是否全部执行完毕
- 工作线程WorkThread和异步任务AsyncTask取舍
- C# 创建线程的简单方式:异步委托
- Spring如何处理线程并发
- 在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
- [C# 线程处理系列]专题五:线程同步——事件构造
- 聊聊并发处理和java线程
- 进程 线程 多线程 并发 同步异步
- 并发,同步,异步,阻塞,非阻塞,线程
- android起始页面与Handler(异步线程处理)
- C#线程 在某一时间内,只有N个线程在并发执行,其余都在队列中的实现 收藏
- c#使用多线程并发之Thread