PageSourceSearch

https://shenao1100.github.io/assets/2024-11-12-csharp-multi-thread-Be7cYOZs.js

js shenao1100.github.io collected 2026-10-03 09:56:31 UTC 15,552 bytes, 486 lines download raw bytes

1const n=`---
2title: C#多线程入门
3description: C#多线程常用操作
4author: ShenNya
5date: 2024-11-13 17:01:00 +0800
6categories: [CSharp]
7tags: [CSharp, 多线程, Thread, 并发, 并行]
8#pin: true
9math: true
10mermaid: true
11---
12
13# C#多线程入门
14
15## 前言
16> 注:Python3.13去掉了Gil,已经拥有了真正的多线程
17
18在使用Python写了几年 **虚假的** 多线程之后,想使用C#进行真正的多线程操作
19
20问题的起因是遇到了高并发场景,同时需要控制线程对唯一文件IO进行操作,于是就有了这篇文章
21
22如果你想深入了解更多的线程操作和同步机制,请点[这里](/posts/csharp-multi-thread-advance/)
23
24后期应该会写一个实战,~~但不是现在 [Soon(TM)]~~ 已经[写完了](/posts/csharp-multi-thread-download-file/)
25
26
27## 最简单的Task
28
29\`\`\`csharp
30using System;
31using System.Threading.Tasks;
32
33class Program
34{
35  static void Main()
36  {
37    Task.Run(() => DoWork(1));
38    Task.Run(() => DoWork(2));
39  }
40  
41  static void DoWork(int id)
42  {
43    Console.WriteLine($"{id} is started");
44    Thread.Sleep(1000);
45    Console.WriteLine($"{id} is done");
46  }
47}
48\`\`\`
49## 异步
50
51常用于UI交互,处理长时任务时不会造成线程假死
52
53\`\`\`csharp
54using System;
55using System.Net.Http;
56using System.Threading.Tasks;
57
58class Program
59{
60    static async Task Main()
61    {
62        Console.WriteLine("开始下载...");
63        await DownloadDataAsync();
64        Console.WriteLine("下载完成");
65    }
66
67    static async Task DownloadDataAsync()
68    {
69        using (HttpClient client = new HttpClient())
70        {
71            string data = await client.GetStringAsync("https://example.com"); // 阻塞等待下载完成
72            Console.WriteLine(data);
73        }
74    }
75}
76
77\`\`\`
78## 并发
79
80强调任务交替执行,没有控制任务在哪个核心上执行,也不一定所有任务都会同时执行
81
82\`\`\`csharp
83using System;
84using System.Collections.Generic;
85using System.Threading.Tasks;
86
87class Program
88{
89    static async Task Main(string[] args)
90    {
91        List<Task> tasks = new List<Task>();
92
93        for (int i = 1; i <= 5; i++)
94        {
95            int taskId = i;
96            tasks.Add(Task.Run(() => DoWorkAsync(taskId)));
97        }
98
99        await Task.WhenAll(tasks);
100
101        Console.WriteLine("所有任务已完成!");
102    }
103
104    static async Task DoWorkAsync(int id)
105    {
106        Console.WriteLine($"任务{id}开始");
107        await Task.Delay(1000 * id);  // 模拟异步操作
108        Console.WriteLine($"任务{id}完成");
109    }
110}
111\`\`\`
112是不是感觉并发和并行在代码上很相似?你可以在[这里](#并行并发异步的区别)找到区别
113
114## 并行
115
116充分利用多核CPU,确保多个任务能够同时执行
117
118> 注:Parallel本身是基于线程池的执行工具 (基于ThreadPool调度管理线程) ,你可以在[这里](#线程池)找到有关线程池的信息
119
120
121\`\`\`csharp
122using System;
123using System.Threading.Tasks;
124
125class Program
126{
127    static void Main(string[] args)
128    {
129        Console.WriteLine("并行计算开始");
130
131        // 使用 Parallel.For 并行计算
132        Parallel.For(1, 11, i =>
133        {
134            int result = i * i;
135            Console.WriteLine($"数字 {i} 的平方是: {result} (线程 {Task.CurrentId})");
136        });
137
138        Console.WriteLine("并行计算结束");
139    }
140}
141
142\`\`\`
143
144是不是感觉并发和并行在代码上很相似?你可以在[这里](#并行并发异步的区别)找到区别
145
146## 并行、并发、异步的区别
147
148> 这里把异步一起提到是因为很多人接触图形界面编程时更熟悉异步,而并不了解他
149
150### 区别
151
152**并行**
153
154多个任务真正同时执行。并行通常需要多核处理器或多台计算机。每个任务运行在独立的核心或处理器上 
155
156**并发**
157
158多个任务在同一时间段内交替执行。例如,仅单核处理但可以在不同任务之间快速切换,给人一种“同时进行”的感觉。
159
160**异步**
161
162异步是一种*编程模型*,指的是任务不需要等待其他任务完成就可以继续执行。异步任务通常会在等待某个操作(如 I/O 操作或网络请求)完成时释放控制权,让程序可以去做其他事情,等到操作完成后再继续
163
164---
165
166某种程度上说,异步是并发的一种实现方式,我认为原因如下:
167- 异步是一种并发模型
168- 异步本质上是一种非阻塞的任务处理
169- 并发强调交替进行,而并行则是同时进行,异步是通过非阻塞的方式实现并发的
170- 并发是一种任务管理思想,可以通过多线程、多进程、协程等方式来实现,而异步是其中一种方式
171
172因此 **并发 ≠ 并行**,而 **异步 ⊂ 并发**
173
174由此我们可以得到下面的表格 (理论上异步不应该在表格里):
175
176| 方式 | 类型           | 实现方式                                      | 特点                       |
177| ---- | -------------- | --------------------------------------------- | -------------------------- |
178| 并行 | 多进程         | 在多个处理器或核心上运行多个进程              | 并行依赖硬件               |
179|      | 多线程         | 多核 CPU 上运行多个线程,每个线程执行不同任务 |                            |
180| 并发 | 多线程并发     | 同一个进程中创建多个线程,交替执行            | 并发是一种任务管理方式     |
181|      | 多进程并发     | 通过多进程方式交替执行多个任务                |                            |
182|      | 协程           | 在一个线程内非阻塞地切换任务                  |                            |
183| 异步 | 事件异步       | 使用事件触发任务执行                          | 异步是一种非阻塞的编程模型 |
184|      | Async/Await    | 非阻塞的 I/O 操作                             |                            |
185|      | Future/Promise | 表示即将完成的操作                            |                            |
186
187
188## 线程阻塞
189
190> 线程阻塞适合那些比较强调\`顺序\`的操作,如等待用户输入, 定时任务, 等待另一个线程完成某项任务后才能继续执行等
191>
192> 他与线程锁的区别你可以在[这里](#线程锁和线程阻塞的区别)看到
193{: .prompt-info }
194
195\`\`\`csharp
196using System;
197using System.Threading;
198
199class Program
200{
201    // 创建一个 ManualResetEvent 实例,初始状态为未触发(即阻塞状态)
202    private static ManualResetEvent resetEvent = new ManualResetEvent(false);
203
204    static void Main(string[] args)
205    {
206        Console.WriteLine("主线程启动。");
207
208        // 启动一个新线程
209        Thread workerThread = new Thread(Worker);
210        workerThread.Start();
211
212        Console.WriteLine("按任意键解除阻塞...");
213        Console.ReadKey();
214
215        // 解除阻塞,使工作线程继续
216        resetEvent.Set();
217
218        // 等待工作线程结束
219        workerThread.Join();
220
221        Console.WriteLine("主线程结束。");
222    }
223
224    // 工作线程的执行方法
225    static void Worker()
226    {
227        Console.WriteLine("工作线程启动,等待主线程解除阻塞...");
228
229        // 阻塞,等待 ManualResetEvent 被触发
230        resetEvent.WaitOne();
231
232        Console.WriteLine("工作线程继续运行...");
233
234        // 模拟一些工作
235        Thread.Sleep(2000);
236
237        Console.WriteLine("工作线程结束。");
238    }
239}
240
241\`\`\`
242
243## 线程加锁
244
245> 线程锁适合需要确保\`数据一致性\`的操作,如下面的文件IO操作,他与线程阻塞的区别你可以在[这里](#线程锁和线程阻塞的区别)看到
246{: .prompt-info }
247
248\`\`\`csharp
249using System;
250using System.IO;
251using System.Threading;
252using System.Threading.Tasks;
253
254class Program
255{
256    private static readonly object fileLock = new object();
257    private static string filePath = "example.txt";
258
259    static async Task Main(string[] args)
260    {
261        // 启动多个线程同时写入文件
262        Task task1 = Task.Run(() => WriteToFile("线程1的数据"));
263        Task task2 = Task.Run(() => WriteToFile("线程2的数据"));
264        
265        await Task.WhenAll(task1, task2);
266        
267        Console.WriteLine("文件写入完成。");
268    }
269
270    static void WriteToFile(string content)
271    {
272        lock (fileLock) // 加锁,确保只有一个线程能进入
273        {
274            using (StreamWriter writer = new StreamWriter(filePath, true)) // 追加模式
275            {
276                writer.WriteLine(content);
277            }
278        }
279    }
280}
281\`\`\`
282
283
284## 线程锁和线程阻塞的区别
285
286|          | 线程锁                                                        | 线程阻塞                     |
287| -------- | ------------------------------------------------------------- | ---------------------------- |
288| 定义     | 控制线程对资源的\`独占\`访问<br/>(同时只能有一个线程在访问资源) | 让线程暂停执行,等待条件满足 |
289| 目的     | 保护共享资源的完整性                                          | 控制线程执行的顺序           |
290| 触发条件 | 线程试图访问已上锁的资源                                      | 等待某个条件、资源、时间     |
291| 影响     | 同时只允许一个线程访问资源                                    | 当前线程停止运行,释放CPU    |
292
293
294## 线程池
295
296**目的**: 管理和复用线程,减少频繁创建和销毁线程带来的开销
297
298适用于 I/O 密集型任务、短时间大量的的并行任务、频繁执行的任务
299
300> ***在C#中,线程的创建、销毁和复用都是自动管理的, 这会引出下面的问题***
301{: .prompt-info }
302
303> **线程池中的线程数量是有限的**,默认会根据系统的 CPU 核心数自动调节, 你可以通过 \`ThreadPool.SetMinThreads\` 和 \`ThreadPool.SetMaxThreads\` 设置最小和最大线程数
304>
305> **线程池中的线程由系统自动管理**,开发者无法直接控制线程的启动和终止。这适合短时间任务,但不适合需要精确控制线程生命周期的场景
306>
307> **线程池主要设计用于处理短小的任务**,不适合长时间运行的任务。长时间运行的任务可能会占用线程池中的线程,导致其他任务无法及时获得线程执行。
308{: .prompt-warning }
309
310
311有三种方法可以使用线程池:
312
313### ThreadPool.QueueUserWorkItem
314
315\`\`\`csharp
316using System;
317using System.Threading;
318
319class Program
320{
321    static void Main()
322    {
323        // 提交任务到线程池
324        ThreadPool.QueueUserWorkItem(DoWork, "任务1");
325        ThreadPool.QueueUserWorkItem(DoWork, "任务2");
326
327        Console.WriteLine("任务已提交到线程池");
328        Console.ReadLine(); // 防止程序立即退出
329    }
330
331    static void DoWork(object state)
332    {
333        string taskName = state as string;
334        Console.WriteLine($"{taskName} 正在执行,线程ID: {Thread.CurrentThread.ManagedThreadId}");
335        Thread.Sleep(1000); // 模拟任务耗时
336        Console.WriteLine($"{taskName} 执行完成");
337    }
338}
339\`\`\`
340
341### Task
342
343\`\`\`csharp
344using System;
345using System.Threading.Tasks;
346
347class Program
348{
349    static async Task Main()
350    {
351        // 使用 Task 提交任务到线程池
352        Task task1 = Task.Run(() => DoWork("任务1"));
353        Task task2 = Task.Run(() => DoWork("任务2"));
354
355        await Task.WhenAll(task1, task2);
356
357        Console.WriteLine("所有任务完成");
358    }
359
360    static void DoWork(string taskName)
361    {
362        Console.WriteLine($"{taskName} 正在执行,线程ID: {Thread.CurrentThread.ManagedThreadId}");
363        Task.Delay(1000).Wait(); // 模拟任务耗时
364        Console.WriteLine($"{taskName} 执行完成");
365    }
366}
367\`\`\`
368
369### Parallel
370
371此方法在[上文](#并行)有提到过
372
373\`\`\`csharp
374using System;
375using System.Threading.Tasks;
376
377class Program
378{
379    static void Main()
380    {
381        // 使用 Parallel.For 并行执行任务
382        Parallel.For(0, 5, i =>
383        {
384            Console.WriteLine($"任务 {i} 正在执行,线程ID: {Thread.CurrentThread.ManagedThreadId}");
385            Task.Delay(500).Wait(); // 模拟任务耗时
386            Console.WriteLine($"任务 {i} 执行完成");
387        });
388
389        Console.WriteLine("所有任务完成");
390    }
391}
392\`\`\`
393
394## 取消线程
395
396在需要让某一线程停止工作时,由于可能会发生资源泄露、不一致(反正就是会出问题)一般不推荐销毁线程,解决方法是使用**取消请求**
397
398\`\`\`csharp
399using System;
400using System.Threading;
401
402class Program
403{
404    static void Main()
405    {
406        // 创建取消信号源
407        CancellationTokenSource cts = new CancellationTokenSource();
408
409        // 启动线程并传入取消令牌
410        Thread thread = new Thread(() => DoWork(cts.Token));
411        thread.Start();
412
413        // 等待一段时间再取消线程
414        Thread.Sleep(2000);
415        Console.WriteLine("请求取消线程...");
416        cts.Cancel();
417
418        // 等待线程完成
419        thread.Join();
420        Console.WriteLine("线程已终止");
421    }
422
423    static void DoWork(CancellationToken token)
424    {
425        Console.WriteLine("线程开始执行...");
426        for (int i = 0; i < 10; i++)
427        {
428            // 检查是否有取消请求
429            if (token.IsCancellationRequested)
430            {
431                Console.WriteLine("取消请求已收到,线程即将退出...");
432                return;
433            }
434
435            // 模拟工作
436            Console.WriteLine($"正在处理数据 {i}...");
437            Thread.Sleep(1000);
438        }
439        Console.WriteLine("线程正常结束");
440    }
441}
442
443\`\`\`
444
445## 终止线程
446
447> ***在C#中,终止线程可能会导致不一致的状态,因此在.NET Core和.Net5之后已被弃用***
448{: .prompt-danger }
449
450由于已经弃用且不推荐使用,这里只说一下方法名
451\`\`\`csharp
452Thread.Abort()
453\`\`\`
454
455## 如何使用并发和并行
456
457在Web开发中,常会听到人们在强调高并发而非高并行
458
459我们就以这个为例子来讲一下什么时候用并发,什么时候用并行
460
461### 工作负载
462
463工作负载常为**I/O密集型**和**CPU密集型**任务
464
465### I/O密集型
466
467大部分 Web 请求的操作包括数据库查询、文件读取、网络通信等,主要是**I/O 操作**而非**复杂的计算**
468
469在处理这些操作时,处理的等待时间通常是瓶颈,而非计算能力
470
471因此 Web 开发更关注如何处理大量的并发请求,以便在多个请求之间有效利用资源,而不是提高单个请求的执行速度
472
473### CPU密集型
474
475科学计算、图像处理等任务需要充分利用CPU多核性能,这样可以利用多个核心运行多个任务来加速计算结果
476
477### I/O密集型任务常用并发,CPU密集型任务常用并行
478
479由此可见,像Web服务这种对算力要求更低,但是涉及到很多请求的**I/O密集型**任务,使用**并发**是个更好的选择,在单个核心内能够同时处理更多的任务
480
481在高并发环境中,资源的复用和高效调度是关键。现代 Web 服务器倾向ä
481ºŽä½¿ç”¨çº¿ç¨‹æ± ã€åç¨‹ç­‰æœºåˆ¶æ¥æŽ§åˆ¶èµ„源消耗。如果每个请求都创建一个线程,系统资源会迅速耗尽;相反,通过并发编程模型,可以让少量的线程处理大量的请求,提升服务器的资源使用效率
482
483---
484
485像科学计算这种需要大量算力的**CPU密集型**任务,使用**并行**是个更好的选择,可以充分利用多核CPU的全部性能
486`;export{n as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.