Thread就是线程,我们在上文中已经给大家介绍了线程的等相关概念,不清楚的小伙伴可以看上文:.NET进阶------深入理解线程(1)同步异步与单线程多线程的区分今天我们来详细介绍一下多线程,多线程是.NET中非常重要的 - 掘金
本文紧接着上文来介绍关于Thread的相关知识。
一、如何创建线程
先思考一个问题:我们为什么要创建新的线程?、
原因无非是需要新开一个线程来完成特殊的方法或者函数,你可以让新开的子线程异步执行,也可以让它同步执行,这都取决于你。但是不变的是,你肯定需要让新开的线程执行一些函数或者方法,否则你根本不需要新开线程。那么如何让新开的线程知道我要执行什么方法呢?
我们需要用到委托!!!
1.1 创建线程的两种方式
我们可以用Thread类,创建两种类型的线程,分别是无参数无返回值的线程 ,以及有参数无返回值的线程 ,区别就是接收的参数不一样,一个是ThreadStart类型的委托,另一个是ParameterizedThreadStart先看一下Thread类的两个构造函数:
cs
// 无参构造方法
public Thread(ThreadStart start)
{
// 内部的代码我们不需要看
}
// 有参构造方法
public Thread(ParameterizedThreadStart start)
{
// 内部的代码我们不需要看
}
可见,在创建Thread线程之前,我们要先创建ThreadStart或者是ParameterizedThreadStart委托的实例,然后把实例传递到Thread中。
1.1.1 创建无参数无返回值的线程
创建无参数无返回值的线程,需要先创建一个符合ThreadStart委托的方法,也就是无参无返回值的方法,然后把方法装载到ThreadStart委托中,然后把委托实例放到Thread类里,作为构造函数的参数。
1. 什么是ThreadStart委托:
ThreadStart是.NET 内置的、无返回值、无参数 的委托,专门用来封装线程要执行的 "无参数方法"。它的底层定义非常简单:
cs
public delegate void ThreadStart();
翻译过来就是:这个委托只能绑定 "没有参数、返回值为 void" 的方法。
2. ThreadStart如何创建线程:
只要线程执行的方法没有参数,就用ThreadStart把方法装进去(实际开发中通常省略显式写ThreadStart,编译器会自动推断,但是我们需要知道执行原理):
cs
using System;
using System.Threading;
class Program
{
static void Main()
{
// 方式1:显式声明ThreadStart委托(新手易理解)
ThreadStart ts = new ThreadStart(DoWork);
Thread t1 = new Thread(ts);
// 方式2:简化写法(实际开发常用,编译器自动转ThreadStart)
Thread t2 = new Thread(DoWork);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
// 符合ThreadStart要求:无参数、返回void
static void DoWork()
{
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}执行无参方法");
}
}
1.1.2 创建有参数无返回值的线程
1. 什么是ParameterizedThreadStart委托 当我们需要新开一个线程,让这个新的线程去执行一个有参数无返回值的方法时,需要用到ParameterizedThreadStart委托,ParameterizedThreadStart是.NET 内置的、无返回值、带单个 object 参数 的委托,用来封装线程要执行的 "带参数方法"。底层定义:
cs
public delegate void ParameterizedThreadStart(object obj);
翻译过来就是:这个委托只能绑定 "有且仅有一个 object 类型参数、返回值为 void" 的方法。
2. ParameterizedThreadStart如何创建线程:
当线程执行的方法需要传参数时,就用这个委托,注意参数是object类型,内部使用需要装箱拆箱:
csharp
using System;
using System.Threading;
class Program
{
static void Main()
{
// 方式1:显式声明(新手易理解)
ParameterizedThreadStart pts = new ParameterizedThreadStart(DoWorkWithParam);
Thread t1 = new Thread(pts);
// 方式2:简化写法(实际开发常用)
Thread t2 = new Thread(DoWorkWithParam);
// 启动线程时传入参数(只能传一个object类型)
t1.Start("线程1的参数");
t2.Start(100); // 可以传任意类型,因为都是object的子类
t1.Join();
t2.Join();
}
// 符合ParameterizedThreadStart要求:单个object参数、返回void
static void DoWorkWithParam(object obj)
{
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}接收到参数:{obj},参数类型:{obj.GetType()}");
}
}
1.1.3 更常用更高级的写法
由于创建线程时需要的参数是一个委托,我们可以用Lambda表达式传递,如果还记得Lambda表达式吗,实际上就是一个匿名方法,
cs
namespace Advanced_Thread_03
{
public class Program
{
static void Main(string[] args)
{
Thread thread1 = new Thread(() =>
{
Console.WriteLine("我是由ThreadStart创建的线程");
});
Thread thread2 = new Thread((obj) =>
{
var student = obj as Student;
if (obj == null)
{
Console.WriteLine("参数为空!");
return;
}
Console.WriteLine($"我是由ParameterizedThreadStart创建的线程,{student.Name}年龄为:{student.Age}");
student.Age++;
Console.WriteLine($"{student.Name}年龄增加了一岁:{student.Age}");
});
thread1.Start();
Student mystudent = new Student() { Age = 12, Name = "光泽" };
thread2.Start(mystudent);
// 等待两个子线程执行完毕
Thread.Sleep(5000);
}
}
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
}
}
为什么这里可以直接传递Lambda表达式呢? Lambda 表达式() => { ... }或(obj) => { ... }并不是 "直接写方法",而是 C# 提供的语法糖 (编译器帮你简化代码):编译器会自动把 Lambda 表达式转换成对应的委托实例,不需要你手动new ThreadStart(...)或new ParameterizedThreadStart(...)。
针对 thread1 的无参 Lambda(对应 ThreadStart)
cs
Thread thread1 = new Thread(() =>
{
Console.WriteLine("我是由ThreadStart创建的线程");
});
编译器会自动做这些事:① 识别这个 Lambda 是「无参数、返回 void」的,符合ThreadStart委托的定义(public delegate void ThreadStart(););② 自动创建ThreadStart委托实例,并把 Lambda 里的代码作为委托的执行逻辑;③ 把这个委托实例传给Thread构造函数。
等价于编译器帮你生成了这样的代码(你不用写,但底层是这样的):
cs
// 编译器自动生成的匿名方法
static void <Main>b__0()
{
Console.WriteLine("我是由ThreadStart创建的线程");
}
// 编译器自动创建委托实例
ThreadStart ts = new ThreadStart(<Main>b__0);
Thread thread1 = new Thread(ts);
针对 thread2 的带参 Lambda(对应 ParameterizedThreadStart)
cs
Thread thread2 = new Thread((obj) =>
{
var student = obj as Student;
// ... 后续逻辑
});
编译器同样会自动处理:① 识别这个 Lambda 是「单个 object 参数、返回 void」的,符合ParameterizedThreadStart委托的定义(public delegate void ParameterizedThreadStart(object obj););② 自动创建ParameterizedThreadStart委托实例,把 Lambda 逻辑封装进去;③ 把委托实例传给Thread构造函数。
等价于编译器生成:
cs
// 编译器自动生成的匿名方法
static void <Main>b__1(object obj)
{
var student = obj as Student;
// ... 后续逻辑
}
// 自动创建委托实例
ParameterizedThreadStart pts = new ParameterizedThreadStart(<Main>b__1);
Thread thread2 = new Thread(pts);
二、线程的相关操作
2.1 Start ()方法
Start () ≠ 直接调用委托
如果只是 "调用委托方法",直接写委托实例().Invoke()就可以了(同步执行),但Start()的核心是 "创建新线程",然后再新的线程里面执行委托。
cs
namespace Advanced_Thread_03
{
public class Program
{
static void Main(string[] args)
{
Console.WriteLine($"主线程是:{Thread.CurrentThread.ManagedThreadId}");
ThreadStart threadStart = () => Console.WriteLine($"正在执行的线程是:{Thread.CurrentThread.ManagedThreadId}");
Thread thread =new Thread(threadStart);
// 执行委托实例
threadStart.Invoke();
// 创建新线程,让新线程执行委托
thread.Start();
}
}
}
执行结果:
cs
主线程是:1
正在执行的线程是:1
正在执行的线程是:7
可见,用threadStart.Invoke()直接调用委托是并不会改变线程的,线程ID不变,委托只会在主线程执行。但是,thread.Start()不同,他新开了一个线程,于是线程ID变了,这个新的线程里,执行了委托。
2.2 IsBackground属性
在讲 IsBackground属性之前,我们需要先回顾几个概念,进程、主线程、子线程分别是什么:
| 概念 | 比喻 | 核心作用 |
|---|---|---|
| 进程 | 一个正在运营的 "工厂" | 操作系统分配资源的基本单位(独立内存空间、CPU 配额、文件句柄等),工厂有围墙,和其他工厂(进程)互不干扰 |
| 主线程 | 工厂的 "厂长" | 进程启动时操作系统自动创建的第一个线程,是进程的 "入口"(比如 C# 的 Main 方法),负责统筹整个工厂的启动 / 核心逻辑 |
| 子线程 | 工厂里的 "工人" | 由厂长(主线程)手动创建,帮厂长分担工作,和厂长并行干活,共享工厂的资源(内存、工具等),但有自己的 "工作台"(线程栈) |
1. 进程是 "容器",线程是 "容器里的执行者"
- 一个进程可以包含多个线程(主线程 + N 个子线程),但至少有一个线程(主线程);
- 所有线程共享进程的资源(比如全局变量、静态变量、堆内存),但每个线程有独立的栈内存(存储局部变量、方法调用栈)------ 这也是线程安全问题的根源(多个工人抢同一个工具);
- 线程不能脱离进程存在,线程的所有操作都基于所属进程的资源;进程退出后,所有线程都会被强制终止。
2. 主线程是 "进程的入口,但不是进程的主宰"
-
进程启动 → 操作系统自动创建主线程 → 主线程执行
Main方法(C#)/main函数(C++),这是程序的入口; -
主线程的生命周期≠进程的生命周期:
- 主线程执行完
Main方法("厂长下班"),只要进程中还有前台子线程在运行("工人还在干活"),进程就不会退出; - 只有当所有前台线程都执行完,进程才会销毁,释放所有资源。
- 主线程执行完
3. 子线程是 "主线程的助手,分前台 / 后台两种身份"
- 子线程由主线程通过
new Thread()创建,和主线程并行执行(CPU 调度下同时干活); - 子线程默认是前台线程("正式工"),进程必须等所有正式工干完活才关门;
- 子线程可设为后台线程("临时工"),进程不会等临时工,只要正式工都走了,不管临时工有没有干完活,工厂直接关门,临时工被强制终止。
| 线程类型 | 进程是否等待? | 进程退出时的行为 | 示例(结合你的代码) |
|---|---|---|---|
| 前台线程 | 是(必须等) | 进程会等待该线程执行完毕后退出 | 你代码中new Thread()创建的默认前台线程,主线程结束后进程等它打印 ID |
| 后台线程 | 否(不等) | 进程退出时强制终止该线程 | 设thread.IsBackground = true,主线程结束后进程直接退出,子线程可能来不及打印 |
补充:主线程的默认身份
主线程本身也是前台线程(厂长是正式工),所以如果主线程一直运行(比如死循环),即使没有子线程,进程也会一直运行。
所以,当我们把之前的代码改成这样后,新创建的线程变成了后台线程(临时工),而进程(工厂)不会等临时工,所以会直接关厂关门。
cs
namespace Advanced_Thread_03
{
public class Program
{
static void Main(string[] args)
{
Console.WriteLine($"主线程是:{Thread.CurrentThread.ManagedThreadId}");
ThreadStart threadStart = () =>
{
Thread.Sleep(3000);
Console.WriteLine($"正在执行的线程是:{Thread.CurrentThread.ManagedThreadId}");
};
Thread thread = new Thread(threadStart);
thread.IsBackground = true;
thread.Start();
}
}
}
执行结果:
cs
主线程是:1
由于 thread.IsBackground = true,把子线程从默认的前台线程变成了后台线程,用我们刚刚的话说就是把thread从正式工变成了临时工。由于工厂不会等待临时工,所以在主线程,也就是唯一的前台线程执行完毕后,进程就关闭了,所以这个临时工干的活老板根本不承认,于是就打印不出来。
2.3 Join()方法
1 核心定义
Join()的本质是:让调用这个线程的上级线程等这个线程执行完毕,上级线程才能接着走。
用之前的 "工厂(进程)- 厂长(主线程)- 工人(子线程)" 比喻:
- 工人(子线程)喊 "老板,我干完活你再走" → 就是
workerThread.Join(); - 厂长会站在原地等工人干完活,哪怕工人要干 3 小时,厂长也不挪步;
- 等工人干完,厂长才继续自己的收尾工作(比如锁门、结账)。
Join()有 2 个重载,满足不同等待需求,核心区别是 "是否设置超时":
| 方法签名 | 作用 | 返回值 |
|---|---|---|
void Join() |
无限等待,直到子线程执行完毕(哪怕子线程跑 1 小时,主线程也等) | 无 |
bool Join(int ms) |
等待指定毫秒数(ms),超时后不再等 | true = 子线程执行完;f |
比如还是上面那个设置成后台线程的例子,只要加上Join()后,主线程就必须等待子线程了,不能自己先执行结束。
cs
namespace Advanced_Thread_03
{
public class Program
{
static void Main(string[] args)
{
Console.WriteLine($"主线程是:{Thread.CurrentThread.ManagedThreadId}");
ThreadStart threadStart = () =>
{
Thread.Sleep(3000);
Console.WriteLine($"正在执行的线程是:{Thread.CurrentThread.ManagedThreadId}");
};
Thread thread = new Thread(threadStart);
thread.IsBackground = true;
thread.Start();
thread.Join();
}
}
}
执行结果:
cs
主线程是:1
正在执行的线程是:7
2.4 执行顺序
问题描述:如果有两个线程,我要控制两个线程的执行顺序,如何实现?
错误版本:
cs
namespace Advanced_Thread_03
{
public class Program
{
static void Main(string[] args)
{
ThreadStart threadStart1 = () =>
{
Console.WriteLine("我是先执行的线程");
};
Thread thread1 = new Thread(threadStart1);
ThreadStart threadStart2 = () =>
{
Console.WriteLine("我是后执行的线程");
};
Thread thread2 = new Thread(threadStart2);
thread1.Start();
thread2.Start();
}
}
}
执行结果:
我是后执行的线程
我是先执行的线程
结果很崩溃啊,明明是线程1先执行Start(),结果却是线程1在下面。这是为什么呢?
thread1.Start()不是 "让 thread1 立刻跑",只是给操作系统递了个 "执行申请"(线程进等待队列);CPU 调度器选线程时,不看 "谁先递申请",只看当下系统状态(比如 CPU 空闲、线程优先级),随机挑一个先跑,所以 thread2 可能先被选中。重复执行这个代码,结果会发生变化。
改进后的代码:
cs
namespace Advanced_Thread_03
{
public class Program
{
static void Main(string[] args)
{
ThreadStart threadStart1 = () =>
{
Console.WriteLine("我是先执行的线程");
};
Thread thread1 = new Thread(threadStart1);
ThreadStart threadStart2 = () =>
{
Console.WriteLine("我是后执行的线程");
};
Thread thread2 = new Thread(threadStart2);
// 方法1
thread1.Start();
thread1.Join();
thread2.Start();
thread2.Join();
// 方法2
ThreadStart threadStart3 = threadStart1;
threadStart3 += threadStart2;
Thread thread3 = new Thread(threadStart3);
thread3.Start();
}
}
}
这两者方法各有优劣,但是都可以实现控制线程执行顺序。
2.5 带返回值的线程封装
1. 先认清原生 Thread 的核心痛点
.NET 原生Thread的构造函数仅接收ThreadStart(签名:void ())或ParameterizedThreadStart(签名:void (object))委托,无法直接执行带返回值的方法(如int GetYear())。
错误示例代码
cs
namespace Advanced_Thread_03
{
public class Program
{
static void Main(string[] args)
{
// 报错原因:ThreadStart要求无返回值(void),但GetYear返回int,签名不匹配
ThreadStart threadStart = new ThreadStart(GetYear());
}
// 带返回值的业务逻辑:获取当前年份
static int GetYear()
{
return DateTime.Now.Year;
}
}
}
👉 核心问题 :原生Thread不支持带返回值的委托,无法直接执行GetYear这类有返回值的逻辑。
2. 解决 "返回值 + 解耦" 问题
2.1 初步解决:变量暂存返回值(耦合版)
通过 "外部变量暂存结果 + Join等待线程执行",解决返回值问题,但方法与具体业务逻辑(GetYear)强耦合,复用性差。
cs
namespace Advanced_Thread_03
{
public class Program
{
static void Main(string[] args)
{
// 调用方法时直接阻塞主线程,直到子线程返回结果
int result = ThreadWithReturn();
Console.WriteLine($"当前年份:{result}");
}
// 业务逻辑:获取当前年份
static int GetYear()
{
Thread.Sleep(1000); // 模拟耗时操作
return DateTime.Now.Year;
}
// 耦合版:仅支持GetYear逻辑,复用性差
public static int ThreadWithReturn()
{
int result = default(int); // 定义变量存子线程结果
// 把带返回值的逻辑包进无返回值的ThreadStart
ThreadStart threadStart = new ThreadStart(() =>
{
result = GetYear(); // 子线程执行业务逻辑,结果存到外部变量
});
Thread thread = new Thread(threadStart);
thread.Start(); // 启动子线程
thread.Join(); // 立刻阻塞主线程,等子线程执行完
return result; // 返回结果
}
}
}
✅ 解决:原生Thread无返回值的问题;❌ 新问题:方法与GetYear强耦合(换业务逻辑需重写方法),且Join立刻阻塞主线程,灵活性差。
2.2 优化:泛型解耦(通用版)
通过Func<T>委托接收任意带返回值的业务逻辑,封装成泛型方法,解决耦合问题。
cs
namespace Advanced_Thread_03
{
public class Program
{
static void Main(string[] args)
{
// 1. 把业务逻辑封装成Func<T>委托(支持任意返回值类型)
Func<int> func = new Func<int>(GetYear);
// 2. 调用通用方法,直接获取结果(但主线程会立刻阻塞)
int result = ThreadWithReturn(func);
Console.WriteLine($"当前年份:{result}");
}
// 业务逻辑:获取当前年份(可替换为任意带返回值的方法)
static int GetYear()
{
Thread.Sleep(1000); // 模拟耗时操作
return DateTime.Now.Year;
}
// 通用泛型版:支持任意返回值类型,解耦业务逻辑
public static T ThreadWithReturn<T>(Func<T> func)
{
T result = default(T); // 泛型变量存子线程结果
ThreadStart threadStart = new ThreadStart(() =>
{
result = func.Invoke(); // 子线程执行传入的业务逻辑
});
Thread thread = new Thread(threadStart);
thread.Start(); // 启动子线程
thread.Join(); // 仍会立刻阻塞主线程
return result; // 返回子线程结果
}
}
}
✅ 解决:原生Thread无返回值 + 方法耦合问题;❌ 新问题:thread.Join()在启动线程后立刻执行,主线程被强制阻塞(哪怕暂时不需要结果,也得等子线程执行完),比如想先打印 "线程已启动" 再等结果,完全做不到。
3. 核心优化:延迟 Join(按需阻塞)
3.1 优化思路
把 "等待线程(Join)+ 取结果" 的逻辑封装成Func<T>委托返回给主线程:
- 启动子线程时,主线程不阻塞(可先执行其他操作,如打印提示);
- 只有主线程真正需要结果时 ,调用返回的委托,才执行
Join阻塞。
3.2 最终优化版代码
cs
namespace Advanced_Thread_03
{
public class Program
{
static void Main(string[] args)
{
// 1. 封装业务逻辑为Func<T>委托(支持任意返回值类型)
Func<int> yearFunc = new Func<int>(GetYear);
// 2. 调用通用方法,获取"延迟取结果"的委托(此时仅启动子线程,不阻塞)
Func<int> getResultFunc = ThreadWithReturn(yearFunc);
// 3. 主线程可先执行其他操作(无阻塞)
Console.WriteLine("子线程已启动,主线程先执行其他逻辑~~~");
Thread.Sleep(500); // 模拟主线程的其他操作
// 4. 真正需要结果时,调用委托(此时才执行Join,阻塞主线程等结果)
int currentYear = getResultFunc.Invoke();
Console.WriteLine($"当前年份:{currentYear}");
}
// 业务逻辑:获取当前年份(可替换为任意带返回值的耗时操作)
static int GetYear()
{
Thread.Sleep(1000); // 模拟耗时操作(如数据库查询、计算)
return DateTime.Now.Year;
}
/// <summary>
/// 通用带返回值线程封装:延迟阻塞,按需取结果
/// </summary>
/// <typeparam name="T">返回值类型</typeparam>
/// <param name="func">带返回值的业务逻辑委托</param>
/// <returns>延迟取结果的委托(调用时才阻塞等结果)</returns>
public static Func<T> ThreadWithReturn<T>(Func<T> func)
{
T result = default(T); // 闭包变量:存子线程执行结果
Thread thread = new Thread(new ThreadStart(() =>
{
result = func.Invoke(); // 子线程执行业务逻辑,结果存入闭包变量
}));
thread.Start(); // 启动子线程(立刻返回,不阻塞)
// 返回"延迟取结果"的委托:调用时才Join等待
return new Func<T>(() =>
{
thread.Join(); // 调用委托时,阻塞当前线程(主线程)等子线程
return result; // 返回子线程的执行结果
});
}
}
}
3.3 执行结果
子线程已启动,主线程先执行其他逻辑~~~
当前年份:2025
3.4 核心改进说明
| 执行阶段 | 主线程状态 | 关键逻辑 |
|---|---|---|
调用ThreadWithReturn |
无阻塞 | 仅启动子线程,返回 "延迟取结果" 委托 |
| 主线程执行其他操作 | 无阻塞 | 子线程后台执行耗时逻辑,主线程并行处理自己的任务 |
调用getResultFunc.Invoke() |
阻塞(按需) | 执行Join等待子线程,获取结果后恢复执行 |
总结: 一定要清楚的明白,我们使用多线程是为了什么,主要就是为了实现异步,而异步和同步的最核心区别就是,同步会傻等,不管要不要用到都会傻等,而异步会按需等,甚至是不等。