进程与线程
概念
1.CPU的线程概念和程序的进程线程概念不同。这里我们只将程序的概念。程序中一次只能执行一个进程,一个进程至少包含一个线程(windows系统中是这样)。具体可以查看简书:https://www.jianshu.com/p/af6dcc255dbe中大佬的讲解
2.如果有一块内存空间很特殊,要求每次只能有一个线程进行读写操作,那么可以使用“互斥锁”(Mutual exclusion,缩写Mutex),防止多个线程同时读写某一块内存区域。
3.如果有一块内存空间允许多线程操作,但要求每次最多只能有N个线程进行读写操作,那么可以使用“信息量”(Semaphore)的方法进行限制。实现方式类似于假如这块内存空间被线程操作时有个count进行计数,每当有个线程读写时count就-1,当count=0时就不允许有线程对这块内存空间进行操作了,直到有一个线程退出,才能允许下一个线程进入。
不难看出,Mutex是Semaphore的一种特殊情况。
4.操作系统的设计,可以归结为三点:
(1)以多进程形式,允许多个任务同时进行。
(2)以多线程形式,允许单个任务分为不同的部分进行。
(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程方面和线程方面共享资源。
线程-委托方式发起线程
在.NET Framework中我们可以使用委托调用BeginInvoke方法开启一个线程。
Ps:.NET core 3.0不支持该方法,编译可通过,但运行会报错
直接贴代码:
BeginInvoke开启线程
需要引入命名空间using System.Threading;
class Program { static int Test(int i, string str) { Console.WriteLine("线程开启。ID:" + i + " 线程名:" + str); Console.WriteLine("运算中"); Thread.Sleep(100);//使当前线程休眠,单位为ms Console.WriteLine("\n线程结束,返回结果"); return i+1; } static void Main(string[] args) { Func<int, string, int> f = Test;//委托指向所需开启的线程 //IAsyncResult类的对象用于取得当前线程的状态 IAsyncResult ar = f.BeginInvoke(100, "线程测试", null, null); //通过BeginInvoke开启线程。ps:.NET core 3.0不支持BeginInvoke Console.WriteLine("Main"); Console.ReadKey(); } }
其中:Console.ReadKey();//如果不等线程结束就执行这步,进程会强制关闭线程,因为它开启的是一个后台线程。关于后台和前台线程下面会讲
IAsyncResult接口
首先解释下里面的IAsyncResult接口,这个接口是用来表示异步操作的状态的。按F12可查看到:
然后是IAsyncResult ar = f.BeginInvoke(100, "线程测试", null, null);这句代码。
光标放在BeginInvoke上我们可以看到,对应着我们定义的Func<int,string,int>,前两个参数即是我们传递到Func中的两个参数(100, "线程测试"),都很好理解。Func指向的方法需要几个参数,BeginInvoke前面部分就需要几个参数;如果Func不需要参数,那么BeginInvoke就只有了两个参数;
然后是倒数两个参数。第一个是回调函数,可在线程结束后调用一个方法,这个方法或委托只能传递一个IAsyncResult类型的参数,该参数值由第二个参数提供,一般可以传递被调用方法的委托;
检测线程结束并输出返回值
检测线程结束并输出返回值的方法有一下几个:
方法1:使用IAsynResult.IsCompleted判断线程状态并用Func<>.EndInvoke(IAsynResult result)取得返回值
while (ar.IsCompleted == false) { Console.Write("."); Thread.Sleep(10); } int result = f.EndInvoke(ar);//通过EndInvoke取得返回值 //EndInvoke将会阻塞线程,直到异步调用完成后才返回 Console.WriteLine("线程的返回结果为:" + result); Console.ReadKey();
阻塞线程在这里的意思就是result会等待委托f开启的线程结束,才会被EndInvoke的返回值赋值。加入f指向的方法中最后一步Thread.Sleep(10000);这个操作的话,result会直到10s后方法结束了才得到赋值,继续执行下面的代码。
运行结果:
由于是异步,所以’.’可能会在”线程开启.....”前输出,如上图所示
方法2:使用IAsyncResult.AsyncWaitHandle.WaitOne(int millisecondsTimeout)判断线程状态
bool isEnd = ar.AsyncWaitHandle.WaitOne(2000); if(isEnd) { int res = f.EndInvoke(ar); Console.WriteLine("线程的返回结果为:" + res); }
WaitOne()有多个重载,这里我们只需要使用第一个参数为int类型的即可。为了简便我们直接将millisecondsTimeout简略成t。
WaitOne(int t)表示在指定时间t毫秒内等待,直到超时或者完成。在时间内完成返回true;当超时线程仍没有完成则返回false。t为0表示不等待,t为-1表示永远等待直到异步调用完成
输出结果如下:
方法3:使用BeginInvoke中的回调函数进行返回
既然是使用回调函数,我们就需要将上面的BeginInvoke改一下:
IAsyncResult ar = f.BeginInvoke(100,”线程测试”,OnCallBack,f);
//ps:.NET core 3.0不支持BeginInvoke
编写一个回调函数:
static void OnCallBack(IAsyncResult iaRes) { Func<int,string,int> f = iaRes.AsyncState as Func<int,string,int>; int i = f.EndInvoke(iaRes);//取得返回值 Console.WriteLine(“线程测试结束,返回值为:”+i); }
看下输出结果:
没问题。
这里有两个注意点:
(1)IAsyncResult ar = f.BeginInvoke(100,”线程测试”,OnCallBack,f);中f的执行顺序和BeginInvoke中最后面的两个参数。
顺序:首先会先执行委托f(BeginInvoke前),然后执行参数f,而后再执行OnCallBack方法。
最后两个参数:第一个参数好理解,即我们要使用的回调函数名。它一定指向的是一个方法,这样才能执行异步同调。
第二个参数可以通过IAsyncResult.AsyncState获得。输入的值分三种情况:
当参数值为null时,由于没有传入什么参数给回调参数,所以回调参数中只能做一些与线程返回值无关的操作,因为此时AsyncState没有值,我们用Console.WriteLine(iaRes.AsyncState)输出一下可得如下结果:
当参数值为委托或者方法时,会执行这个方法并将返回值作为OnCallBack的参数传递给OnCallBack。以上面f为例,按照顺序会先执行f.BeginInvoke,然后执行f的方法(Test),将f的返回值作为回调函数的参数。
当参数值为object类型的变量时,直接作为回调函数的参数。具体如何使用暂时还不懂,看到有些大佬说当遇到多线程的情况就很有用,可以用来区别线程。以后遇到了再更新吧。
(2)Func<int,string,int> f = iaRes.AsyncState as Func<int,string,int>;这样做的用意:
AsyncState用于获取IAsyncResult的一个对象,把光标放到上面我们可以看到提示:
前面我们知道EndInvoke可以获取线程的返回值,所以我们可以将AsyncState对象转换为委托类型的对象,然后调用该对象中EndInvoke得到我们想要的返回值。
扩展:可以使用Lambda表达式简化回调函数
IAsyncResult ar = f.BeginInvoke(100,”线程测试”,a=> { int res = f.EndInvoke(a); Console.WriteLine(“用Lambda表达式简化回调函数,返回值为:”+res); } ,null);
这样就显得代码更加简洁了。
以上参考大佬博客:https://www.cnblogs.com/oracleblogs/p/3275193.html
https://www.cnblogs.com/renhaojie/archive/2009/09/10/1564052.html
Thread开启线程
除了BeginInvoke,我们还能使用Thread中Start开启线程
需要引入System.Threading;命名空间
编写一个Fileload方法作为线程
static void DownloadFile(object FileName)//必须为object类的参数才能创建Thread对象 { Console.WriteLine("线程id为:"+ Thread.CurrentThread.ManagedThreadId+" 文件名为: "+FileName);//Thread.CurrentThread.ManagedThreadId为线程id Console.WriteLine("下载开始"); Thread.Sleep(2000); Console.WriteLine("下载完成"); }
注意:参数必须为object类型;方法不能为Conditional特性,下面会报错
然后在Main中进行调用:
Thread t = new Thread(DownloadFile);//如果上面的参数不为object这里会报错 t.start("xxx.avi"); Console.ReadKey();
输出:
可以用Lambda表达式简化一下:
Thread t = new Thread((object FileName )=> { Console.WriteLine("线程id为:"+ Thread.CurrentThread.ManagedThreadId+" 文件名为: "+FileName);//Thread.CurrentThread.ManagedThreadId为线程id Console.WriteLine("下载开始"); Thread.Sleep(2000); Console.WriteLine("下载完成"); });
我们也可以自己编写一个类,使用上面的方式调用类实例中的方法开启线程。就不赘述了。
后台线程和前台线程
概念:线程是寄托在进程上的,进程都结束了,线程会关闭。而前台线程只要未退出,进程就不会终止。也就是程序不会关闭(即在资源管理器中可以看到进程未结束。)
上面Thread开启的线程就是一个前台线程(默认创建后就是前台),main中会等待他执行完毕后才开始继续下面的代码。我们可以通过Thread.IsBackground赋值,将其设置为后台线程。
代码也是很简单,直接赋值即可。
控制线程
终止线程
我们可以使用Thread.Abort();终止线程。被终止的线程会抛出一个ThreadAbortException类型的异常,我们可以通过try catch获取这个异常,然后在异常结束前做一些清理工作。
线程睡眠
我们可以使用Thread.Join();使当前线程睡眠,直到t线程结束再继续执行下面的代码
线程池开启线程
我们可以使用ThreadPool.QueueUserWorkItem(method m,object o1,object o2....)方法来开启多个线程。
代码如下:
ThreadPool.QueueUserWorkItem(DownloadFile,"线程池测试1"); ThreadPool.QueueUserWorkItem(DownloadFile,"线程池测试2"); ThreadPool.QueueUserWorkItem(DownloadFile,"线程池测试3"); ThreadPool.QueueUserWorkItem(DownloadFile,"线程池测试4"); ThreadPool.QueueUserWorkItem(DownloadFile,"线程池测试5"); ThreadPool.QueueUserWorkItem(DownloadFile,"线程池测试6"); ThreadPool.QueueUserWorkItem(DownloadFile,"线程池测试7"); ThreadPool.QueueUserWorkItem(DownloadFile,"线程池测试8"); ThreadPool.QueueUserWorkItem(DownloadFile,"线程池测试9");
输出结果为:
线程池注意事项:
- 线程池中的线程都是后台线程,如果进程中的所有前台线程都结束了,所有后台线程都会停止。且不能讲入池的线程改为前台线程。
- 不能给入池的线程设置优先级或名称。
- 入池的线程只能用于时间较短的任务。如果线程需要一直运行(如word的拼写检查器线程),就应该使用Thread创建一个线程。
Task开启线程
使用Task.Start();开启线程:
依然是上面的DownloadFile方法作为线程:
static void DownloadFile(object FileName)//必须为object类的参数才能创建Thread对象 { Console.WriteLine("线程id为:" + Thread.CurrentThread.ManagedThreadId + " 输入参数为:" + FileName); Console.WriteLine("下载开始"); Thread.Sleep(2000); Console.WriteLine("下载完成"); }
然后创建一个Task对象用于获取方法:
Main中:
Task t = new Task(DownloadFile,"任务1"); t.Start(); Console.WriteLine("Main"); Console.ReadKey();
使用TaskFactory.StartNew开启线程:
TaskFactory tf = new TaskFactory(); Task t1 = tf.StartNew(DownloadFile, "任务1");//该方法能返回一个Task的对象 Task t2 = tf.StartNew(DownloadFile, "任务2"); Task t3 = tf.StartNew(DownloadFile, "任务3"); Task t4 = tf.StartNew(DownloadFile, "任务4"); Task t5 = tf.StartNew(DownloadFile, "任务5"); Console.WriteLine("Main"); Console.ReadKey();
输出结果如下:
简单控制任务顺序
如果一个任务t1的执行时依赖于另一个任务t2的,那么就需要在这个任务t2执行完毕后才开始执行t1。这个时候我们就可以使用连续任务。
比如,定义两个任务:
static void DoFirst() { Console.WriteLine("do in Task:"+Task.CurrentId); Thread.Sleep(3000); } static void DoSecond(Task t) { Console.WriteLine("task+"+t.Id+" finish"); Console.WriteLine("this task id is "+Task.CurrentId); Thread.Sleep(3000); }
Main中创建任务:
Task t1 = new Task(DoFirst); Task t2 = t1.ContinueWith(DoSecond); Task t3 = t1.ContinueWith(DoSecond); Task t4 = t2.ContinueWith(DoSecond);
这里t2和t3会等t1执行结束后才开始执行,t4则会等待t2执行完后开始。
任务层次结构
线程中是没有父子关系的,而任务中有。
当我们在一个任务t1中启动另一个任务t2,t2就相当与是t1的子任务了,两个任务异步执行,如果父任务t1执行完了但t2还没执行完,那么父任务的状态会被设置为WaitForChildrenToComplete而不是Complete;只有当子任务也执行完了,父任务的状态才会设置为RunToCompletion。
举个栗子:
//父任务 static void parentTask() { Console.WriteLine("parent task id "+Task.CurrentId); var child = new Task(childTask); child.Start(); Thread.Sleep(1000); Console.WriteLine("parent started child,parent end"); } //子任务 static void childTask() { Console.WriteLine("child start"); Thread.Sleep(5000); Console.WriteLine("child finish"); }
Main中:
var parent = new Task(parentTask); parent.Start(); Thread.Sleep(1000); Console.WriteLine("parent.Status:"+parent.Status); Thread.Sleep(4000); Console.WriteLine("parent.Status:"+parent.Status); Console.WriteLine("Main"); Console.ReadKey();
结果如下:
多线程间的争用条件
当同时有多个相同线程时,它们可能会争用同一个资源发生一些我们不想要看到的情况,这时候我们可以给被争用的对象加上一个所lock(对象)。
直接上代码:
自定义MyThreadObject类用于创建对象:
class MyThreadObject { private int state = 5; public void ChangeState() { state++; if (state==5) { Console.WriteLine("state = 5"); } state = 5; } }
编写一个循环调用的方法:
//循环调用MyThreadObject中的ChangeState方法 static void circleChange(object o) { MyThreadObject m = o as MyThreadObject; while (true) { //锁定对象,让其同一时刻只允许被一个线程使用 lock (m) { m.ChangeState(); } } }
主函数中创建两个线程模拟争用
static void Main(string[] args) { //发起第一个线程循环调用 MyThreadObject m = new MyThreadObject(); Thread t = new Thread(circleChange); t.Start(m); //发起第二个线程与第一个线程异步调用,此时两个线程会不停的将state更改为5,这样就会满足类中的ChangeState一直输出"state = 5" //解决方法就是在循环调用的方法中加上一个lock将对象锁定 new Thread(circleChange).Start(m); Console.ReadKey(); }
死锁问题
当两个线程中有两个锁并且造成了互相锁定导致程序无法继续执行的情况,如以下代码:
public void Deadlock1() { int i = 0; while (true) { lock (s1) { lock (s2) { s1.ChangeState(); s2.ChangeState(); i++; Console.WriteLine("Running i:"+i); } } } } public void Deadlock2() { int i = 0; while (true) { lock (s2) { lock (s1) { s1.ChangeState(); s2.ChangeState(); i++; Console.WriteLine("Running i:" + i); } } } }
当线程1运行到Deadlock1中的lock(s1)时s1将被锁定,此时如果线程2运行到Deadlock2中的lock(s2)并将s2锁定,这样的话线程1将无法获取s2,线程2无法获取s1,他们就造成了我们常说的死锁。死锁归根到底是lock的顺序问题,所以我们只要安排好顺序,就能解决死锁的问题。就以上面的例子来说,我们只要统一好顺序,将Deadlock2中的lock(s2)和lock(s1)调换即可。
MyThreadObject m1 = new MyThreadObject(); MyThreadObject m2 = new MyThreadObject(); SampleThread s = new SampleThread(m1,m2); new Thread(s.Deadlock1).Start(); new Thread(s.Deadlock2).Start();
输出结果为: