关于线程的知识点实际上是不少的,好比多线程编程、线程上下文、异步编程、线程同步构造、GUI的跨线程访问等等,本文只是从常见面试题的角度(也是开发过程当中经常使用)去深刻浅出线程相关的知识。若是想要系统的学习多线程,没有捷径的,也不要偷懒,仍是去看专业书籍的比较好。html
1. 描述线程与进程的区别?面试
2. 为何GUI不支持跨线程访问控件?通常如何解决这个问题?编程
3. 简述后台线程和前台线程的区别?缓存
4. 说说经常使用的锁,lock是一种什么样的锁?安全
5. lock为何要锁定一个参数,可不可锁定一个值类型?这个参数有什么要求?网络
6. 多线程和异步有什么关系和区别?多线程
7. 线程池的优势有哪些?又有哪些不足?异步
8. Mutex和lock有何不一样?通常用哪个做为锁使用更好?异步编程
9. 下面的代码,调用方法DeadLockTest(20),是否会引发死锁?并说明理由。post
public void DeadLockTest(int i) { lock (this) //或者lock一个静态object变量 { if (i > 10) { Console.WriteLine(i--); DeadLockTest(i); } } }
10. 用双检锁实现一个单例模式Singleton。
11.下面代码输出结果是什么?为何?如何改进她?
int a = 0; System.Threading.Tasks.Parallel.For(0, 100000, (i) => { a++; }); Console.Write(a);
咱们运行一个exe,就是一个进程实例,系统中有不少个进程。每个进程都有本身的内存地址空间,每一个进程至关于一个独立的边界,有本身的独占的资源,进程之间不能共享代码和数据空间。
每个进程有一个或多个线程,进程内多个线程能够共享所属进程的资源和数据,线程是操做系统调度的基本单元。线程是由操做系统来调度和执行的,她的基本状态以下图。
当咱们建立了一个线程后,线程里面到底有些什么东西呢?主要包括线程内核对象、线程环境块、1M大小的用户模式栈、内核模式栈。其中用户模式栈对于普通的系统线程那1M是预留的,在须要的时候才会分配,可是对于CLR线程,那1M是一开始就分类了内存空间的。
补充一句,CLR线程是直接对应于一个Windows线程的。
还记得之前学校里学习计算机课程里讲到,计算机的核心计算资源就是CPU核心和CPU寄存器,这也就是线程运行的主要战场。操做系统中那么多线程(通常都有上千个线程,大部分都处于休眠状态),对于单核CPU,一次只能有一个线程被调度执行,那么多线程怎么分配的呢?Windows系统采用时间轮询机制,CPU计算资源以时间片(大约30ms)的形式分配给执行线程。
计算鸡资源(CPU核心和CPU寄存器)一次只能调度一个线程,具体的调度流程:
上面线程调度的过程,就是一次线程切换,一次切换就涉及到线程上下文等数据的搬入搬出,性能开销是很大的。所以线程不可滥用,线程的建立和消费也是很昂贵的,这也是为何建议尽可能使用线程池的一个主要缘由。
对于Thread的使用太简单了,这里就不重复了,总结一下线程的主要几点性能影响:
固然如今硬件的发展,CPU的核心愈来愈多,多线程技术能够极大提升应用程序的效率。但这也必须在合理利用多线程技术的前提下,了线程的基本原理,而后根据实际需求,还要注意相关资源环境,如磁盘IO、网络等状况综合考虑。
单线程的使用这里就略过了,那太easy了。上面总结了线程的诸多不足,所以微软提供了可供多线程编程的各类技术,如线程池、任务、并行等等。
线程池的使用是很是简单的,以下面的代码,把须要执行的代码提交到线程池,线程池内部会安排一个空闲的线程来执行你的代码,彻底不用管理内部是如何进行线程调度的。
ThreadPool.QueueUserWorkItem(t => Console.WriteLine("Hello thread pool"));
每一个CLR都有一个线程池,线程池在CLR内能够多个AppDomain共享,线程池是CLR内部管理的一个线程集合,初始是没有线程的,在须要的时候才会建立。线程池的主要结构图以下图所示,基本流程以下:
线程池是有一个容量的,由于他是一个池子嘛,能够设置线程池的最大活跃线程数,调用方法ThreadPool.SetMaxThreads能够设置相关参数。但不少编程实践里都不建议程序猿们本身去设置这些参数,其实微软为了提升线程池性能,作了大量的优化,线程池能够很智能的肯定是否要建立或是消费线程,大多数状况均可以知足需求了。
线程池使得线程能够充分有效地被利用,减小了任务启动的延迟,也不用大量的去建立线程,避免了大量线程的建立和销毁对性能的极大影响。
上面了解了线程的基本原理和诸多优势后,若是你是一个爱思考的猿类,应该会很容易发现不少疑问,好比把任务添加到线程池队列后,怎么取消或挂起呢?如何知道她执行完了呢?下面来总结一下线程池的不足:
所以微软为咱们提供了另一个东西叫作Task来补充线程池的某些不足。
任务Task与并行Parallel本质上内部都是使用的线程池,提供了更丰富的并行编程的方式。任务Task基于线程池,可支持返回值,支持比较强大的任务执行计划定制等功能,下面是一个简单的示例。Task提供了不少方法和属性,经过这些方法和属性可以对Task的执行进行控制,而且可以得到其状态信息。Task的建立和执行都是独立的,所以能够对关联操做的执行拥有彻底的控制权。
//建立一个任务 Task<int> t1 = new Task<int>(n => { System.Threading.Thread.Sleep(1000); return (int)n; }, 1000); //定制一个延续任务计划 t1.ContinueWith(task => { Console.WriteLine("end" + t1.Result); }, TaskContinuationOptions.AttachedToParent); t1.Start(); //使用Task.Factory建立并启动一个任务 var t2 = System.Threading.Tasks.Task.Factory.StartNew(() => { Console.WriteLine("t1:" + t1.Status); }); Task.WaitAll(); Console.WriteLine(t1.Result);
并行Parallel内部其实使用的是Task对象(TPL会在内部建立System.Threading.Tasks.Task的实例),全部并行任务完成后才会返回。少许短期任务建议就不要使用并行Parallel了,并行Parallel自己也是有性能开销的,并且还要进行并行任务调度、建立调用方法的委托等等。
这是不少开发C/S客户端应用程序会遇到的问题,GUI程序的界面控件不容许跨线程访问,若是在其余线程中访问了界面控件,运行时就会抛出一个异常,就像下面的图示,是否是很熟悉!这其中的罪魁祸首就是,就是“GUI的线程处理模型”。
.NET支持多种不一样应用程序模型,大多数的线程都是能够作任何事情(他们可能没有引入线程模型),但GUI应用程序(主要是Winform、WPF)引入了一个特殊线程处理模型,UI控件元素只能由建立它的线程访问或修改,微软这样处理是为了保证UI控件的线程安全。
为何在UI线程中执行一个耗时的计算操做,会致使UI假死呢?这个问题要追溯到Windows的消息机制了。
由于Windows是基于消息机制的,咱们在UI上全部的键盘、鼠标操做都是以消息的形式发送给各个应用程序的。GUI线程内部就有一个消息队列,GUI线程不断的循环处理这些消息,并根据消息更新UI的呈现。若是这个时候,你让GUI线程去处理一个耗时的操做(好比花10秒去下载一个文件),那GUI线程就没办法处理消息队列了,UI界面就处于假死的状态。
那咱们该怎么办呢?不难想到使用线程,那在线程里处理事件完成后,须要更新UI控件的状态,又该怎么办呢?经常使用几种方式:
① 使用GUI控件提供的方法,Winform是控件的Invoke方法,WPF中是控件的Dispatcher.Invoke方法
//1.Winform:Invoke方法和BeginInvoke this.label.Invoke(method, null); //2.WPF:Dispatcher.Invoke this.label.Dispatcher.Invoke(method, null);
② 使用.NET中提供的BackgroundWorker执行耗时计算操做,在其任务完成事件RunWorkerCompleted 中更新UI控件
using (BackgroundWorker bw = new BackgroundWorker()) { bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler((ojb,arg) => { this.label.Text = "anidng"; }); bw.RunWorkerAsync(); }
③ 看上去很高大上的方法:使用GUI线程处理模型的同步上下文来送封UI控件修改操做,这样能够不须要调用UI控件元素
.NET中提供一个用于同步上下文的类SynchronizationContext,利用它能够把应用程序模型连接到他的线程处理模型,其实它的本质仍是调用的第一步①中的方法。
实现代码分为三步,第一步定义一个静态类,用于GUI线程的UI元素访问封装:
public static class GUIThreadHelper { public static System.Threading.SynchronizationContext GUISyncContext { get { return _GUISyncContext; } set { _GUISyncContext = value; } } private static System.Threading.SynchronizationContext _GUISyncContext = System.Threading.SynchronizationContext.Current; /// <summary> /// 主要用于GUI线程的同步回调 /// </summary> /// <param name="callback"></param> public static void SyncContextCallback(Action callback) { if (callback == null) return; if (GUISyncContext == null) { callback(); return; } GUISyncContext.Post(result => callback(), null); } /// <summary> /// 支持APM异步编程模型的GUI线程的同步回调 /// </summary> public static AsyncCallback SyncContextCallback(AsyncCallback callback) { if (callback == null) return callback; if (GUISyncContext == null) return callback; return asynresult => GUISyncContext.Post(result => callback(result as IAsyncResult), asynresult); } }
第二步,在主窗口注册当前SynchronizationContext:
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); CLRTest.ConsoleTest.GUIThreadHelper.GUISyncContext = System.Threading.SynchronizationContext.Current; }
第三步,就是使用了,能够在任何地方使用
GUIThreadHelper.SyncContextCallback(() => { this.txtMessage.Text = res.ToString(); this.btnTest.Content = "DoTest"; this.btnTest.IsEnabled = true; });
多线程编程中很经常使用、也很重要的一点就是线程同步问题,掌握线程同步对临界资源正确使用、线程性能有相当重要的做用!基本思路是很简单的,就是加锁嘛,在临界资源的门口加一把锁,来控制多个线程对临界资源的访问。但在实际开发中,根据资源类型不一样、线程访问方式的不一样,有多种锁的方式或控制机制(基元用户模式构造和基元内核模式构造)。.NET提供了两种线程同步的构造模式,须要理解其基本原理和使用方式。
基元线程同步构造分为:基元用户模式构造和基元内核模式构造,两种同步构造方式各有优缺点,而混合构造(如lock)就是综合两种构造模式的优势。
基元用户模式比基元内核模式速度要快,她使用特殊的cpu指令来协调线程,在硬件中发生,速度很快。但也所以Windows操做系统永远检测不到一个线程在一个用户模式构造上阻塞了。举个例子来模拟一下用户模式构造的同步方式:
缺点有没有发现?线程2会一直使用CPU时间(假如当前系统只有这两个线程在运行),也就意味着不只浪费了CPU时间,并且还会有频繁的线程上下文切换,对性能影响是很严重的。
固然她的优势是效率高,适合哪一种对资源占用时间很短的线程同步。.NET中为咱们提供了两种原子性操做,利用原子操做能够实现一些简单的用户模式锁(如自旋锁)。
System.Threading.Interlocked:易失构造,它在包含一个简单数据类型的变量上执行原子性的读或写操做。
Thread.VolatileRead 和 Thread.VolatileWrite:互锁构造,它在包含一个简单数据类型的变量上执行原子性的读和写操做。
以上两种原子性操做的具体内涵这里就细说了(有兴趣能够去研究文末给出的参考书籍或资料),针对题目11,来看一下题目代码:
int a = 0; System.Threading.Tasks.Parallel.For(0, 100000, (i) => { a++; }); Console.Write(a);
上面代码是经过并行(多线程)来更新共享变量a的值,结果确定是小于等于100000的,具体多少是不稳定的。解决方法,可使用咱们经常使用的Lock,还有更有效的就是使用System.Threading.Interlocked提供的原子性操做,保证对a的值操做每一次都是原子性的:
System.Threading.Interlocked.Add(ref a, 1);//正确
下面的图是一个简单的性能验证测试,分别使用Interlocked、不用锁、使用lock锁三种方式来测试。不用锁的结果是95,这答案确定不是你想要的,另外两种结果都是对的,性能差异却很大。
为了模拟耗时操做,对代码稍做了修改,以下,全部的循环里面加了代码Thread.Sleep(20);。若是没有Thread.Sleep(20);他们的执行时间是差很少的。
System.Threading.Tasks.Parallel.For(0, 100, (i) => { lock (_obj) { a++; //不正确 Thread.Sleep(20); } });
这是针对用户模式的一个补充,先模拟一个内核模式构造的同步流程来理解她的工做方式:
看上去是否是很是棒!完全解决了用户模式构造的缺点,但内核模式也有缺点的:将线程从用户模式切换到内核模式(或相反)致使巨大性能损失。调用线程将从托管代码转换为内核代码,再转回来,会浪费大量CPU时间,同时还伴随着线程上下文切换,所以尽可能不要让线程从用户模式转到内核模式。
她的优势就是阻塞线程,不浪费CPU时间,适合那种须要长时间占用资源的线程同步。
内核模式构造的主要有两种方式,以及基于这两种方式的常见的锁:
既然内核模式和用户模式都有优缺点,混合构造就是把二者结合,充分利用二者的优势,把性能损失降到最低。大概的思路很好理解,就是若是是在没有资源竞争,或线程使用资源的时间很短,就是用用户模式构造同步,不然就升级到内核模式构造同步,其中最典型的表明就是Lock了。
经常使用的混合锁还很多呢!如SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockSlim,这些锁各有特色和锁使用的场景。这里主要就使用最多的lock来详细了解下。
lock的本质就是使用的Monitor,lock只是一种简化的语法形式,实质的语法形式以下:
bool lockTaken = false; try { Monitor.Enter(obj, ref lockTaken); //... } finally { if (lockTaken) Monitor.Exit(obj); }
那lock或Monitor须要锁定的那个对象是什么呢?注意这个对象才是锁的关键,在此以前,须要先回顾一下引用对象的同步索引块(AsynBlockIndex),这是前面文章中提到过的引用对象的标准配置之一(还有一个是类型对象指针TypeHandle),它的做用就在这里了。
同步索引块是.NET中解决对象同步问题的基本机制,该机制为每一个堆内的对象(即引用类型对象实例)分配一个同步索引,她实际上是一个地址指针,初始值为-1不指向任何地址。
所以,锁对象要求必须为一个引用对象(在堆上)。
首先仍是尽可能避免线程同步,无论使用什么方式都有不小的性能损失。通常状况下,大多使用Lock,这个锁是比较综合的,适应大部分场景。在性能要求高的地方,或者根据不一样的使用场景,能够选择更符合要求的锁。
在使用Lock时,关键点就是锁对象了,须要注意如下几个方面:
由于GUI应用程序引入了一个特殊的线程处理模型,为了保证UI控件的线程安全,这个线程处理模型不容许其余子线程跨线程访问UI元素。解决方法仍是比较多的,如:
上面几个方式在文中已详细给出。
应用程序必须运行完全部的前台线程才能够退出,或者主动结束前台线程,无论后台线程是否还在运行,应用程序都会结束;而对于后台线程,应用程序则能够不考虑其是否已经运行完毕而直接退出,全部的后台线程在应用程序退出时都会自动结束。
经过将 Thread.IsBackground 设置为 true,就能够将线程指定为后台线程,主线程就是一个前台线程。
经常使用的如如SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockSlim,lock是一个混合锁,其实质是Monitor['mɒnɪtə]。
lock的锁对象要求为一个引用类型。她能够锁定值类型,但值类型会被装箱,每次装箱后的对象都不同,会致使锁定无效。
对于lock锁,锁定的这个对象参数才是关键,这个参数的同步索引块指针会指向一个真正的锁(同步块),这个锁(同步块)会被复用。
多线程是实现异步的主要方式之一,异步并不等同于多线程。实现异步的方式还有不少,好比利用硬件的特性、使用进程或纤程等。在.NET中就有不少的异步编程支持,好比不少地方都有Begin***、End***的方法,就是一种异步编程支持,她内部有些是利用多线程,有些是利用硬件的特性来实现的异步编程。
优势:减少线程建立和销毁的开销,能够复用线程;也从而减小了线程上下文切换的性能损失;在GC回收时,较少的线程更有利于GC的回收效率。
缺点:线程池没法对一个线程有更多的精确的控制,如了解其运行状态等;不能设置线程的优先级;加入到线程池的任务(方法)不能有返回值;对于须要长期运行的任务就不适合线程池。
Mutex是一个基于内核模式的互斥锁,支持锁的递归调用,而Lock是一个混合锁,通常建议使用Lock更好,由于lock的性能更好。
public void DeadLockTest(int i) { lock (this) //或者lock一个静态object变量 { if (i > 10) { Console.WriteLine(i--); DeadLockTest(i); } } }
不会的,由于lock是一个混合锁,支持锁的递归调用,若是你使用一个ManualResetEvent或AutoResetEvent可能就会发生死锁。
public static class Singleton<T> where T : class,new() { private static T _Instance; private static object _lockObj = new object(); /// <summary> /// 获取单例对象的实例 /// </summary> public static T GetInstance() { if (_Instance != null) return _Instance; lock (_lockObj) { if (_Instance == null) { var temp = Activator.CreateInstance<T>(); System.Threading.Interlocked.Exchange(ref _Instance, temp); } } return _Instance; } }
int a = 0; System.Threading.Tasks.Parallel.For(0, 100000, (i) => { a++; }); Console.Write(a);
输出结果不稳定,小于等于100000。由于多线程访问,没有使用锁机制,会致使有更新丢失。具体缘由和改进在文中已经详细的给出了。
版权全部,文章来源:http://www.cnblogs.com/anding
我的能力有限,本文内容仅供学习、探讨,欢迎指正、交流。
.NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引
书籍:CLR via C#
书籍:你必须知道的.NET