前言:
因为平时挺少用到多线程的,写游戏时都在用协程,至于协程那是另一个话题了,除了第一次学习多线程时和以前某个小项目有过就挺少有接触了,最近准备面试又怕被问的深入,所以就赶紧补补多线程基础。
网上已经有很多线程编程的学习笔记了,那我为什么还要再整理一篇呢。因为我在搜索网上文章的时候发现一般别人整理的面试文章那很多语法都一笔带过了默认大家都懂,学习文章又很少有给出经典的题目,一般都是要几篇集合着一起看,既然如此的话我为什么不自己整理出一份呢?自己看的轻松,说不定以后也有人喜欢这种风格能帮助到别人。所以这篇文章也会参考很多其他的文章,最后都会写上引用的。
我写算法的时候也是很喜欢用C++ 来学习,而且笔试的时候我或者很多公司也喜欢用C++,毕竟C++ 的控制台程序输入输出格式化做的也比较好,那为什么这篇又用的是C#而不是C++ 呢?因为最近实习只带着笔记本,我的笔记本上只装了vscode用来写轻量级程序学习。C++装的是MinGW来编译,但是MinGW因为跨平台吧对std::thread支持的又不太好,反正又不是不会用别的语言,最重要的是学习的思想嘛。
一、进程和线程有什么区别?为什么要使用多线程?
首先用最经典的一道面试题作为引入。
①进程是资源分配的最小单位,线程是CPU调度的最小单位。
②一个线程只能属于一个进程,而一个进程可以有多个线程。
③进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。
④进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉。
⑤进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
⑥部分任务可能比较耗时,长时间占用CPU(你肯定不希望应用执行某个功能时整个程序都卡死),如果创建进程解决可能额外CPU开销更大,因此部分时候需要使用多线程技术。
二、C# 中使用多线程
在 C# 中,System.Threading.Thread 类用于线程的工作。它允许创建并访问多线程应用程序中的单个线程。进程中第一个被执行的线程称为主线程。 当 C# 程序开始执行时,主线程自动创建使用 Thread 类创建的线程被主线程的子线程调用。
再介绍Thread类中比较有用的一个静态方法,Sleep,用于挂起(可以看成暂停)线程一段时间,参数是毫秒。
下面来一个简单的例子看Thread的使用:
using System;
using System.Threading;
namespace LeeCarry
{public class Test{public static void Main(string[] args){Thread.Sleep(1000);//主线程暂停Thread npt=new Thread(NonParaThread);npt.Start();Thread wpt=new Thread(WithParaThread);wpt.Start("我是带参数的线程。");}private static void NonParaThread(){Console.WriteLine("我是不带参数的线程。");Thread.Sleep(1000);//子线程暂停Console.WriteLine("我是不带参数的线程。");}private static void WithParaThread(Object obj){Console.WriteLine(obj.ToString());}}
}
执行该程序首先暂停1秒(1000毫秒),接着连续输出
我是不带参数的线程
我是带参数的线程
然后再暂停1秒,接着输出
我是不带参数的线程
一开始是在主线程中挂起(暂停)的,所以两个线程都要等1秒才执行,后面是在不带参数的线程中挂起的,不影响另一个线程,所以带参数的线程就直接输出了。
但是要注意的是我这里是在Thread直接指定了方法,但实际上该方法有委托类型
public delegate void ThreadStart()//用于无参数方法
public delegate void ParameterizedThreadStart(object obj) //用于有参数方法
所以说刚刚的实例化可以展开成如下形式
Thread npt=new Thread(new ThreadStart(NonParaThread));
Thread wpt=new Thread(new ParameterizedThreadStart(WithParaThread));
这里的参数用的是object,有可能会被问到一个问题就是拆箱装箱,当然拆箱装箱和之前提的委托,包括什么匿名函数、lambda之类的都是另一个话题了,这里为了保持知识的独立性不过多的引入其他特性了,只在必要的时候讲。
三、资源抢占与信号
上面的内容十分简单,似乎跟普通的实例化类调用下函数没什么区别啊。
那我们再用一道面试题作为引入:
两个线程交替打印0~100的奇偶
如果只用上面的知识写出下面的代码,那运行一下就可以发现问题所在了。
using System;
using System.Threading;
namespace LeeCarry
{public class Test{public static void Main(string[] args){Thread oddThread=new Thread(OddThread);Thread evenThread=new Thread(EvenThread);evenThread.Start(); oddThread.Start();}private static void EvenThread(){for(int i=0;i<=100;i+=2){Console.WriteLine("线程1:{0}",i);}}private static void OddThread(){for(int i=1;i<=100;i+=2){Console.WriteLine("线程2:{0}",i);}}}
}
结果:
当然这个结果不一定是一样的,毕竟是两个线程并发在跑,但是却只有一个控制台啊,当然,往深一点说就是多个线程共享的资源。
就像十字路口如果不控制车辆开动的顺序,仍由他们乱开会引发严重的后果一样,实际上我们在编程中也经常会遇到控制线程顺序的需求。
那说到这其实也很好理解了,就跟十字路口需要红绿灯一样,我们也会用到信号灯的思想去控制线程的执行顺序。
在C#中有封装好类EventWaitHandle(并不严谨,下期说,这期是为了方便理解概念),有几个成员方法,
WaitOne():如果是红灯的话会将线程暂停在当前位置。
Set():相当于开绿灯,允许被暂停的线程通过开始执行下面的代码了
Reset():相当于开红灯,线程遇到wait会暂停住。
EventWaitHandle必须指定一个枚举类型,AutoReset或ManualReset,ManualReset很好理解,就是手动开关红绿灯。而AutoReset是指在执行Set()后会马上自定执行一次Reset(),相当于只是把当前在红灯前的线程放行。
那么有了这些知识我们就可以开始写代码了
using System;
using System.Threading;
namespace LeeCarry
{public class Test{public static EventWaitHandle oddFlag=new EventWaitHandle(false,EventResetMode.AutoReset);public static EventWaitHandle evenFlag=new EventWaitHandle(false,EventResetMode.AutoReset);public static void Main(string[] args){Thread oddThread=new Thread(OddThread);Thread evenThread=new Thread(EvenThread);evenThread.Start(); oddThread.Start();}private static void EvenThread(){for(int i=0;i<=100;i+=2){Console.WriteLine("线程1:{0}",i);oddFlag.Set();evenFlag.WaitOne();}oddFlag.Set();//最后开一次绿灯防止线程一直被阻塞,也可以在waitone加时间参数}private static void OddThread(){oddFlag.WaitOne();//确保偶数线程先运行for(int i=1;i<=100;i+=2){Console.WriteLine("线程2:{0}",i);evenFlag.Set();oddFlag.WaitOne();}evenFlag.Set();//最后开一次绿灯防止线程一直被阻塞,也可以在waitone加时间参数}}
}
当然,这里写成开了两个flag完全是为了方便理解,事实上可以只开一个叫flag,然后把evenFlag和oddFlag都改成flag,毕竟只有两个线程,要么停要么走嘛。
最后我们似乎能把两个函数再合成成为一个函数,但是这样的话单单一个信号灯似乎是没办法解决的了,可以像上面用两个信号量或者下篇还有机会出来的话讲讲锁。
引用:
.NET面试题解析(07)-多线程编程与线程同步-/梦里花落知多少/
Thread Class-MSDN
C#多线程-菜鸟教程
EventWaitHandle Class-MSDN
多线程C#面试题-Ax0ne