前言
最近特别忙,博客就此荒芜,博主秉着哪里不熟悉就开始学习哪里的精神一直在分享着,有着扎实的基础才能写出茁壮的代码,有可能实现的逻辑有多种,但是心中必须有要有底哪个更适合,用着更好,否则则说明我们对这方面还比较薄弱,这个时候就得好好补补了,这样才能加快提升自身能力的步伐,接下来的时间会着重讲解线程方面的知识。强势分割线。
话题乱入,一到跳槽季节想必我们很多人就开始刷面试题,这种情况下大部分都能解决问题,但是这样的结果则是导致有可能企业招到并非合适的人,当然作为面试官的那些人们也懒得再去自己出一份面试题,问来问去就那些技术【排除有些装逼的面试官】,如果我作为面试官我会在网上挑出50%的面试题,其他面试则是现场问答,看看面试者的实际能力和平时的积累是怎样的。好了,现在随便出三道面试题,作为面试者的你,看你如何作答:
(1)利用Thread类创建线程有几种方式。
(2)如果你已工作3年,我要问你创建线程的至少3种方式,如果你已工作6年,我会问你创建线程的7种方式。
(3)线程的发展历程是怎样的,每一个历程分别是为了解决什么问题。
如果你需要沉思一会或者回答不出来,那你就有必要好好补补线程这方面的知识了!如果答案已有请对照文章最底部参考答案是否大概一致。
线程
线程确实很强大,强大到对于我而言只知道这个概念,由于自身的能力无法从底层去追究,只能通过网上资料或书籍来强势入脑,但是利用线程不当则导致各种各样问题的出现,若不作为开发者我们只能重启电脑或者打开任务管理器去直接关闭该死的那所属的进程,作为开发者的我们知道线程有着内存占用和运行时的性能开销即创建和销毁都是需要开销。每个线程都有以下因素
(1)线程内核对象。
(2)线程环境块。
(3)用户模式栈。
(4)内核模式栈。
(5)DLL线程连接和线程分离通知。
上述摘抄来自CLR Via C#,请原谅我懒得去看这段文字也不想看,没多大意思【因为我不懂】,比较底层的东西我就不去过多探讨了。好了,开始进入我们最原始的线程创建讲解。
线程基础(Thread)
我们创建一个线程并执行对应方法,如下:
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)
有时候我们需要等待上一线程执行完毕得到其结果接着往下进行,此时我们可以通过线程中的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,所以对于优先级的设置谁会去干呢。
多线程
保持UI界面持续响应
当有工作线程需要执行很长时间时,此时用多线程依然可以保持键盘和鼠标的事件。
并行计算
如果需要执行许多任务时,此时利用多线程采用分治策略将任务进行分摊,此时会提高计算效率。
充分利用cpu
当执行任务时此时有线程出现阻塞状态,此时利用多线程则能够充分利用已经被空闲无所事事的线程。
同时处理多个请求
如果客户端出现并发同时来多个请求,此时我们利用多线程则能够完全处理这样的情况。
总结
本文只是作为线程系列开胃菜,接下来我们将讲述线程池以及线程同步构造,内容开端的答案是否已经准备好呢,我们一一来解答。
(1)上述已经给出答案
(2)创建线程的7种方式如下:
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; } }
(3)线程历程
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开发者的福音。