概念

进程概念

程序在运行时,占据的计算资源集合,称之为进程,进程之间不会相互干扰,但是进程之间的通信比较困难。

线程概念

计算机的概念:

程序执行的最小的单位,响应操作的最小的执行流,线程也包含自己的计算资源,线程不能单独存在,线程属于进程,一个进程可以有多个线程。

多线程:一个进程里面有多个线程并发执行。

C#中的概念:

Theard:类,是对.NetFramework对线程对象的一个封装。通过Thread去完成的操作,最终是通过向操作系统请求得到的执行流。

CurrentThread:一个Therad类的属性,表示当前线程—任何操作都是线程完成的,运行当前这句话的线程。

ManagedThreadId:是.net平台(clr)给Thread起的名字。就是一个尽量不重复的Int值。

异步与线程的关系

在C#中,异步是通过创建线程的方式来实现的,多个线程同时工作称为异步,但是并非所有的异步都是通过创建线程的方式来实现,比如经典的node.js,他是用过单线程非阻塞的方式来实现异步,所以说异步可以多种实现,但是在C#中使用创建线程的方式,源于最初的语言设计,C#中的线程是单线程阻塞的。两种方式没有好坏之分,各有千秋。

线程的调用

》同步单线程:按顺序执行,每次调用完成后才能进入下一行,同一个线程运行。

》异步多线程:任何异步多线程都离不开委托delegate。发起调用不等待结束,动作会由新线程来执行。主线程与子线程同时计算-》并发。

  1. //同步单线程
  2. action. Invoke ("btnAsync_ _Click_ 1");
  3. action("btnAsync_ _Click_ 2") ;
  4. //多线程异步调用
  5. action. BeginInvoke(" btnAsync_ _Click_ 3' ,null, null) ;

1、异步多线程方法快、不卡界面

常见用处:注册发邮件、发短信/写日志等。

2、同步多线程方法慢、卡界面

多线程就是资源换性能。但是并不是线性增长的。

多线程存在协调管理成本,这会花费时间。资源是由上限的为,即使是多线程也不可以超越。

线程是有上限的,由线程池来控制。当然也可以突破这种方式,用thread启动。线程并不是越多越好的。

3、无序性(不可预测性):异步多线程

启动无序:同一时间向操作系统请求分配线程,cpu会调度分配,所以启动可能无序的。

结束无序:执行时间不确定(一个相同的任务,同一个线程执行也可能花费时间不同)。所以结束时间不确定。

操作系统:cpu是分片的,操作系统将cpu分为多个份,所以执行任务要看操作系统的调度,完全是看运气的——线程的优先级。优先级可以影响系统的调度。

/正式因为多战程具备不可预测性,很多时候你的想法并不一定能够贯 彻实施。也许大多数情况是ok
的,但是总有一定的概率出问题(墨事:任何小概率的坏事情。随着时间的推格,- - 定会发生)
电商下订单:增加订单表一日志发邮件生成支付一物流通知
多线程,有顺序要求,等待500ms执行 下一个动作,顺序测试100次都没问题
上线都没问题。。。但是,偶尔。一个月总有那么几次。顺序错了。。而且无法重现,
随着用户增加,数据量累计,数据率压力变大,服务器硬件资源不够,都会影响到执行效率。
/

使用多线程时,不要用延时等方式掌控顺序,不要试图(风骚的多模式)掌控顺序。

  1. static void Main(string[] args)
  2. {
  3. Program program = new Program();
  4. program.Begin();
  5. /*
  6. 这里有一个主意事项:如果不加下面的话,则会等不到子线程执行完,主线程就结束了,这样是没有意义的,所以说要想BeginInvoke可以执行,需要主线程等待 */
  7. Console.ReadKey();
  8. }
  9. public void Begin()
  10. {
  11. Action<string> action = this.GetNumber;
  12. string name = "老大";
  13. string name1 = "老二";
  14. for (int i = 0; i < 3; i++)
  15. {
  16. Console.WriteLine($"name:{name},线程:{Thread.CurrentThread.ManagedThreadId},开始");
  17. //action.Invoke(name);
  18. action.BeginInvoke("老二", null, null);
  19. Console.WriteLine($"name:{name},线程:{Thread.CurrentThread.ManagedThreadId},结束");
  20. }
  21. }
  22. public void GetNumber(string name)
  23. {
  24. int sum = 0;
  25. for (int i = 0; i < 1000000000; i++)
  26. {
  27. sum = sum + 2;
  28. }
  29. Console.WriteLine($"name:{name},线程:{Thread.CurrentThread.ManagedThreadId},次数:{sum}");
  30. }
  31. }

等待线程

回调函数

action.BeginInvoke(“参数”, 回调函数(委托), 状态码(可由回调函数获取));

  1. //回调函数
  2. AsyncCallback asyncCallback = ar =>
  3. {
  4. //这个方法获取第三个参数“ok”;
  5. Console.WriteLine(ar.AsyncState);
  6. Console.WriteLine("-------执行结束-------");
  7. };
  8. for (int i = 0; i < 3; i++)
  9. {
  10. Console.WriteLine($"name:{name},线程:{Thread.CurrentThread.ManagedThreadId},开始");
  11. //action.Invoke(name);
  12. // BeginInvoke执行不了
  13. //第一个参数是执行参数,第二个是回调函数,第三个是回掉函数可以使用的参数
  14. action.BeginInvoke("老二", asyncCallback, "ok");
  15. Console.WriteLine($"name:{name},线程:{Thread.CurrentThread.ManagedThreadId},结束");
  16. }
  17. /*打印结果
  18. name:老大,线程:1,开始
  19. name:老大,线程:1,结束
  20. name:老大,线程:1,开始
  21. name:老大,线程:1,结束
  22. name:老大,线程:1,开始
  23. name:老大,线程:1,结束
  24. name:老二,线程:3,次数:2000000000
  25. ok
  26. -------执行结束-------
  27. name:老二,线程:4,次数:2000000000
  28. ok
  29. -------执行结束-------
  30. name:老二,线程:5,次数:2000000000
  31. ok
  32. -------执行结束-------
  33. */

IsCompleted等待

》需求:用户必须确定操作完成,才能返回—-上传文件,只有成功之后才能预览—-需要等到任务计算真的完成后才能给用户返回

同步可以保证顺序,但是页面会卡死,异步不能保证顺序。

希望一方面文件上传完成之后才能预览,一方面给用户一个进度提示。

  1. IAsyncResult asyncResult = action. BeginInvoke("文件上传”,null null); //启动子线程完成计算
  2. int i=0;
  3. //IsCompleted是- - 个属性,用来描述异步动作是否完成;其实一开始就是个false,异步动作完成后回去修改这个属性为true.
  4. while (!lasyncResult. IsCompleted)//这里主线程在等待,界面会卡。
  5. {
  6. if(i<9){
  7. this. ShowConsoleAndView($"当前文件上传进度为{++i * 10}%...");
  8. }else{
  9. this. ShowConsoleAndView($"当前文件上传进度为99.999%... ");
  10. }
  11. Thread. Sleep (200);//这里会有最多200毫秒的误差
  12. }
  13. Console.WriteLine("界面预览");
  14. //真实开发中,一开始可以读倒文件的size, 然后就直接获取已经上传好的size, 做个比例就是进度

信号量等待

  1. var asyncResult = action. BeginInvoke("调用接口”,null null);
  2. asyncResult. AsyncWaitHandle. WaitOne() ;//阻塞当前线程,直到收到信号量,从asyncResult发出,无延迟,
  3. asyncResult. AsyncWaitHandle. Wait0ne(-1);//一直等待
  4. asyncResult. AsyncWaitHandle. WaitOne (1000) ;//阻塞当前线程,等待且最多只等待1000ms,超过就过时了。这个就是做超时控制的。

EndInvoke等待

有时候我们的异步多线程需要获取返回结果,则需要用一下这种方法,调用后他还会将线程快速回收

  1. Func<int> func = this. RemoteService;
  2. int iResult2 = func. Invoke() ;
  3. IAsyncResult asyncResult = func. BeginInvoke (nullnul1) ;//异步调用结果,描述异步操作的
  4. int iResult3 = func. EndInvoke (asyncResult) ;//获取返回值。

多版本的使用多线程的方式

注意使用线程时,一定要灵活多变,一定要记住线程执行的不确定性。

Framwork1.0(Thread)

Thread是一个类,是对计算机概念层级的封装,Threadstart是一个委托,当用到线程的时候就去申请,用完释放。

简单使用:

  1. ThreadStart threadStart = () =>
  2. {
  3. Console. WriteLine($"This is Thread Start{Thread. Current Thread. ManagedThreadId}");
  4. Thread. Sleep (2000) ;
  5. Console. WriteLine($"This is Thread End{Thread. CurrentThread. ManagedThreadId}") ;
  6. };
  7. Thread thread = new Thread (threadStart) ;
  8. thread. Start() ;//这里默认启动为前台线程,也就是主界面关闭线程不退出,直到线程执行完毕。

优势与缺陷:

Thread有非常丰富的API,但是这些API非常不好控制,因为线程是操作系统的,一个API要想执行,时间是不好控制的。响应并不灵敏。

Thread启动线程是没有控制的,启动多了可能会死机。

如果使用thread,吸引的地方可能就是前台线程(thread. Start())与做线程等待(thread.join())这两个需求api吸引人,其他的可能因为历史遗留问题或等等的缺点,并不建议使用,所以没有特别需求不建议去使用thread来开启线程

Framwork2.0(新的clr,ThreadPool)

池化资源管理:做一个容器,存放提前申请好的线程,当使用线程的时候,找线程池拿,用完放回容器(主要是控制状态),避免了频繁的申请与销毁。容器会根据自己的闲置数量去向操作系统去申请与释放。控制一定的线程量。与thread的对比就是引入了池化概念,减少了对线程操作的api(这些api很鸡肋)。

简单使用

  1. WaitCallback callback=o=>
  2. {
  3. Console. WriteLine($"This is ThreadPool Start{Thread. CurrentThread. ManagedThreadId} ") ;
  4. Thread. Sleep (2000) ;
  5. Console. WriteLine($"This is ThreadPool End{Thread. CurrentThread. ManagedThreadId} ") ;
  6. };
  7. ThreadPool.QueueUserWorkItem(callback) ;

好处与缺陷

线程池:实现了线程的复用,可以闲置最大线程数量。但是API太少。对线程等待与控制比较弱。可以用MRE,但是影响开发。

Framwork3.0

Task:被称为多线程的最佳实践

简单使用(启动的方式很多)
  1. Action action = () =>
  2. {
  3. Console. WriteLine($"This is Task Start{Thread. Current Thread. ManagedThreadId} ");
  4. Thread. Sleep (2000) ;
  5. Console. WriteLine($"This is Task End {Thread. CurrentThread. ManagedThreadId}");
  6. };
  7. Task task = new Task (action) ;
  8. task. Start() ;

好处与缺陷

线程全部出自线程池,提供了丰富的API。非常适合开发。

常用方法

  1. Task.WaitAll(tasks.ToArray());//阻塞主线程,等待所有tasks全部执行完才会执行主线程。。
  2. Task.WaitAny(tasks.ToArray());//阻塞主线程,只要有一个线程执行完,则会继续执行主线程。

TaskFacktory

出现对task的封装:taskFactory,扩展api

下 面的方法是等待全部任务完成后再驱动一个新的task来完成后续动作。

taskFactory.ContinueWhenAll
  1. Console.WriteLine($"-----线程:{Thread.CurrentThread.ManagedThreadId}开始-----");
  2. //声明一个集合,装载一些异步多线程执行方法的委托。
  3. IList<Task> tasks = new List<Task>();
  4. tasks.Add(Task.Run(()=>GetNumber("1")));
  5. tasks.Add(Task.Run(() => GetNumber("2")));
  6. tasks.Add(Task.Run(() => GetNumber("3")));
  7. //实例一个task工厂
  8. TaskFactory taskFactory = new TaskFactory();
  9. //调用一个方法:当所以线程完成之后,执行一个新的线程方法
  10. taskFactory.ContinueWhenAll(tasks.ToArray(), nt =>
  11. {
  12. Console.WriteLine($"-----线程:{Thread.CurrentThread.ManagedThreadId}开始-----");
  13. Console.WriteLine("这是一个新的方法,当所有tasks中的线程执行完执行");
  14. Console.WriteLine($"-----线程:{Thread.CurrentThread.ManagedThreadId}结束-----");
  15. });
  16. Console.WriteLine($"-----线程:{Thread.CurrentThread.ManagedThreadId}结束-----");
  17. Console.ReadLine();

下面的方法是等待任意一个任务完成就执行新的task。

  1. taskFactory.ContinueWhenAny(tasks.ToArray(), nt =>
  2. {
  3. Console.WriteLine($"-----线程:{Thread.CurrentThread.ManagedThreadId}开始-----");
  4. Console.WriteLine("这是一个新的方法,当有任意tasks中的线程执行完执行");
  5. Console.WriteLine($"-----线程:{Thread.CurrentThread.ManagedThreadId}结束-----");
  6. });

线程是不可预测的,所以几个动作的先后都有可能。也就是收,任意执行的task可能在所有的线程执行完才执行,也可能有一个线程执行完就执行了,这完全是cpu随机调度的结果。

Framwork4.0(Parallel)

主要是节约主线程,主线程参与计算。相当于task+waitall。因为是waitall,所以时卡界面的,我的理解就是解决了主线程在等待所有线程完成的过程中主线程资源浪费的问题

  1. //Parallel可以启动多线程,主线程也参与计算,节约-一个线程;
  2. //可以通过Parallelp轻松控制最大并发数量
  3. Parallel. Invoke(() =>
  4. {
  5. Console. WriteLine($"This is Parallel Startl{Thread. CurrentThread. ManagedThreadId} ");
  6. Thread. Sleep (2000) ;
  7. Console. WriteLine($" This is Parallel End1{Thread. Current Thread. ManagedThreadId}' ) ;
  8. },
  9. () =>
  10. {
  11. Console. WriteLine($"This is Parallel Startl{Thread. CurrentThread. ManagedThreadId} ");
  12. Thread. Sleep (2000) ;
  13. Console. WriteLine($" This is Parallel End1{Thread. Current Thread. ManagedThreadId}' ) ;
  14. });

理解:parallel的主要实现思想应该是一对任务主线程参与执行,每执行完一个,产看是否还有任务待执行和所有线程是否将任务执行完毕,都完了之后在向下执行,如果还有任务,则分配任务执行,完毕后在检查。

parallel的原理就是封装了task,深度不用管了。

BeginInvoke方式

这种方式支持的是委托去调用多线程,但是这种凡是在.net core中不被实现。

这种方式只支持framework的框架,也是开始介绍的一种形式。不多做介绍。

Framwork4.5(await/async)

这是一个语法糖,并不是一个全新的异步多线程的使用方法。

语法糖:是编译器提供的新功能。

await/async:本身不会产生新的线程,但是依托于Task而存在,在程序执行过程中是有多线程的。

用同步的编码形式去写异步编程。

async可以随便添加,可以不用await,await必须出现task前面且方法必须为async。

await/async之后,原本没有返回值的可 以返回Task,原本返回X类型的,可以返回Task,一般写async的返回值为task,不要返回void。

  1. 普通的taskawait/async对比:
  2. 可以认为,加了await 就等同于将await后面的代码,包装成- -个回调一其实回调的线程具备多种可能性。
  3. 原理:
  4. IL里面没有asyncawait,就是常规代码
  5. 首先实例化状态,状态是-1,然后去执行task前面的,启动线程去执行task》判断完成没?没有就把状态重置到0,然后递归调用下,就结束了,回去继续自己的使命。
  6. 子线程递归回去再次执行状态机动作,包括完成自己的后续动作,如果再遇到await,又把状态重置为-1,然后再继续。
  7. 状态机切换+递归,就能支持无线层级await
  8. yield return也是基于状态机的。

多线程的安全问题

线程安全问题一般都是“写“的时候。怎样保持数据的完整性和一致性很重要。

一段代码,如果单线程执行和多线程执行结果不一致那就是线程安全问题。

多线程的异常捕获

多线程里的异常是会被吞掉的(主线程是捕获不到的)。除非用waitall(主线程等待执行,如发生异常,主线程可以捕获。),但是一般情况下,请不要在主线程去捕获子线程的异常,可以在线程内部去处理异常(在子线程内部捕获异常)

  1. try
  2. {
  3. Task task = Task.Run(() =>
  4. {
  5. Console.WriteLine($"子线程执行{Thread.CurrentThread.ManagedThreadId}");
  6. Thread.Sleep(1000);
  7. throw new Exception("普通的异常");
  8. });
  9. // 不设置主线程等待,则主线程捕获不到子线程的异常
  10. Task.WaitAll(task);
  11. }
  12. catch (Exception ex)
  13. {
  14. Console.WriteLine(ex);
  15. }

一般情况下,子线程的异常由子线程本身来捕获,不要交给主线程捕获

  1. Task task = Task.Run(() =>
  2. {
  3. try
  4. {
  5. Console.WriteLine($"子线程执行{Thread.CurrentThread.ManagedThreadId}");
  6. Thread.Sleep(1000);
  7. throw new Exception("普通的异常");
  8. }
  9. catch (Exception ex)
  10. {
  11. Console.WriteLine(ex);
  12. }
  13. });

注意,两种捕获异常的方法,执行的顺序是不一样的,子线程无序,主线程有序。

线程的取消

线程取消要基于task。线程取消不是直接操作线程,而是操作一个共享的变量(信号量),变量在所有的线程中都是可以被访问的,每一个线程在执行的过程中都会去经常查看这个信号量。 当在某一个信号的时候,线程可以自己结束自己,但是结束的过程必然会有延迟,这是避免不了的。

  1. // 这是C#对信号量这个的封装,可以理解为一个bool值
  2. CancellationTokenSource cts = new CancellationTokenSource();
  3. for (int i = 0; i < 40; i++)
  4. {
  5. string name = $"---{i}---";
  6. Action<object> action = t =>
  7. {
  8. try
  9. {
  10. Thread.Sleep(2000);
  11. if (t.ToString().Equals("---11---"))
  12. {
  13. throw new Exception($"{t}执行失败");
  14. }
  15. if (t.ToString().Equals("---12---"))
  16. {
  17. throw new Exception($"{t}执行失败");
  18. }
  19. if (cts.IsCancellationRequested)
  20. {
  21. Console.WriteLine($"{t}放弃执行");
  22. }
  23. else
  24. {
  25. Console.WriteLine($"{t}执行成功");
  26. }
  27. }
  28. catch (Exception ex)
  29. {
  30. cts.Cancel();
  31. Console.WriteLine(ex.Message);
  32. }
  33. };
  34. TaskFactory taskFactory = new TaskFactory();
  35. taskFactory.StartNew(action, name, cts.Token);
  36. }

执行结果:(这里有机制:CancellationTokenSource是fw为了task的对信号量的一个面向对象的封装。 调用Cancel方法可以对当前线程取消,并对信号量做改变。

对已经启动的线程,检查到信号量改变,可以放弃执行。对与没有启动的线程任务,可以用taskFactory.StartNew(action, name, cts.Token);方式去,其中cts.Token是信号量的标志,当发生改变的时候(cts调用Cancel方法的之后),可以取消所有没有启动的任务(线程启动时有时间消耗的,这里是每一个线程的启动间间隔为2秒(2000毫秒))。

这个方式的满足的需求是:当一个任务执行失败的时候,可以用这种方式,将后续任务全部取消。取消任务会抛出异常的。 )

  1. 主线程开始执行1
  2. 主线程结束执行1
  3. ---6---执行成功
  4. ---4---执行成功
  5. ---7---执行成功
  6. ---3---执行成功
  7. ---0---执行成功
  8. ---5---执行成功
  9. ---1---执行成功
  10. ---2---执行成功
  11. ---8---执行成功
  12. ---10---执行成功
  13. ---13---执行成功
  14. ---14---执行成功
  15. ---9---执行成功
  16. ---12---执行失败
  17. ---15---执行成功
  18. ---11---执行失败
  19. ---17---放弃执行
  20. ---16---放弃执行
  21. ---18---放弃执行
  22. ---19---放弃执行

线程内部变量

  1. #region 实现线程内部变量
  2. {
  3. // 这里的打印完全是根据cpu的执行能力而出现不同的打印结果.
  4. // 不加变量的k的情况下:可能出现的结果为1,2,3,4,5或1,2,3,3,5等无法判断的数字情况,且无法得知多少种。原因就是cpu的执行速度很快,这是在主线程每启动一个子线程之前还睡100毫秒的情况下出现的,当主线程不睡100毫秒就启动子线程的情况下,那输出就是5,5,5,5,5了,原因也很简单:主线程执行速度过快,在子线程尚未启动执行的情况下,主线程已经将循环结束了,所以i等于5,这时候的子线程才开始执行,所以说出都为5.不要怀疑主线程的执行能力,启动一个线程是耗费时间的,这段时间主线程完全可以干完这个活。
  5. // 加变量k的情况下:可能出现3,1,0,4,2或者3,4,2,1,0等诸多情况,但是每一个k都是不一样的,这就和变量的作用域有关联了,每一次for循环都会赋值给k一个新的值,这是毋庸置疑的,但是每一个k在内存中是不一样的,可以理解为不同的k。所以在子线程执行打印k的时候,k对应的是当前作用域的k,所以执行打印的k是不同的,而i在内存中只有一个,所以是相同的。
  6. for (int i = 0; i < 5; i++)
  7. {
  8. int k = i;
  9. //Thread.Sleep(100);
  10. Task.Run(() =>
  11. {
  12. Console.WriteLine(i);
  13. Console.WriteLine(k);
  14. });
  15. }
  16. Console.ReadLine();
  17. }
  18. #endregion

加lock(锁)

原理:加lock就能解决线程安全问题——-就是单线程化—Lock就是保证方法任意时刻只有一个线程能进去,其他线程排队。最根本的实现就是单线程化。

有这样一个场景:

  1. Console.WriteLine($"-----主线程:{Thread.CurrentThread.ManagedThreadId}开始-----");
  2. IList<int> ts = new List<int>();
  3. for (int i = 0; i < 10000; i++)
  4. {
  5. Task.Run(() =>
  6. {
  7. ts.Add(i);
  8. });
  9. }
  10. Thread.Sleep(2000);
  11. Console.WriteLine($"个数{ts.Count()}");
  12. Console.WriteLine($"-----主线程:{Thread.CurrentThread.ManagedThreadId}结束-----");
  13. Console.ReadLine();

打印结果:(每一次打印结果不一样)

  1. -----主线程:1开始-----
  2. 个数7517
  3. -----主线程:1结束-----

造成这种现象的原因就是,List是一个数组结构,当在写入List时,同一时间可能有多个线程同时写入数据,所以会出现覆盖,也就是结果是小于10000的。

解决方法:加锁

  1. private static readonly object LOCK = new object();
  2. static void Main(string[] args)
  3. {
  4. Console.WriteLine($"-----主线程:{Thread.CurrentThread.ManagedThreadId}开始-----");
  5. IList<int> ts = new List<int>();
  6. for (int i = 0; i < 10000; i++)
  7. {
  8. Task.Run(() =>
  9. {
  10. lock (LOCK)
  11. {
  12. ts.Add(i);
  13. }
  14. });
  15. }
  16. Thread.Sleep(2000);
  17. Console.WriteLine($"个数{ts.Count()}");
  18. Console.WriteLine($"-----主线程:{Thread.CurrentThread.ManagedThreadId}结束-----");
  19. Console.ReadLine();
  20. /*语法糖
  21. lock (LOCK)
  22. {
  23. ts.Add(i);
  24. }
  25. 等价于(编译后):
  26. Monitor.Enter(LOCK);
  27. ts.Add(i);
  28. Monitor.Exit(LOCK);
  29. */

输出结果:

  1. -----主线程:1开始-----
  2. 个数10000
  3. -----主线程:1结束-----

原理:

  1. 其实这个Monitor是锁定内存引用地址,所以锁定的内容不能是值类型(null也不行)。
  2. 所以这里要声明:private static readonly object LOCK = new object();

注意:要想子线程抛异常,必须try—catch。

扩展

1、如果想要主程序与方法是并发的,就要用不同的锁,使用相同的锁会导致相互阻塞,不能并发。(使用相同的锁会导致不同并发,使用不同的锁会并发)

例子:(主方法与调用方法不能并发)

  1. /*---------------------------主方法--------------------------------*/
  2. static void Main(string[] args)
  3. {
  4. TaskTest.TT("大");
  5. for (int i = 0; i < 5; i++)
  6. {
  7. Task.Run(() =>
  8. {
  9. lock (TaskTest.LOCK)
  10. {
  11. Console.WriteLine($"很 线程:{Thread.CurrentThread.ManagedThreadId}----开始");
  12. Thread.Sleep(2000);
  13. Console.WriteLine($"很 线程:{Thread.CurrentThread.ManagedThreadId}----结束");
  14. }
  15. });
  16. }
  17. Console.Read();
  18. //-----------------------自定义类------------------------------------
  19. class TaskTest
  20. {
  21. public static readonly object LOCK = new object();
  22. public static void TT(string name)
  23. {
  24. for (int i = 0; i < 5; i++)
  25. {
  26. Task.Run(() =>
  27. {
  28. lock (LOCK)
  29. {
  30. Console.WriteLine($"任务:{name}-----线程:{Thread.CurrentThread.ManagedThreadId}----开始");
  31. Thread.Sleep(2000);
  32. Console.WriteLine($"任务:{name}-----线程:{Thread.CurrentThread.ManagedThreadId}----结束");
  33. }
  34. });
  35. }
  36. }
  37. }

2、当实例化一个类的两个不同的对象,分别调用相同方法,方法内部加锁,则两个对象调用的方法之间是并发的。(因为实例化的时候锁也会实例,本质就是调用的加锁方法的锁是不同的。)

例子:(可以并发)

  1. //主方法
  2. static void Main(string[] args)
  3. {
  4. TaskTest.TT("大");
  5. TaskTest.TT("小");
  6. Console.Read();
  7. return;
  8. }
  9. //自定义类
  10. class TaskTest
  11. {
  12. public static readonly object LOCK = new object();
  13. public static void TT(string name)
  14. {
  15. for (int i = 0; i < 5; i++)
  16. {
  17. Task.Run(() =>
  18. {
  19. lock (LOCK)
  20. {
  21. Console.WriteLine($"任务:{name}-----线程:{Thread.CurrentThread.ManagedThreadId}----开始");
  22. Thread.Sleep(2000);
  23. Console.WriteLine($"任务:{name}-----线程:{Thread.CurrentThread.ManagedThreadId}----结束");
  24. }
  25. });
  26. }
  27. }
  28. }

3、当方法内部锁定一个引用字符串,主线程锁定一个引用字符串时(前提是两个字符串内容一样),两者不能并发。(因为字符串是享元的,所以在内存中就是一个字符串。)

例子:(不能并发)

  1. //主线程
  2. public static readonly string LOCK = "哗哗";
  3. static void Main(string[] args)
  4. {
  5. TaskTest taskTest = new TaskTest();
  6. taskTest.TT("大");
  7. for (int i = 0; i < 5; i++)
  8. {
  9. Task.Run(() =>
  10. {
  11. lock (LOCK)
  12. {
  13. Console.WriteLine($"很 线程:{Thread.CurrentThread.ManagedThreadId}----开始");
  14. Thread.Sleep(20);
  15. Console.WriteLine($"很 线程:{Thread.CurrentThread.ManagedThreadId}----结束");
  16. }
  17. });
  18. }
  19. Console.Read();
  20. //自定义类
  21. class TaskTest
  22. {
  23. public readonly string LOCK = "哗哗";
  24. public void TT(string name)
  25. {
  26. for (int i = 0; i < 5; i++)
  27. {
  28. Task.Run(() =>
  29. {
  30. lock (LOCK)
  31. {
  32. Console.WriteLine($"任务:{name}-----线程:{Thread.CurrentThread.ManagedThreadId}----开始");
  33. Thread.Sleep(2000);
  34. Console.WriteLine($"任务:{name}-----线程:{Thread.CurrentThread.ManagedThreadId}----结束");
  35. }
  36. });
  37. }
  38. }
  39. }

4、当我调用一个泛型的类的时候,传递泛型参数一样不能并发,传递的泛型参数不一样,能并发。(泛型类,只要传递的泛型一样,就是同一个类。传递的泛型参数不同时,就是不同的类。)

例子:(taskTest与taskTest2之间不能并发,taskTest与taskTest3可以并发)

  1. //主方法
  2. static void Main(string[] args)
  3. {
  4. TaskTest<int> taskTest = new TaskTest<int>();
  5. TaskTest<int> taskTest2 = new TaskTest<int>();
  6. TaskTest<string> taskTest3 = new TaskTest<string>();
  7. taskTest.TT("大");
  8. taskTest2.TT("小");
  9. taskTest2.TT("中");
  10. Console.Read();
  11. }
  12. //自定义
  13. class TaskTest<T>
  14. {
  15. public static readonly object LOCK = new object();
  16. public void TT(string name)
  17. {
  18. for (int i = 0; i < 5; i++)
  19. {
  20. Task.Run(() =>
  21. {
  22. lock (LOCK)
  23. {
  24. Console.WriteLine($"任务:{name}-----线程:{Thread.CurrentThread.ManagedThreadId}----开始");
  25. Thread.Sleep(2000);
  26. Console.WriteLine($"任务:{name}-----线程:{Thread.CurrentThread.ManagedThreadId}----结束");
  27. }
  28. });
  29. }
  30. }
  31. }

5、当锁住的是关键字this时,是可以并发的。(this关键字指向当前对象,每一次实例化的对象是不一样的。所以锁是不一样的,可以并发)。

  1. class TaskTest
  2. {
  3. public static readonly object LOCK = new object();
  4. public void TT(string name)
  5. {
  6. for (int i = 0; i < 5; i++)
  7. {
  8. Task.Run(() =>
  9. {
  10. lock (this)
  11. {
  12. Console.WriteLine($"任务:{name}-----线程:{Thread.CurrentThread.ManagedThreadId}----开始");
  13. Thread.Sleep(2000);
  14. Console.WriteLine($"任务:{name}-----线程:{Thread.CurrentThread.ManagedThreadId}----结束");
  15. }
  16. });
  17. }
  18. }
  19. }
  20. //主线程
  21. static void Main(string[] args)
  22. {
  23. TaskTest taskTest = new TaskTest();
  24. TaskTest taskTest2 = new TaskTest();
  25. taskTest.TT("大");
  26. taskTest2.TT("小");
  27. Console.Read();
  28. }

7、深度判断是否会发生死锁—-答案:不会。应该和栈有关。

class TaskTest
    {
        public static readonly object LOCK = new object();

        private int _NUM = 0;
        public void TT(string name)
        {
            for (int i = 0; i < 5; i++)
            {
                this._NUM++;
                int k = i;
                lock (this)
                {
                    Console.WriteLine(this.GetHashCode());
                    Console.WriteLine($"任务:{name}-----线程:{Thread.CurrentThread.ManagedThreadId}----开始");
                    Thread.Sleep(2000);
                    Console.WriteLine($"任务:{name}-----线程:{Thread.CurrentThread.ManagedThreadId}----结束");

                    if (this._NUM<5)
                    {
                        this.TT(name);
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }
    }