最近特别忙,博客就此荒芜,博主秉着哪里不熟悉就开始学习哪里的精神一直在分享着,有着扎实的基础才能写出健壮的代码,有可能实现的逻辑有多种,可是心中必须有要有底哪一个更适合,用着更好,不然则说明咱们对这方面还比较薄弱,这个时候就得好好补补了,这样才能加快提高自身能力的步伐,接下来的时间会着重讲解线程方面的知识。强势分割线。面试
话题乱入,一到跳槽季节想必咱们不少人就开始刷面试题,这种状况下大部分都能解决问题,可是这样的结果则是致使有可能企业招到并不是合适的人,固然做为面试官的那些人们也懒得再去本身出一份面试题,问来问去就那些技术【排除有些装逼的面试官】,若是我做为面试官我会在网上挑出50%的面试题,其余面试则是现场问答,看看面试者的实际能力和平时的积累是怎样的。好了,如今随便出三道面试题,做为面试者的你,看你如何做答:windows
(1)利用Thread类建立线程有几种方式。api
(2)若是你已工做3年,我要问你建立线程的至少3种方式,若是你已工做6年,我会问你建立线程的7种方式。浏览器
(3)线程的发展历程是怎样的,每个历程分别是为了解决什么问题。安全
若是你须要沉思一会或者回答不出来,那你就有必要好好补补线程这方面的知识了!若是答案已有请对照文章最底部参考答案是否大概一致。cookie
线程确实很强大,强大到对于我而言只知道这个概念,因为自身的能力没法从底层去追究,只能经过网上资料或书籍来强势入脑,可是利用线程不当则致使各类各样问题的出现,若不做为开发者咱们只能重启电脑或者打开任务管理器去直接关闭该死的那所属的进程,做为开发者的咱们知道线程有着内存占用和运行时的性能开销即建立和销毁都是须要开销。每一个线程都有如下因素多线程
(1)线程内核对象。并发
(2)线程环境块。异步
(3)用户模式栈。函数
(4)内核模式栈。
(5)DLL线程链接和线程分离通知。
上述摘抄来自CLR Via C#,请原谅我懒得去看这段文字也不想看,没多大意思【由于我不懂】,比较底层的东西我就不去过多探讨了。好了,开始进入咱们最原始的线程建立讲解。
咱们建立一个线程并执行对应方法,以下:
var t = new Thread(Basic); t.Start(); static void Basic() { Console.WriteLine("跟着Jeffcky学习线程系列"); }
就是这么简单, 该线程实例有一个 IsAlive 属性,一旦线程启动该属性则会为True直到线程执行完毕。接下来咱们将上述再添加一句打印以下:
var t = new Thread(Basic); t.Start(); Console.WriteLine("我是主线程");
固然也有多是这样的
在主线程上建立了一个新的线程,此时虽然建立了新的线程可是还未就绪,主线程抢先一步而执行。致使打印前后顺序就不一样。下面咱们再来看一个例子:
class Program { static bool isRun; static void Main(string[] args) { var t = new Thread(Basic); t.Start(); Basic(); Console.ReadKey(); } static void Basic() { if (!isRun) { Console.WriteLine("正在运行"); isRun = true; } } }
此时你以为结果可能会是这样的,是否是必定是以下这样呢?
若是咱们再多运行几回,你会发现出现以下结果:
为何会出现两种大相径庭的结果,这里就得涉及到线程安全的问题,这里两个线程就属于多线程场景,有可能当主线程或者建立的线程先执行打印出【正在执行】,此时将isRun设置为True,而这个时候主线程或者新线程才执行到这个Basic,此时isRun已经为True,那么将只能打印一次。若是将上述代码进行以下改造,只打印出一个的几率将会大大提升。
static void Basic() { if (!isRun) { isRun = true; Console.WriteLine("正在运行"); } }
此时为了保证在控制台中只打印一次,咱们须要采用加锁机制,以下:
class Program { static bool isRun; static readonly object objectLocker = new object(); static void Main(string[] args) { var t = new Thread(Basic); t.Start(); Basic(); Console.ReadKey(); } static void Basic() { lock (objectLocker) { if (!isRun) { isRun = true; Console.WriteLine("正在运行"); } } } }
咱们看看Thread这个类中建立线程的构造函数,看到建立线程有以下两个构造函数:
// // 摘要: // 初始化 System.Threading.Thread 类的新实例。 // // 参数: // start: // 表示开始执行此线程时要调用的方法的 System.Threading.ThreadStart 委托。 // // 异常: // T:System.ArgumentNullException: // start 参数为 null。 [SecuritySafeCritical] public Thread(ThreadStart start); // // 摘要: // 初始化 System.Threading.Thread 类的新实例,指定容许对象在线程启动时传递给线程的委托。 // // 参数: // start: // 一个委托,它表示此线程开始执行时要调用的方法。 // // 异常: // T:System.ArgumentNullException: // start 为 null。 [SecuritySafeCritical] public Thread(ParameterizedThreadStart start);
咱们简单过一下
var t = new Thread(new ThreadStart(Basic));
第二个构造函数中的参数为一个委托类型,以下:
[ComVisible(false)] public delegate void ParameterizedThreadStart(object obj);
这个时候就明朗了,在没有lambda表达式出现前,咱们只能经过匿名方法来实现。
var t = new Thread(delegate () { Basic(); }); t.Start();
有了lambda出现,建立线程注入参数则更加简便了,以下:
var t = new Thread(()=> { Basic(); }); t.Start();
固然根据上述委托定义,咱们一样可以传递参数,以下:
var t = new Thread(()=> { Basic("Hello cnblogs"); }); t.Start(); static void Basic(string message) { Console.WriteLine(message); }
同时咱们看到启动线程的方法Start还有以下参数为object的重载。
此时咱们还能够经过Start来传递委托参数,以下:
var t = new Thread(()=> { Basic }); t.Start("Hello cnblogs"); static void Basic(object message) { var msg = message as string; Console.WriteLine(message); }
好了到了这里咱们解决了第一道面试题,经过Thread建立线程有如上四种方式(确切的说是两种不一样方式,四种表现形式)。有时候咱们在多线程场景下须要阻塞主线程而等待建立的线程的结果再往下执行,此时咱们须要用到JOIN和Sleep来进行阻塞。
有时候咱们须要等待上一线程执行完毕获得其结果接着往下进行,此时咱们能够经过线程中的JOIN和Sleep来阻塞当前线程,以下所示由于Main方法调用JOIN方法,那么JOIN方法会形成调用线程阻塞当前执行的任何代码等待新建立线程的销毁或终止才继续往下执行。
class Program { static void Main(string[] args) { var t = new Thread(Basic); t.Start("Hello cnblogs"); t.Join(); Console.WriteLine("我是主线程"); Console.ReadKey(); } static void Basic(object message) { var msg = message as string; Console.WriteLine(message); } }
一样利用Sleep也是如此
var t = new Thread(Basic); t.Start("Hello cnblogs"); Thread.Sleep(4000); Console.WriteLine("我是主线程");
同时咱们应该看到Sleep方法有以下说明:
也就是说用Thread.Sleep(0)会当即释放当前时间片,让出cpu来执行其余线程,此时就有可能打印出主线程和新线程的顺序前后不同。
讲到线程咱们就离不开对进程的讲解,线程被称为轻量级进程,它是cpu执行的最小单元,而进程是操做系统执行的基本单元,一个进程能够包含多个线程,在任务管理器咱们看到的则是进程,每一个进程之间相互独立,各自为政,这个稍微想象一下就能明白,如有影响那就乱套了,究其根本缘由则是,每一个进程都被赋予了一块虚拟地址空间,这样就确保在一个进程中使用的代码和数据没法由另一个进程访问,但线程与线程之间就不必定,线程与线程之间能够共享内存,这个理解起来也不难,当咱们一个线程在获取数据时,此时另外一个线程则能够显示去获取数据进程内存中所存放的数据。那么问题又来了,线程究竟是如何工做的呢?就像一场活动,总有主办方来安排这一切,来的客人一进门都会被工做人员安排会座位并被好生招牌,如此一切才能井井有理进行,此时的客户就像一个线程,因此同理,在线程内部有一个线程调度器来安排线程的几个状态,好比活动主办方请客户过来观看,此时就有一个帖子上面写好了邀请的人,这就像线程中的状态之一【新建】,当主办方一切安排稳当活动开始后,此时会邀请客户到上面去演讲,上一个快要演讲完毕此时会通知下一位,此时就像线程状态之二【就绪】,最后轮到客户上去演讲,很天然就过渡到了线程状态之三【运行】,在客户演讲时中途可能还有答问环节才能继续进行下一环节的继续进行,此时就像线程状态之四【阻塞】,最终客户演讲完毕,主办方会送客户离场,此时客户的任务算是结束,这就像线程最终状态【死亡】,如此就完成了一个线程的整个生命周期。线程调度器会确保当前全部线程都可以分配到合适的时间,就像人民名义中侯亮平对全部人都一视同仁,毫不徇私。若是一个线程在等待一个用户的操做,在一个时间片的长度内用户没有彻底用完,也就说用户没有进行持续输入,那么此时线程将进入等待状态,剩余的时间片将自动进行放弃,使得在任何cpu上都不会执行该线程,直到发生下一次输入事件,因此在总体上加强了系统的性能,由于其线程可自动终止其时间片,因此调用线程的线程调度器在必定程度上保证那些被阻塞的线程不会消耗cpu时间。
那么问题来了,当一个线程的时间片用完,操做系统将进行上下文切换(windows操做系统大约30毫秒执行一次上下文切换),那么进行上下文切换时到底发生了什么呢?
这个时候咱们就有必要了解线程的组成部分:一个标准的线程由线程ID,当前指令指针,寄存器组合和堆栈-来源(http://baike.sogou.com/v49119.htm?fromTitle=线程##5)那么再下次获取上一次线程用户输入的值须要通过如下三个阶段。
(1)将cpu寄存器中的值保存到当前正在运行的线程的内核对象内部的一个上下文结构中。
(2)从现有线程集合中选出一个线程供调度,若是该线程由另一个进程拥有,windows在开始执行任何代码或者接触任何数据以前,还必须切换cpu可以看见的虚拟地址空间。
(3)将全部上下文结构中的值加载到cpu的寄存器中。
默认状况下经过Thread建立的线程都为前台线程,若是咱们须要显式指定建立的线程为后台线程,此时咱们须要进行以下指定。
var t = new Thread(Basic); t.IsBackground = true; t.Start("Hello cnblogs"); Console.WriteLine("我是主线程");
上述咱们将建立的线程改写为后台线程,一旦前台线程即主线程执行完毕,此时那么后台线程也随即结束,接下来咱们进行以下改造。
var str = string.Empty; var t = new Thread(() => Console.WriteLine()); if (str.Length > 0) { t.IsBackground = true; }
如上咱们知道str长度为0此时也就说明建立的新线程为前台线程,即便此时主线程结束了,可是建立的新线程会依然赖活着,关于前台线程一旦结束则全部后台也会强制结束,然后台线程结束并不会致使前台线程自动结束,这个也不难理解,好比在浏览器上多开几个页面,此时在后台也会建立对应的打开的tab线程,可是如果关闭这个tab页只是关闭了建立这个tab的后台线程而前台线程即浏览器不会关闭,若关闭浏览器的线程此时全部打开页的后台线程将强制进行结束就是这么个缘由。
咱们来看下程序:
class Program { static void Main(string[] args) { try { var t = new Thread(Basic); t.Start(); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.ReadKey(); } static void Basic() { throw null; } }
咱们会发现对上述执行方法try{}catch{}结果永远都不会抛异常,这是由于线程有其独立的执行路径,因此在当前线程上不会抛出异常,因此咱们只能在方法内部去抛出异常并解析,以下:
static void Basic() { try { throw null; } catch (Exception ex) { Console.WriteLine(ex.Message); } }
咱们稍微过一下线程的优先级,线程优先级有以下几个枚举值。
enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
为什么要出现线程优先级,咱们想一想若是是在多线程场景下咱们有可能明确须要某个线程的优先级很高,让其优先执行,因此在多线程下线程优先级颇有意义,可是实际状况下不多有开发者去设置这个属性,那是为何呢,此时咱们得讲讲优先级了,线程优先级有0(最低)-31(最高)之间的值,操做系统决定让cpu去执行哪一个线程时首先会去检查线程的优先级,经过优先级来采起轮流的方式调度线程以此类推,可是这其中就存在一个问题,若是将一个线程优先级设置为31,那么系统将永远不会对0-30的线程分配cpu,如此则形成【线程饥饿】,就像签定了霸王条款同样,致使优先级高的线程长期占用cpu,那么其余的线程处于空闲则没法充分利用cpu,因此对于优先级的设置谁会去干呢。
当有工做线程须要执行很长时间时,此时用多线程依然能够保持键盘和鼠标的事件。
若是须要执行许多任务时,此时利用多线程采用分治策略将任务进行分摊,此时会提升计算效率。
当执行任务时此时有线程出现阻塞状态,此时利用多线程则可以充分利用已经被空闲无所事事的线程。
若是客户端出现并发同时来多个请求,此时咱们利用多线程则可以彻底处理这样的状况。
本文只是做为线程系列开胃菜,接下来咱们将讲述线程池以及线程同步构造,内容开端的答案是否已经准备好呢,咱们一一来解答。
class Program { static BackgroundWorker bw = new BackgroundWorker(); static void Main(string[] args) { //线程实现方式一 var t = new Thread(Basic); t.Start(); //线程实现方式二 bw.DoWork += bw_basic; bw.RunWorkerAsync("Jeffcky from cnblogs"); //线程实现方式三 ThreadPool.QueueUserWorkItem(Basic); //线程实现方式四 Func<string, int> method = RetLength; IAsyncResult cookie = method.BeginInvoke("Jeffcky", null, null); int result = method.EndInvoke(cookie); //线程实现方式五 new Task(Basic, 23).Start(); //线程实现方式六 Task.Run(() => Basic(23)); //线程实现方式七 Task.Factory.StartNew(() => Basic(23)); Console.ReadKey(); } static void bw_basic(object sender, DoWorkEventArgs e) { Console.WriteLine(e.Argument); } static void Basic(object message) { var msg = (string)message; Console.WriteLine(message); } static int RetLength(string str) { return str.Length; } }
Thread:虽说是有CLR来管理但实际上可等同于Windows线程,咱们能够看所是操做系统级别线程,有它的堆栈和核心资源,虽然有丰富的api咱们能够设置其运行状态和优先级可是其性能开销之大可想而知,每一个线程的建立都要消耗没记错的话应该是1兆的内存,同时对于线程进行上下文的切换额外还增长了cpu的开销,若是线程不够处理当前请求还得从新建立线程同时咱们还得手动去维护线程的状态。
ThreadPool:线程池这才正式由CLR管理,线程池就像线程的包装器,它没有任何控制,咱们能够随时来提交咱们须要执行的工做,咱们能够控制线程池的大小来优化性能,咱们不须要再额外设置其余内容,咱们不须要告诉线程什么时候开始执行咱们的任务,在CLR初始化时,线程池中没有任何线程,在线程池内部维护了一个操做请求队列,当程序执行操做时,此时会将该任务追加到线程池的队列中,当到要执行的线程池队列中的线程时,此时从队列中取出并将任务派发给已取出队列中的线程,当线程池中的线程执行完任务后此时线程将不会被销毁,它会从新返回到线程池中并处于空闲状态,等待下一个请求的调度,因此因为线程不会自身进行销毁而是进行回收,不会再产生额外的性能损失,固然建立线程会形成必定的性能损失这是不可避免的,可是利用线程池来执行任务最适合哪些不须要通知结果的操做,若是咱们须要明确知道操做何时完成而且有返回值,那么此时线程池就作不到。
Task:该TPL提供了足够丰富的api而且像线程池同样不会建立本身的操做系统级别线程,经过Task咱们能够查找到任务什么时候完成而且能够在现有任务基础上进行ContinueWith,同时咱们能够经过Wait来同步等待其结果就像Thread中的JOIN方法同样,因为任务依然是在线程池上执行,因此不适合执行长时间的任务操做,由于任务能够填充线程池来阻塞新的任务,Task提供了一个LongRunning选项来告知不运行在线程池上。全部最新的高级并发api,如Parallel.For *()方法,PLINQ,C#5等待以及BCL中的现代异步方法都是基于Task构建的。
综上所述,咱们能够得出一个结论:Thread为操做系统级别线程,建立线程以及上下文切换带来的巨大性能开销可想而知,致使死锁的状况更是没法想象,利用ThreadPool来对线程进行回收不会再形成上下文切换的性能损失,可是它没法告知任务执行的结果,经过Task在线程池的基础上实现任务执行完成的结果并在现有任务上进行其余操做以及其余对于并发的高级api让咱们再次欢喜,成为.net开发者的福音。