CodeGym /课程 /C# SELF /多线程入门

多线程入门

C# SELF
第 55 级, 课程 0
可用

1. 介绍

多线程就像办公室里几个人并行工作:一个打印文件,另一个给客户打电话,第三个煮咖啡(当然,他们都是程序员)。如果只有一个人,那他得按顺序做所有事,办公室会被无聊和排队的咖啡淹没。在编程里也是类似:单线程程序一次只能做一件事。

假设我们的应用在执行一个很耗时的操作,比如从网络下载文件或计算一个超大的表格。其他所有东西在那一刻都会“冻结”——按钮点不了,动画不动,弹窗会显示 "没有响应"。

多线程让应用能同时做多件事:界面保持响应,操作并行运行,我们不用再对着电脑说“电脑,你又卡了?!”。

基本概念和术语

在深入之前,弄清楚什么是线程 (thread),它和进程的区别。

  • 进程 (Process): 独立运行的程序,有自己的地址空间、变量和资源。例如,在 Windows 中每个启动的应用就是一个进程。
  • 线程 (Thread): 进程内的执行单元。一个进程可以包含一个或多个线程,它们共享同样的资源(内存、变量)。

为什么多线程会引出这么多问题?

因为线程是爱冒险的家伙:它们可以在任何时刻开始执行、打乱数据、打断彼此,如果不按顺序处理,会在内存里制造混乱。如果感觉这像没有管理员的幼儿园——完全正确!纪律和对线程的照顾,是写出可靠多线程程序的基础。

2. C# 和 .NET 中多线程的历史和角色

很久以前 C# 是单线程的,程序很简单。随着对性能的需求增长、多核处理器的出现以及需要构建不冻结界面的响应式应用,.NET 引入了多线程支持。最初是经典的 System.Threading.Thread,后来出现了任务 (Task)、异步方法 (async/await), 并行数据处理 (PLINQ) 以及更高级的同步原语。

C# 已成长为一个强大的平台,多线程不是稀罕事而是日常。

直观:进程和线程

下面是一个简单示意图:


+--------------------------------------------------+
|                 进程(你的程序)                 |
|      +-------------+   +-------------+           |
|      |   线程 1    |   |   线程 2   |           |
|      +-------------+   +-------------+           |
|                ...                                 |
|      +-------------+                              |
|      |   线程 N    |                              |
|      +-------------+                              |
+--------------------------------------------------+

进程内的所有线程能看到共同的变量和资源。

3. 在 C# 中如何创建线程?

从最基础的开始:来自命名空间 System.Threading 的 Thread 类。

示例:启动第二个线程

假设我们有个耗时任务——比如计算从 1 到 10_000_000 的和。计算进行时,主线程向用户打招呼。


using System;
using System.Threading;

class Program
{
    // 用于第二个任务的方法
    static void CalculateSum()
    {
        long sum = 0;
        for (int i = 1; i <= 10_000_000; i++)
            sum += i;
        Console.WriteLine($"[线程 2] 和: {sum}");
    }

    static void Main()
    {
        // 创建线程,指定方法的委托
        Thread thread = new Thread(CalculateSum);
        
        thread.Start(); // 启动第二个线程

        // 主线程继续工作
        Console.WriteLine("[线程 1] 你好!我们并行工作...");

        // 在退出前等待第二个线程完成
        thread.Join();

        Console.WriteLine("[线程 1] 全部完成!");
    }
}

会发生什么?
屏幕上会出现 "[线程 1] 你好!我们并行工作...",然后当计算线程结束时,会输出计算结果。

典型问题:谁先输出

多次运行这段代码你会看到输出顺序可能不同!有时和先出现,有时问候先出现。这就是多线程——程序变得不那么可预测,像周一猫的心情一样。

4. 内存区域:线程能看到什么?

进程内的所有线程都可以访问相同的变量(如果它们不是方法的局部变量)。在一个线程中改变变量,其他线程也会看到!

示例:共享变量


using System;
using System.Threading;

class Program
{
    static int counter = 0;

    static void Increment()
    {
        for (int i = 0; i < 1000; i++)
            counter++;
    }

    static void Main()
    {
        Thread t1 = new Thread(Increment);
        Thread t2 = new Thread(Increment);

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine($"counter = {counter}");
    }
}

我们期望在 counter 中看到多少?逻辑上应该是 2000,因为每个线程增加 1000 次。

但并不是!多次运行会得到不同的值:1782、1935、1999……
为什么?这是经典的 Race Condition(竞态条件)——线程在“读 -> 增加 -> 写”这个序列中互相抢占,部分自增丢失了。

5. 有用的细节

线程如何与界面交互?

在现代桌面应用(WinForms/WPF/MAUI)中,主线程负责图形界面。所有用户操作(点击、输入)都在这个线程上。后台任务应该在其他线程执行,但通常不允许直接从后台线程“碰”界面。这是为了避免混乱。

在控制台程序里没有这种限制,可以从任何线程写 Console.WriteLine。不过在真实应用中,如果没有正确同步,界面还是可能出问题。

顺序性和并行性

用表格来巩固区别。

单线程代码 多线程代码
按顺序执行任务 任务可以同时执行
长任务会让 UI “卡住” UI 保持响应
读写变量很简单 需要控制对数据的访问
容易调试 调试可能很复杂

与线程工作时的重要点

  • 共享变量 = 共享风险。 如上所示,多个线程用同一个变量时,如果不做同步会出错!(更详细内容在后面的讲座。)
  • 可以用 Join() 等待线程。 Join() 方法可以暂停主线程直到后台线程完成。需要等待结果时就用它。
  • 线程不能启动两次。 线程执行完后不能再次 Start() —— 需要创建新的 Thread 对象。
  • 线程的结束。 线程在其方法执行完时结束。强行“杀死”线程通常是坏主意(像 Abort() 这样的做法早已被认为有害且过时)。

多线程在实践中的用途

  • UI 应用:当后台加载或计算时不让界面冻结。
  • 服务器和服务:同时处理多个客户端请求。
  • 高性能计算:把大任务(比如处理上百万条记录)分成多份并行执行。
  • 游戏、模拟、数据处理:模拟复杂系统同时保持性能。

多线程的问题

多线程带来力量,但也增加复杂性:

  • Race Condition(竞态条件): 当多个线程同时修改相同数据时,结果依赖于指令执行顺序,这是不可预测的。
  • Deadlock(死锁): 线程相互等待对方,谁也无法继续。
  • Starvation(饥饿): 某个线程长期得不到资源访问。

在这节课我们只是提到多线程的主要问题,接下来的课会学会识别和避免它们。先记住一句话:如果一切都“卡住”了,不一定只是bug,也可能是线程在开派对。

2
任务
C# SELF, 第 55 级, 课程 0
已锁定
创建和启动线程
创建和启动线程
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION