【从零开始入门unity游戏开发之——C#篇37】进程、线程和C# 中实现多线程有多种方案
文章目录
- 进程、线程和C#多线程
- 一、`进程`的基本概念
- 二、`线程`的基本概念
- 三、C#中的多线程
- 1、为什么需要多线程?
- 2、*C# 中如何实现多线程**
- 2.1 **使用 `Thread` 类**
- (1)示例
- (2)线程休眠
- (3)设置为后台线程
- 普通的前台线程
- 设置为后台线程
- (4)停止线程
- 2.2 **使用 `Task` 类** (推荐)
- 2.3 使用 ThreadPool 类
- 2.4 **使用 `async` 和 `await`**
- 2.5 使用 Parallel 类
- 3、对比总结:
- 4、锁(`lock`)线程安全
- 4.1 问题分析
- 4.2 解决方案
- 4.3 示例:
- 4.4 工作机制:
- 5、死锁
- 5.1 问题分析
- 5.2 避免死锁的策略:
- 四、总结
- 专栏推荐
- 完结
进程、线程和C#多线程
一、进程
的基本概念
进程是计算机中正在运行的程序的实例。每个进程有自己的内存空间和资源,系统通过进程来隔离不同程序之间的执行。
例如,你打开一个浏览器,它会启动一个进程;然后,你打开一个文本编辑器,它又是另一个进程。每个进程相互独立,它们有自己的内存、文件句柄等。
我们打开win的任务管理器,这里其实就是一个个进程,一个正在运行的程序就是一个进程
二、线程
的基本概念
线程是进程中的一个执行单元。一个进程至少有一个线程,这个线程负责执行进程中的代码。
如果你有一个程序,这个程序会启动一个主线程来执行代码。当程序需要执行多个任务时,可以在这个进程里创建多个线程。
线程是 CPU 调度的基本单位
。你可以把线程理解成进程内部的工作小单元,多个线程可以在同一个进程中并行或交替执行。
三、C#中的多线程
C# 支持多线程编程
,这意味着你可以同时执行多个任务或操作。举个简单的例子,你可以在一个程序里同时播放音乐、下载文件、处理数据,而这些操作并不会互相干扰。每个操作都可以由不同的线程处理。
1、为什么需要多线程?
假设你有一个程序需要做很多事情(比如下载文件、处理数据等),如果它只用一个线程,程序会在一个任务完成后才开始下一个任务,这会导致程序的响应变慢,特别是在处理耗时任务时。多线程的好处就是能并行或并发地处理这些任务,让程序更高效。
2、C# 中如何实现多线程*
在 C# 中实现多线程有多种方案,每种方案有不同的使用场景和特点。以下是几种常见的实现方式:
2.1 使用 Thread
类
Thread 类提供了直接的多线程控制,是最基本的多线程编程方式。
- 优点:简单直接,易于理解。
- 缺点:需要手动管理线程的生命周期,容易产生线程安全问题,效率较低。
(1)示例
using System;
using System.Threading;
class Program
{
static void Main()
{
// 创建一个线程,执行 DoWork 方法
Thread t = new Thread(DoWork);
t.Start(); // 启动线程
// 主线程继续执行其他操作
Console.WriteLine("Main thread is running.");
}
static void DoWork()
{
// 线程执行的任务
Console.WriteLine("Working in the background...");
}
}
在上面的例子中,我们创建了一个新的线程 t
,并让它去执行 DoWork
方法。然后主线程会继续执行后面的代码。主线程和新线程是并行工作的。
(2)线程休眠
可以通过 Thread.Sleep()
方法让线程暂停执行指定的时间。
- 参数是毫秒数,1秒=1000毫秒。
示例代码:
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread t = new Thread(NewThreadLogic);
t.Start();
}
static void NewThreadLogic()
{
while (true)
{
Console.WriteLine("线程执行中...");
Thread.Sleep(1000); // 休眠1秒
}
}
}
(3)设置为后台线程
使用 Thread.IsBackground = true
将线程设置为后台线程,可以让你处理那些不需要程序等待完成的任务,提高程序的响应速度。
在 后台线程 的情况下,主线程结束时,后台线程会被强制终止,即使它还没有完成任务。
普通的前台线程
using System;
using System.Threading;
class Program
{
static void Main()
{
// 创建后台线程
Thread t = new Thread(DoWork);
t.Start();
Console.WriteLine("主线程结束,后台线程继续执行中...");
}
static void DoWork()
{
int count = 0;
while (count < 5)
{
Console.WriteLine($"工作线程执行中... {count}");
Thread.Sleep(1000); // 每秒执行一次
count++;
}
}
}
结果,主线程结束时,如果有后台线程在运行,程序就不会退出,直到后台线程执行完毕。
设置为后台线程
using System;
using System.Threading;
class Program
{
static void Main()
{
// 创建后台线程
Thread t = new Thread(DoWork);
t.IsBackground = true; // 设置为后台线程
t.Start();
// 主线程不等待后台线程
Console.WriteLine("主线程结束,后台线程继续执行中...");
// 主线程结束,程序直接退出
}
static void DoWork()
{
int count = 0;
while (count < 5)
{
Console.WriteLine($"工作线程执行中... {count}");
Thread.Sleep(1000); // 每秒执行一次
count++;
}
}
}
结果,在 后台线程 的情况下,主线程结束时,后台线程会被强制终止,即使它还没有完成任务。
(4)停止线程
对于死循环线程,必须通过某种方式来终止它。可以使用一个标志变量来控制线程停止。
也可以调用 Thread.Abort()
来强制终止线程,但该方法在 .NET Core 中已经被废弃,因为它会抛出 ThreadAbortException 异常,且有潜在的稳定性风险。所以这里就不介绍了。
使用标志变量停止线程
using System;
using System.Threading;
class Program
{
static bool isRunning = true;
static void Main()
{
Thread t = new Thread(NewThreadLogic);
t.Start();
// 稍后停止线程
Thread.Sleep(5000); // 等待5秒
isRunning = false; // 设置标志变量为false,线程会退出
}
static void NewThreadLogic()
{
while (isRunning)
{
Console.WriteLine("线程正在运行");
Thread.Sleep(1000); // 休眠1秒
}
Console.WriteLine("线程已停止");
}
}
2.2 使用 Task
类 (推荐)
除了 Thread
类,C# 还提供了 Task
类,它更高级、使用起来更简单,通常用于处理异步操作。Task
适合于执行一些后台工作,不需要创建管理线程。示例如下:
- 优点:Task 自动管理线程池线程,提供了更高层次的抽象,支持任务组合、取消、异常处理等功能。
- 缺点:由于是基于线程池,因此任务是异步执行的,可能不是实时响应的。
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Task.Run(() => DoWork());
Console.ReadLine(); // 防止程序过早退出
}
static void DoWork()
{
Console.WriteLine("Task is running...");
}
}
2.3 使用 ThreadPool 类
ThreadPool 是 .NET 提供的一个线程池,可以重用线程池中的线程,从而减少线程的创建和销毁开销。
- 优点:线程池可以重用线程,避免了线程的频繁创建和销毁,效率较高。
- 缺点:线程池的线程是共享的,可能会引发竞态条件(Race Condition),需要特别注意线程安全。
using System;
using System.Threading;
class Program
{
static void Main()
{
ThreadPool.QueueUserWorkItem(DoWork);
}
static void DoWork(object state)
{
Console.WriteLine("ThreadPool thread is running...");
}
}
2.4 使用 async
和 await
C# 中 async 和 await 关键字使得异步编程变得更加简单,虽然它并不直接涉及多线程,但可以实现非阻塞的操作,常用于 I/O 密集型任务(如网络请求、文件操作等)。
- 优点:代码结构清晰,不需要显式创建线程,可以轻松处理异步操作。
- 缺点:适用于 I/O 密集型操作,对于 CPU 密集型任务效果不佳。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// 异步执行方法
await DoWorkAsync();
Console.WriteLine("Main thread is running.");
}
static async Task DoWorkAsync()
{
// 模拟耗时任务
await Task.Delay(2000); // 延迟2秒
Console.WriteLine("Work completed in the background.");
}
}
2.5 使用 Parallel 类
Parallel 类是并行编程的高级 API,通常用于对大量数据进行并行处理。它可以自动分配任务到多个线程,适用于大规模数据处理等场景。
- 优点:简化并行任务的实现,自动管理线程和任务分配。
- 缺点:适用于需要大量并行计算的场景,对于简单的任务可能显得过于复杂。
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Parallel.For(0, 10, i =>
{
Console.WriteLine($"Task {i} is running...");
});
}
}
3、对比总结:
方案 | 描述 | 优点 | 缺点 | 实际应用示例 |
---|---|---|---|---|
Thread 类 | 使用 System.Threading.Thread 类直接创建和管理线程。 | 控制灵活,可以创建、启动和管理线程;可以访问线程状态等。 | 线程创建和销毁开销较大;较低级别,需要手动管理线程生命周期。 | 执行长时间运算或独立任务,如后台下载或复杂计算任务。 |
Task 类 | 使用 System.Threading.Tasks.Task 类进行任务管理,支持异步编程。 | 更高层次的抽象;自动管理线程池;支持异步编程;异常处理更简洁。 | 相较于 Thread ,无法精细控制线程;适合短时间任务。 | 网络请求、文件操作等 I/O 密集型操作;并行计算等。 |
ThreadPool | 使用 ThreadPool 提供的线程池进行线程管理。 | 重用线程,减少线程创建销毁的开销;自动管理线程;适合短时间任务。 | 不能精确控制线程的生命周期;不适合需要大量 CPU 时间的任务。 | 高并发任务、简单的后台操作、事件处理等。 |
async/await | 基于异步编程模型,使用 async 和 await 关键字进行异步调用。 | 简化异步编程;不会阻塞线程;适合 I/O 密集型操作。 | 只能用于 I/O 密集型任务;不适用于 CPU 密集型任务。 | 异步文件读取、数据库访问、Web API 请求等 I/O 密集型操作。 |
Parallel 类 | 使用 System.Threading.Tasks.Parallel 类来执行并行操作。 | 简化并行计算;自动分配任务到线程池中的线程;易于实现并行。 | 无法控制线程的数量和执行方式;仅适合 CPU 密集型任务。 | 大规模数据处理(例如数组排序、并行计算等)。 |
4、锁(lock
)线程安全
4.1 问题分析
多个线程同时访问共享数据时,可能会导致数据冲突或者不一致。(比如线程1想修改a变量,线程2又想获取a变量。)
4.2 解决方案
因此,在多线程编程中,要特别注意线程安全。常用的做法是使用锁(lock
)来避免同时访问共享资源。
4.3 示例:
using System;
using System.Threading;
class Program
{
static object obj = new object(); // 用于加锁的对象
static void Main()
{
Thread t1 = new Thread(ThreadLogic);
Thread t2 = new Thread(ThreadLogic);
t1.Start();
t2.Start();
}
static void ThreadLogic()
{
while (true)
{
lock (obj) // 锁定共享资源
{
//操作共享数据
}
Thread.Sleep(1000); // 休眠1秒
}
}
}
lock 锁定的对象通常是一个引用类型
(例如 object)。通常情况下,最好选择一个私有的、专用的对象作为锁对象,而不是使用诸如 this
或者 typeof(MyClass)
这样的公共对象。这样做的原因是防止外部代码错误地修改锁对象,导致不安全的同步行为。
4.4 工作机制:
-
进入锁:当线程执行
lock (obj)
时,它会尝试获得obj
的锁。- 如果没有其他线程持有该锁,当前线程就能进入锁定的代码块。
- 如果其他线程已经持有该锁,当前线程将被阻塞,直到锁被释放。
-
释放锁:当线程执行完 lock 代码块中的代码后,自动释放锁,使得其他等待的线程可以获得锁并进入临界区。
5、死锁
5.1 问题分析
如果程序中存在多个线程互相等待对方释放锁的情况,可能会发生死锁。比如线程 A 持有锁 1,等待锁 2,线程 B 持有锁 2,等待锁 1,这种情况会导致两个线程永远无法继续执行。
5.2 避免死锁的策略:
- 尽量避免多个锁的嵌套。
- 遵循一致的锁定顺序:如果多个线程需要获取多个锁,应该遵循相同的顺序获取锁,以防止死锁。
四、总结
- 进程是一个正在运行的程序,它有自己的内存和资源。
- 线程是进程中的执行单位,一个进程至少有一个线程,多个线程可以在同一个进程中并行工作。
- 在 C# 中,多线程可以通过
Thread
类、Task
类以及async/await
来实现。 - 需要注意线程安全问题,避免并发访问导致数据冲突。
专栏推荐
地址 |
---|
【从零开始入门unity游戏开发之——C#篇】 |
【从零开始入门unity游戏开发之——unity篇】 |
【制作100个Unity游戏】 |
【推荐100个unity插件】 |
【实现100个unity特效】 |
【unity框架开发】 |
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~