并发编程 - 线程浅试

三尺非寒  金牌会员 | 2025-1-17 18:28:35 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 893|帖子 893|积分 2679

前面已经对线程有了开端认识,下面我们来尝试使用线程。

01、线程创建

在C#中创建线程主要是通过Thread构造函数实现,下面讲授3种常见的创建方式。
1、通过ThreadStart创建

Thread有一个带有ThreadStart类型参数的构造函数,其中参数ThreadStart是一个无参无返回值委托,因此我们可以创建一个无参无返回值方法传入Thread构造函数中,代码如下:
  1. public class ThreadSample
  2. {
  3.     public static void CreateThread()
  4.     {
  5.         Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
  6.         var thread = new Thread(BusinessProcess);
  7.         thread.Start();
  8.     }
  9.     //线程1
  10.     public static void BusinessProcess()
  11.     {
  12.         Console.WriteLine($"BusinessProcess 线程Id:{Thread.CurrentThread.ManagedThreadId}");
  13.         Console.WriteLine("开始处理业务……");
  14.         //业务实现
  15.         Console.WriteLine("结束处理业务……");
  16.     }
  17. }
复制代码
代码也相当简单,我们在主线程中通过Thread创建了一个新的线程用来运行BusinessProcess方法,同时通过Thread.CurrentThread.ManagedThreadId打印出当前线程Id。
代码执行结果如下,主线程Id和业务线程Id并不雷同。

2、通过ParameterizedThreadStart带参创建

Thread另有一个带有ParameterizedThreadStart类型参数的构造函数,其中参数ParameterizedThreadStart是一个有参无返回值委托,其中参数为object类型,因此我们可以创建一个有参无返回值方法传入Thread构造函数中,然后通过Thread.Start方法把参数通报给线程,代码如下:
  1. public static void CreateThreadParameterized()
  2. {
  3.     Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
  4.     var thread = new Thread(BusinessProcessParameterized);
  5.     //传入参数
  6.     thread.Start("Hello World!");
  7. }
  8. //带参业务线程
  9. public static void BusinessProcessParameterized(object? param)
  10. {
  11.     Console.WriteLine($"BusinessProcess 线程Id:{Thread.CurrentThread.ManagedThreadId}");
  12.     Console.WriteLine($"参数 param 为:{param}");
  13.     Console.WriteLine("开始处理业务……");
  14.     //业务实现
  15.     Console.WriteLine("结束处理业务……");
  16. }
复制代码
我们看看代码执行结果:

该方式有个限制,由于ParameterizedThreadStart委托参数为object类型,因此我们的业务方法也必须要用object类型吸取参数,然后再根据实际类型进行转换。
3、通过Lambda表达式创建

通过上面可以知道无论ThreadStart还是ParameterizedThreadStart本质上都是一个委托,因此我们可以直接使用Lambda表达式直接构建一个委托。可以看看以下代码:
  1. public static void CreateThreadLambda()
  2. {
  3.     Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
  4.     var thread = new Thread(() =>
  5.     {
  6.         Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}");
  7.         Console.WriteLine("开始处理业务……");
  8.         //业务实现
  9.         Console.WriteLine("结束处理业务……");
  10.     });
  11.     //传入参数
  12.     thread.Start();
  13. }
复制代码
代码执行结果如下:

由于Lambda表达式可以直接访问外部作用域中的变量,因此线程传参还可以使用Lambda表达式来实现。
但是这也导致了一些问题,比如下面代码执行结果应该是什么?先自己想想看。
  1. public static void CreateThreadLambdaParameterized()
  2. {
  3.     Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
  4.     var param = "Hello";
  5.     var thread1 = new Thread(() => BusinessProcessParameterized(param));
  6.     thread1.Start();
  7.     param = "World";
  8.     var thread2 = new Thread(() => BusinessProcessParameterized(param));
  9.     thread2.Start();
  10. }
  11. //带参业务线程
  12. public static void BusinessProcessParameterized(string param)
  13. {
  14.     Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}");
  15.     Console.WriteLine($"参数 param 为:{param}");
  16. }
复制代码
看看执行结果:

和你想想的结果一样吗?
这是由于当在Lambda 表达式中使用任何外部局部变量时,编译器会自动生成一个类,并将该变量作为该类的一个属性。因此这些外部变量并不是存储在栈中,而是通过引用存储在堆中,因此此时param参数实际上在内存中是一个类是一个引用类型,所以两个线程中使用的param都指向了堆中的同一个值。
并且使用Lambda表达式引用另一个C#对象的方式有个专有名词叫闭包。感兴趣的可以去相识下闭包概念。
02、线程休眠

可以通过Sleep方法暂停当前线程,使其处于休眠状态,以尽可能少的占用CPU时间。看如下示例代码,通过在Sleep方法前后打印出当前时间对比,来观察暂停线程结果。
  1. public static void ThreadSleep()
  2. {
  3.     Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
  4.     var thread = new Thread(() =>
  5.     {
  6.         Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}");
  7.         Console.WriteLine($"暂停线程前:{DateTime.Now:HH:mm:ss}");
  8.         //暂停线程10秒
  9.         Thread.Sleep(10000);
  10.         Console.WriteLine($"暂停线程后:{DateTime.Now:HH:mm:ss}");
  11.     });
  12.     thread.Start();
  13.     thread.Join();
  14. }
复制代码
代码执行结果如下:

可以发现暂停线程前后恰恰差了10秒钟。
03、线程等待

线程等待指让程序等待另一个必要长时间计算的线程运行完成后,再继续后面操纵。而使用Thread.Sleep方法并不能满足需求,由于当前并不知道执行计算到底必要多少时间,因此可以使用Thread.Join。如上一小节中代码,当代码执行到Thread.Join方法时,则线程会处于阻塞状态,只有线程执行完成后才会继续往下执行。具体示例可以看上一小节。
04、线程其他方法

此外线程另有暂停、恢复、中断、停止等线程方法,这里就不先容了,由于一些方法已经弃用没有须要再花经历学习了。
05、异常处理

对于线程中的异常必要特别注意,对于一个Thread子线程所产生的异常,默认环境下主线程并不能捕获到,可以查看下面示例:
  1. public static void ThreadException()
  2. {
  3.     Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
  4.     try
  5.     {
  6.         var thread = new Thread(ThreadThrowException);
  7.         thread.Start();
  8.     }
  9.     catch (Exception ex)
  10.     {
  11.         Console.WriteLine("子线程异常信息:" + ex.Message);
  12.     }
  13. }
  14. //业务线程不处理异常,直接抛出
  15. public static void ThreadThrowException()
  16. {
  17.     Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}");
  18.     Console.WriteLine("开始处理业务……");
  19.     //业务实现
  20.     Console.WriteLine("结束处理业务……");
  21.     throw new Exception("异常");
  22. }
复制代码
运行结果如下:

可以看到在主线程中并没有捕获到子线程抛出的异常,而导致程序直接中断。因此我们在处理线程异常时必要特别注意,可以直接在线程中处理异常。
06、何时应该使用线程

线程有许多优点,但也并不是万能的,由于每一个线程都会产生大量的资源斲丧,包罗:占用大量内存空间,线程的创建、销毁和管理,线程之间的上下文切换,以及垃圾回收的斲丧。
举个简单例子,比如一个小餐馆,有一个厨师,一个下单员,客户下单给下单员,下单员把客户下的菜单通报给厨师。假如现在客户许多一个下单员忙不过来,老板决定再添加一个下单员,此时下单的效率可以提升一倍,但是厨师还是一个,那么就会导致当厨师和A下单员交接的时间,B下单员只能等着,并且由于之前厨师和A下单员长时间合作形成了相互默契,这是再和B下单员交接的时间效率可能并不高,因此终极团体效率并不愿定提升多少。如果把厨师比作CPU处理器,下单员比作线程,如果要想餐馆的团体效率提升那么在增加下单员的时间,必须要相应的添加厨师,才能使得餐馆最大效率的提升。
因此并不是说无脑的添加线程就可以使得程序效率提升,必要按需使用。
比如在以下使用场景可以考虑使用多线程:文件多写、网络请求、数据库查询、图像处理、数据分析、定时任务等。
:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

三尺非寒

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表