编程博客
002、修改Nuget包位置
001、C#常用的单词
003、收藏的书签
回家准备
004、ASP.NET Core 3.0 gRPC
001、ASP.NET Core 3.0 使用gRPC
002、ASP.NET Core 3.0 gRPC 双向流
003、ASP.NET Core 3.0 gRPC 身份认证和授权
005、飞牛NAS
001、 FnOS飞牛系统实用配置1-应用远程访问(迅雷)
006、DeepSeek
001、DeepSeek 使用指南
007、并发编程
01、并发编程 - 死锁的产生、排查与解决方案
02、并发编程 - 初识线程
03、并发编程 - 线程浅试
04、并发编程 - 线程同步(一)
05、并发编程 - 线程同步(二)
06、并发编程 - 线程同步(三)之原子操作 Interlocked 简介
07、并发编程 - 线程同步(四)之原子操作 Interlocked 详解一
08、并发编程 - 线程同步(五)之原子操作 Interlocked 详解二
09、并发编程 - 线程同步(六)之锁 lock
008、启动项
03、应用--Program 中的 WebApplication
02、主机--Host
01、Program 文件的作用
04、控制反转 IOC 与依赖注入 DI
05、中间件
06、Logger 原理及配置 Log4Net
07、ElasticSearch
本文档使用 MrDoc 发布
-
+
首页
03、并发编程 - 线程浅试
前面已经对线程有了初步认识,下面我们来尝试使用线程。  # ***01***、线程创建 在 C#中创建线程主要是通过 Thread 构造函数实现,下面讲解 3 种常见的创建方式。 ## 1、通过 ThreadStart 创建 Thread 有一个带有 ThreadStart 类型参数的构造函数,其中参数 ThreadStart 是一个无参无返回值委托,因此我们可以创建一个无参无返回值方法传入 Thread 构造函数中,代码如下: ```csharp public class ThreadSample { public static void CreateThread() { Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}"); var thread = new Thread(BusinessProcess); thread.Start(); } //线程1 public static void BusinessProcess() { Console.WriteLine($"BusinessProcess 线程Id:{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine("开始处理业务……"); //业务实现 Console.WriteLine("结束处理业务……"); } } ``` 代码也相当简单,我们在主线程中通过 Thread 创建了一个新的线程用来运行 BusinessProcess 方法,同时通过 Thread.CurrentThread.ManagedThreadId 打印出当前线程 Id。 代码执行结果如下,主线程 Id 和业务线程 Id 并不相同。  ## 2、通过 ParameterizedThreadStart 带参创建 Thread 还有一个带有 ParameterizedThreadStart 类型参数的构造函数,其中参数 ParameterizedThreadStart 是一个有参无返回值委托,其中参数为 object 类型,因此我们可以创建一个有参无返回值方法传入 Thread 构造函数中,然后通过 Thread.Start 方法把参数传递给线程,代码如下: ```csharp public static void CreateThreadParameterized() { Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}"); var thread = new Thread(BusinessProcessParameterized); //传入参数 thread.Start("Hello World!"); } //带参业务线程 public static void BusinessProcessParameterized(object? param) { Console.WriteLine($"BusinessProcess 线程Id:{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"参数 param 为:{param}"); Console.WriteLine("开始处理业务……"); //业务实现 Console.WriteLine("结束处理业务……"); } ``` 我们看看代码执行结果:  该方式有个限制,因为 ParameterizedThreadStart 委托参数为 object 类型,因此我们的业务方法也必须要用 object 类型接收参数,然后再根据实际类型进行转换。 ## 3、通过 Lambda 表达式创建 通过上面可以知道无论 ThreadStart 还是 ParameterizedThreadStart 本质上都是一个委托,因此我们可以直接使用 Lambda 表达式直接构建一个委托。可以看看以下代码: ```csharp public static void CreateThreadLambda() { Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}"); var thread = new Thread(() => { Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine("开始处理业务……"); //业务实现 Console.WriteLine("结束处理业务……"); }); //传入参数 thread.Start(); } ``` 代码执行结果如下:  因为 Lambda 表达式可以直接访问外部作用域中的变量,因此线程传参还可以使用 Lambda 表达式来实现。 但是这也导致了一些问题,比如下面代码执行结果应该是什么?先自己想想看。 ```csharp public static void CreateThreadLambdaParameterized() { Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}"); var param = "Hello"; var thread1 = new Thread(() => BusinessProcessParameterized(param)); thread1.Start(); param = "World"; var thread2 = new Thread(() => BusinessProcessParameterized(param)); thread2.Start(); } //带参业务线程 public static void BusinessProcessParameterized(string param) { Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"参数 param 为:{param}"); } ``` 看看执行结果:  和你想想的结果一样吗? 这是因为当在 Lambda 表达式中使用任何外部局部变量时,编译器会自动生成一个类,并将该变量作为该类的一个属性。因此这些外部变量并不是存储在栈中,而是通过引用存储在堆中,因此此时 param 参数实际上在内存中是一个类是一个引用类型,所以两个线程中使用的 param 都指向了堆中的同一个值。 并且使用 Lambda 表达式引用另一个 C#对象的方式有个专有名词叫闭包。感兴趣的可以去了解下闭包概念。 # ***02***、线程休眠 可以通过 Sleep 方法暂停当前线程,使其处于休眠状态,以尽可能少的占用 CPU 时间。看如下示例代码,通过在 Sleep 方法前后打印出当前时间对比,来观察暂停线程效果。 ```csharp public static void ThreadSleep() { Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}"); var thread = new Thread(() => { Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"暂停线程前:{DateTime.Now:HH:mm:ss}"); //暂停线程10秒 Thread.Sleep(10000); Console.WriteLine($"暂停线程后:{DateTime.Now:HH:mm:ss}"); }); thread.Start(); thread.Join(); } ``` 代码执行结果如下:  可以发现暂停线程前后正好差了 10 秒钟。 # ***03***、线程等待 线程等待指让程序等待另一个需要长时间计算的线程运行完成后,再继续后面操作。而使用 Thread.Sleep 方法并不能满足需求,因为当前并不知道执行计算到底需要多少时间,因此可以使用 Thread.Join。如上一小节中代码,当代码执行到 Thread.Join 方法时,则线程会处于阻塞状态,只有线程执行完成后才会继续往下执行。具体示例可以看上一小节。 # ***04***、线程其他方法 此外线程还有暂停、恢复、中断、终止等线程方法,这里就不介绍了,因为一些方法已经弃用没有必要再花经历学习了。 # ***05***、异常处理 对于线程中的异常需要特别注意,对于一个 Thread 子线程所产生的异常,默认情况下主线程并不能捕捉到,可以查看下面示例: ```csharp public static void ThreadException() { Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}"); try { var thread = new Thread(ThreadThrowException); thread.Start(); } catch (Exception ex) { Console.WriteLine("子线程异常信息:" + ex.Message); } } //业务线程不处理异常,直接抛出 public static void ThreadThrowException() { Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine("开始处理业务……"); //业务实现 Console.WriteLine("结束处理业务……"); throw new Exception("异常"); } ``` 运行结果如下:  可以看到在主线程中并没有捕捉到子线程抛出的异常,而导致程序直接中断。因此我们在处理线程异常时需要特别注意,可以直接在线程中处理异常。 # ***06***、何时应该使用线程 线程有很多优点,但也并不是万能的,因为每一个线程都会产生大量的资源消耗,包括:占用大量内存空间,线程的创建、销毁和管理,线程之间的上下文切换,以及垃圾回收的消耗。 举个简单例子,比如一个小餐馆,有一个厨师,一个下单员,客户下单给下单员,下单员把客户下的菜单传递给厨师。假如现在客户很多一个下单员忙不过来,老板决定再添加一个下单员,此时下单的效率可以提升一倍,但是厨师还是一个,那么就会导致当厨师和 A 下单员交接的时候,B 下单员只能等着,并且因为之前厨师和 A 下单员长时间合作形成了彼此默契,这是再和 B 下单员交接的时候效率可能并不高,因此最终整体效率并不一定提升多少。如果把厨师比作 CPU 处理器,下单员比作线程,如果要想餐馆的整体效率提升那么在增加下单员的时候,必须要相应的添加厨师,才能使得餐馆最大效率的提升。 因此并不是说无脑的添加线程就可以使得程序效率提升,需要按需使用。 比如在以下使用场景可以考虑使用多线程:文件多写、网络请求、数据库查询、图像处理、数据分析、定时任务等。
个人天使
2025年2月10日 10:19
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码