14.2.1 创建一个线程

实例化一个Thread对象,然后调用它的Start方法,就可以创建和启动一个新的线程。最简单的Thread构造方法是接受一个ThreadStart代理:一个无参方法,表示执行开始位置。

  1. //System.Threading.ThreadStart 委托,它表示此线程开始执行时要调用的方法
  2. public Thread(ThreadStart start);

示例:

  1. static void Main(string[] args)
  2. {
  3. Thread t = new Thread(WriteY); //创建一个新线程
  4. t.Start(); //启动线程 WriteY
  5. //同时,主线程也会执行。
  6. for (int i = 0; i < 1000; i++) Console.Write("x");
  7. Console.Read();
  8. }
  9. static void WriteY()
  10. {
  11. for (int i = 0; i < 1000; i++) Console.Write("y");
  12. }

  1. 线程启动后,IsAlive属性变为True,直到线程停止。
  2. Thread的构造函数接收的代理执行完毕,线程会停止。
  3. 停止后,线程无法再启动。

每个线程都有一个Name属性,可用于调试。只能设置一次,修改线程名称会抛出异常。

静态属性Thread.CurrentThread可以返回当前执行的线程:

  1. Console.Write(Thread.CurrentThread.Name);

14.2.2 联合与休眠

等待另一个线程结束时,可以调用另一个现成的Join方法:

  1. static void Main(string[] args)
  2. {
  3. Thread t = new Thread(Go);
  4. t.Start();
  5. t.Join();
  6. Console.WriteLine("线程 t 已经结束");
  7. Console.Read();
  8. }
  9. static void Go() { for (int i = 0; i < 1000; i++) Console.Write("y"); }



打印1000次“y”,然后再接着打印“线程 t 已经结束”。调用Join时,可以指定一个超时时间。然后,它会在线程结束时返回true,或者超时时返回false。

  1. 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密集操作会将大部分时间花费在线程阻塞上。它们可能在一个定期循环中自旋:

  1. while(DateTime < nextStartTime)
  2. Thread.Sleep(100);

14.2.4 本地状态与共享状态

CLR会给每一个线程分配独立的内存堆,从而保证本地变量的隔离。下例定义一个方法,其中包含一个局部(本地)变量,然后同时在主线程和新创建的线程上调用这个方法:

  1. static void Main(string[] args)
  2. {
  3. new Thread(Go).Start(); //在 新线程 上调用GO
  4. Go(); //在 主线程 调用GO
  5. Console.Read();
  6. }
  7. static void Go()
  8. {
  9. for (int cycles = 0; cycles < 5; cycles++)
  10. {
  11. Console.Write('?');
  12. }
  13. }

每一个线程的内存堆会创建cycles变量副本,所以输出结果为10个问号。

如果线程拥有一个对象实例的通用引用,那么这些线程就共享相同的数据:

  1. class ThreadTest
  2. {
  3. bool _done;
  4. static void Main(string[] args)
  5. {
  6. ThreadTest tt = new ThreadTest();
  7. new Thread(tt.GO).Start();
  8. tt.GO();
  9. Console.Read();
  10. }
  11. void GO()
  12. {
  13. if (!_done)
  14. {
  15. _done = true;
  16. Console.WriteLine("Done");
  17. }
  18. }
  19. }

因为这两个线程都在同一个ThreadTest实例上调用GO(),所以它们共享_done域。因此,“Done”只会打印一次,而不会打印两次。

其它方式:

编译器会将Lambda表达式或匿名代理捕获的局部变量转为域,所以它们也可以共享。

静态域是在线程之间共享数据的另一种方法。

14.2.5 锁与线程安全

  1. class ThreadSafe
  2. {
  3. static bool _done;
  4. static readonly object _locker = new object();
  5. static void Main(string[] args)
  6. {
  7. new Thread(Go).Start();
  8. Go();
  9. Console.Read();
  10. }
  11. static void Go()
  12. {
  13. lock (_locker)
  14. {
  15. if (_done)
  16. {
  17. Console.WriteLine("Done");
  18. _done = true;
  19. }
  20. }
  21. }
  22. }

结果:(什么都没有)

当两个线程同时争夺一个锁时(它可以是任意引用类型的对象,这里是_locker),其中一个线程会等待(或阻塞),直到锁释放。这个例子保证一次只会一个线程进入它的代码块,因此“Done”只会打印一次。

在复杂多线程环境中,采用这种方式来保护代码就是具有线程安全性。

锁并不是解决线程安全的万能法宝 —— 人们很容易在访问域时忘记锁,而且锁本身也存在一些问题(如死锁)。

14.2.6 传递数据到线程

给线程启动方法传递一些参数。最简单是使用Lambda表达式,然后用指定参数调用这个方法:

  1. static void Main()
  2. {
  3. Thread t = new Thread(() => Print("Hello from t!"));
  4. t.Start();
  5. }
  6. static void Print(string message) { Console.WriteLine(message); }

这种方法可以给这个方法传递任意数量的参数。甚至可以将整个实现过程封装在一个多语句Lambda表达式中

  1. new Thread (() =>
  2. {
  3. Console.WriteLine ("I'm running on another thread!");
  4. Console.WriteLine ("This is so easy!");
  5. }).Start()

Lambda表达式与捕获的变量

在线程启动之后,一定要注意小心修改捕捉的变量。

  1. for (int i = 0; i < 10; i++)
  2. new Thread(() => Console.Write(i)).Start();

这段代码输出结果不确定,下面是一种常见的:



问题是,在整个循环过程中,变量i都指向同一块内存地址。因此,每次线程调用Console.Write处理变量时,这个变量的值可能发生了变化!解决方法是使用临时变量:

  1. for (int i = 0; i < 10; i++)
  2. {
  3. int temp = i;
  4. new Thread (() => Console.Write (temp)).Start();
  5. }

变量temp是每个循环过程的局部变量,因此,每一个线程都会获取完全不同的内存地址。

  1. String text = "t1";
  2. Thread t1 = new Thread(()=>Console.WriteLine(text));
  3. text = "t2";
  4. Thread t2 = new Thread(() => Console.WriteLine(text));
  5. t1.Start();t2.Start();

结果:t2 t2

由于两个Lambda表达式补货同一个text变量,所以t2会打印两次。

14.2.7 异常处理

线程创建时任何生效的try/catch/finally语句块在线程执行后都与线程无关

  1. static void Main(string[] args)
  2. {
  3. try
  4. {
  5. new Thread(Go).Start();
  6. }
  7. catch (Exception)
  8. {
  9. //代码永远不会运行到这里
  10. Console.WriteLine("exception");
  11. }
  12. }
  13. static void Go() { throw null; } //抛出异常

解决方法 是将异常处理移动到Go方法内:

  1. static void Main(string[] args)
  2. {
  3. new Thread(Go).Start();
  4. }
  5. static void Go()
  6. {
  7. //throw null;
  8. try
  9. {
  10. throw null;//下面补货到异常 NullReferemceException
  11. }
  12. catch (Exception ex)
  13. {
  14. //通常是记录异常,并且/或者发信号给另一个线程,告诉它们捕捉到了异常
  15. Console.WriteLine("exception");
  16. }
  17. }

结果: exception

集中式异常处理

WPF、Metro和Windows窗体应用程序都支持订阅全局异常处理事件,分别是Application.DispatcherUnhandledExceptionApplication.ThreadException。如果通过消息循环调用的程序中出现未处理异常(相当于Application激活时运行在主线程上的所有代码)。就会触发这些异常。这非常适合记录日志和报告缺陷(但是它不会触发非UI线程的未处理异常)。处理这些事件可防止程序意外关闭,但必须选择重启应用。

AppDomain.CurrentDomain.UnhandledException可以触发任意线程的任意未处理异常,CLR 2.0开始会在事件执行完后关闭应用程序。在程序配置文件中添加下面代码,可防止应用程序关闭:

  1. <configuration>
  2. <runtime>
  3. <legacyUnhandledExceptionPolicy enabled="1" />
  4. </runtime>
  5. </configuration>

14.2.8 前台线程与后台线程

默认显示创建的线程为前台线程,使用线程的IsBackground属性。

前台线程:只有所有的前台线程都关闭才能完成程序关闭。(主线程一直是前台线程)

后台线程:只要所有的前台线程都结束,后台线程自动结束(CLR会强制结束所有仍运行的后台线程,却不会抛出异常)。

14.2.9 线程优先级

线程的Priority属性可以确定它与其他激活线程的相对执行时间长短,

  1. public enum ThreadPriority
  2. {
  3. Lowest = 0,
  4. BelowNormal = 1,
  5. Normal = 2,
  6. AboveNormal = 3,
  7. Highest = 4,
  8. }

如果希望一个线程拥有比其他进程的线程更高的优先级,还必须使用System.DiagnosticsProcess类,提高进程本身优先级:

  1. worker.Priority = ThreadPriority.Highest;
  2. //进程
  3. using (Process p = Process.GetCurrentProcess())
  4. {
  5. p.PriorityClass = ProcessPriorityClass.High;
  6. }

14.2.10 发送信号

有时候,一个线程需要等待其他线程的通知,这就是发送信号(signaling)。最简单的发送信号结构是ManualResetEvent。在一个ManualResetEvent上调用WaitOne,可以阻塞当前线程,使之一直等待另一个线程通过调用Set“打开”信号。

下面例子启动一个线程,等待ManualResetEvent到达,它会保持阻塞2秒钟,直至主线程发送信号:

  1. static void Main(string[] args)
  2. {
  3. //通知一个或多个正在等待的线程已发生事件,如果为 true,则将初始状态设置为终止
  4. var signal = new ManualResetEvent(false);
  5. new Thread(() =>
  6. {
  7. Console.WriteLine("等待 signal..");
  8. signal.WaitOne(); //阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号
  9. signal.Dispose(); //释放由 System.Threading.WaitHandle 类的当前实例使用的所有资源。
  10. Console.WriteLine("开始signal");
  11. }).Start();
  12. Thread.Sleep(2000);
  13. signal.Set(); //打开信号 ---(将事件状态设置为终止状态,允许一个或多个等待线程继续)
  14. Console.Read();
  15. }

调用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:

  1. Task.Run(() => Console.WriteLine("Hello from the thread pool"));

Framework 4.0之前不支持任务,所以改为调用ThreadPool.QueueUserWorkItem

  1. ThreadPool.QueueUserWorkItem(notUsed => Console.WriteLine("Hello"));

2.线程池整洁性

线程池还有一个功能,既保证计算密集作业的临时过载不会引起CPU超负荷。

CLR能将任务进行排序,并且控制任务启动数量,从而避免线程池超负荷。

阻塞是很很麻烦的,因为它会让CLR错误地人为它占用了大量CPU。CLR能检测并补偿(往池中注入更多线程),但这可能使线程池受到后续超负荷的影响。此外,这样会增加延迟,一位内CLR会限制注入新线程的速度,特别是应用程序生命周期的前期。

如果想提高CPU利用率,那么一定要报保证线程池整洁性。

14.并发与异步 - 1.线程处理Thread -《果壳中的c#》的更多相关文章

  1. 14.并发与异步 - 2.任务Task -《果壳中的c#》

    线程是创建并发的底层工具,因此具有一定的局限性. 没有简单的方法可以从联合(Join)线程得到"返回值".因此必须创建一些共享域.当抛出一个异常时,捕捉和处理异常也是麻烦的. 线程 ...

  2. 14.并发与异步 - 3.C#5.0的异步函数 -《果壳中的c#》

    14.5.2 编写异步函数 private static readonly Stopwatch Watch = new Stopwatch(); static void Main(string[] a ...

  3. java并发编程学习: 守护线程(Daemon Thread)

    在正式理解这个概念前,先把 守护线程 与 守护进程 这二个极其相似的说法区分开,守护进程通常是为了防止某些应用因各种意外原因退出,而在后台独立运行的系统服务或应用程序. 比如:我们开发了一个邮件发送程 ...

  4. 【转】Struts2的线程安全 和Struts2中的设计模式----ThreadLocal模式

    [转]Struts2的线程安全 和Struts2中的设计模式----ThreadLocal模式 博客分类: 企业应用面临的问题 java并发编程 Struts2的线程安全ThreadLocal模式St ...

  5. 14.6.8 Configuring the InnoDB Master Thread IO Rate 配置InnoDB 主线程IO 速率:

    14.6.8 Configuring the InnoDB Master Thread IO Rate 配置InnoDB 主线程IO 速率: 主线程 在InnoDB 是一个线程 执行各种任务在后台. ...

  6. Python并发编程系列之常用概念剖析:并行 串行 并发 同步 异步 阻塞 非阻塞 进程 线程 协程

    1 引言 并发.并行.串行.同步.异步.阻塞.非阻塞.进程.线程.协程是并发编程中的常见概念,相似却也有却不尽相同,令人头痛,这一篇博文中我们来区分一下这些概念. 2 并发与并行 在解释并发与并行之前 ...

  7. JAVA并发编程——守护线程(Daemon Thread)

    在Java中有两类线程:用户线程 (User Thread).守护线程 (Daemon Thread). 所谓守护 线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称 ...

  8. Java并发(三)线程池原理

    Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池.在开发过程中,合理地使用线程池能够带来3个好处. 1. 降低资源消耗.通过重复利用已创建的线程降低线程 ...

  9. 并发编程-concurrent指南-线程池ExecutorService的实例

    1.new Thread的弊端 执行一个异步任务你还只是如下new Thread吗? new Thread(new Runnable() { @Override public void run() { ...

随机推荐

  1. JAVA多线程-实现同步

    一.什么是线程安全问题 当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题.但是做读操作是不会发生数据冲突问题. 二.如何解决线程安全问题 1)如何 ...

  2. linux添加超级管理员用户,修改,删除用户

    useradd一个用户后,去修改/etc/passwd文件中的这个用户这一行,把其中的uid改为0,gid改为0(其中****代表一个用户名)这样****就具有root权限了 如:root2:x:0: ...

  3. python基础4 列表和元组

    一. 列表列表:python基础数据类型之一:其他语言中也有列表的概念,js 数组,可索引,可切片,可加步长li = ['hello', 100, True, [1, 2, 3], {'name':' ...

  4. AJAX初识(原生JS版AJAX和Jquery版AJAX)

    一.什么是JSON 1.介绍 JSON独立于语言,是一种与语言无关的数据格式. JSON指的是JavaScript对象表示法(JavaScript Object Notation) JSON是轻量级的 ...

  5. Android N和O中使用adb shell dpm set-device-owner 'com.android.cts.verifier/com.android.cts.verifier.managedprovisioning.DeviceAdminTestReceiver' setup Device Owner失败

    PC端出现如下log: D:\workspace\AndroidO\CTS\CTS_Verifier>adb shell dpm set-device-owner 'com.android.ct ...

  6. Go语言公开或未公开的标识符

    Go语言公开或未公开的标识符的基本概念 Go语言支持从包里公开或者隐藏标志符,通过这个特性,可以让用户按照自己的规则控制标识符的可见性. Go语言中的可见性,是通过声明类型的大小写来进行区别的. 例如 ...

  7. FWT快速沃尔什变换学习笔记

    FWT快速沃尔什变换学习笔记 1.FWT用来干啥啊 回忆一下多项式的卷积\(C_k=\sum_{i+j=k}A_i*B_j\) 我们可以用\(FFT\)来做. 甚至在一些特殊情况下,我们\(C_k=\ ...

  8. 题解 CF540D 【Bad Luck Island】

    既然没有大佬写题解那本蒟蒻就厚颜无耻地写(水)一(经)下(验)吧 题目要求算出个种人单独留下的存活率 因为n,m,p的范围极小, 那么就可以方便地设3位dp状态dp[i][j][k]表示剩余i个石头, ...

  9. hdu 2829 Lawrence(四边形不等式优化dp)

    T. E. Lawrence was a controversial figure during World War I. He was a British officer who served in ...

  10. django-url的分发

    1)url的分发: 1,首先在全局的url里面的路径中写好,你要分发的路径名. 2,并且在你要分发的路径下,创好新的url文件. 在分发的路径名里面,把全局url里面的代码,复制过来 3,最后在浏览器 ...