线程是并发编程的基础概念之一。在现代应用程序中,我们通常需要执行多个任务并行处理,以提高性能。C# 提供了多种并发编程工具,如
Thread
、Task
、异步编程和Parallel
等。
Thread 类
Thread
类是最基本的线程实现方法。使用Thread
类,我们可以创建并管理独立的线程来执行任务。
基本使用Thread
创建一个新的实例对象,将一个方法直接给Thread类,并使用实例对象启动线程运行。
static void Test() {
Console.WriteLine("Test事件开始执行");
Thread.Sleep(1000);
Console.WriteLine("Test事件睡眠之后打印数据");
Console.WriteLine("Test事件id: " + Thread.CurrentThread.ManagedThreadId);
}
static void Main(string[] args)
{
#region //主线程id
Console.WriteLine("主线程id: " + Thread.CurrentThread.ManagedThreadId);
#endregion
#region //传递一个方法给线程,执行方法
Thread t = new Thread(Test);
t.Start();
#endregion
}
线程数据传输
传递单个参数
Thread
提供了一个 Start(object parameter)
方法,可以在启动线程时传递一个参数。
static int Downlod(object obj) {
string str = obj as string;
Console.WriteLine(str);
}
static void Main(string[] args)
{
Thread t1 = new Thread(Downlod);
//这里传递的参数是字符串
t1.Start("数据传递方法执行 ,数据传递方法id: "+Thread.CurrentThread.ManagedThreadId);
}
这种方式适用于需要传递单个参数的情况。
使用类的实例方法传递多个参数
先new一个类的实例,给实例字段赋值,调用类的实例,通过实例调用方法,构造函数接收参数,然后在线程中调用该实例的方法。
static void Main(string[] args)
{
// 使用 DownloadTool 实例化并传递数据
DownloadTool downloadTool = new DownloadTool("www.baidu.com", "这是下载链接哦");
Thread thread = new Thread(downloadTool.Download);
thread.Start();
}
public class DownloadTool
{
private string url;
private string message;
public DownloadTool(string url, string message)
{
this.url = url;
this.message = message;
}
public void Download()
{
Console.WriteLine("下载链接: " + url);
Console.WriteLine("提示信息: " + message);
Console.WriteLine("Download 线程 ID: " + Thread.CurrentThread.ManagedThreadId);
}
}
当 thread
线程启动时,它会执行 downloadTool.Download()
方法,输出传递的数据。
线程优先级
在 C# 中,可以使用 Thread.Priority
属性来设置线程的优先级。线程优先级决定了操作系统在多线程环境中调度线程的顺序,但并不保证高优先级的线程总是比低优先级的线程更早或更频繁地执行。
线程优先级级别
C# 提供了五个线程优先级级别,定义在 ThreadPriority
枚举中:
- Lowest:最低优先级。操作系统尽可能少地调度这个优先级的线程。
- BelowNormal:低于正常的优先级。优先级比 Normal 低,但高于 Lowest。
- Normal:默认优先级,大多数线程默认的优先级。适用于一般用途。
- AboveNormal:高于正常的优先级。操作系统更倾向于调度这个优先级的线程。
- Highest:最高优先级。操作系统尽可能多地调度这个优先级的线程。
internal class Program
{
static void A()
{
int i = 0;
while (true)
{
i++;
Console.WriteLine($"A 输出第{i}");
Thread.Sleep(1000);
}
}
static void B()
{
int i = 0;
while (true)
{
i++;
Console.WriteLine($"B 输出第{i}");
Thread.Sleep(1000);
}
}
static void Main(string[] args)
{
//在C#中,线程的优先级可以通过Thread.Priority属性来设置和获取。
// Lowest: 线程的优先级是最低的。在系统中存在其他活动线程时,此优先级的线程很少得到执行。
//BelowNormal: 线程的优先级低于正常线程。
//Normal: 线程的优先级是普通的,这是线程的默认优先级。
//AboveNormal: 线程的优先级高于正常线程。
//Highest: 线程的优先级是最高的。此优先级的线程会尽量优先于其他所有优先级的线程执行。
Thread a = new Thread(A);
Thread b = new Thread(B);
a.Priority = ThreadPriority.Highest;
a.Start();
b.Priority = ThreadPriority.Lowest;
b.Start();
Console.WriteLine("按任意键停止线程...");
Console.ReadKey();
a.Join();
b.Join();
Console.WriteLine("线程已停止");
}
}
A
被设置为最高优先级ThreadPriority.Highest
。B
被设置为最低优先级ThreadPriority.Lowest
。
注意事项
-
优先级不是绝对控制:操作系统可能会忽略优先级设置,特别是在资源有限的系统中。高优先级线程不一定会一直执行,也不能阻止低优先级线程的执行。
-
使用优先级的适用场景:设置线程优先级可能适用于实时系统(例如,某些任务需要优先处理)。但是,大多数应用程序通常可以使用默认的
Normal
优先级。 -
避免使用过多的高优先级线程:如果所有线程都被设置为
Highest
,系统的整体性能可能会下降,甚至导致线程争用 CPU 资源的情况。 -
CPU 密集型任务:在 CPU 密集型任务中,优先级可能会对性能产生较大影响,因为优先级高的线程可能会占用更多的 CPU 时间。
线程优先级的最佳实践
- 默认使用
Normal
优先级,除非有特殊原因。 - 避免滥用
Highest
优先级,因为它会对系统资源产生影响。 - 在 I/O 密集型的线程中,优先级通常不会有显著差异,因为这些线程在等待 I/O 操作完成时,CPU 会调度其他线程。
通过合理设置线程优先级,可以帮助操作系统更好地调度线程,以满足应用程序的需求。 但通常在 .NET 应用程序中,多数情况下使用默认的 Normal
优先级就足够了。
线程池
线程池 (ThreadPool
) 是一种高效的管理和调度线程的方式。线程池自动管理线程的创建、重用和销毁,从而减少了手动创建和管理线程的开销。
为什么使用线程池
- 性能更高:线程池会重用现有的线程,减少了创建和销毁线程的开销。
- 自动管理:线程池会根据系统负载动态调整线程数量。
- 避免线程资源不足:线程池限制了同时运行的线程数,避免了线程过多导致的资源耗尽问题。
基本使用 ThreadPool.QueueUserWorkItem
方法将任务排入线程池队列。
using System;
using System.Threading;
class Program
{
static void Main(string[] args)
{
// 将任务排入线程池
ThreadPool.QueueUserWorkItem(DoWork, "任务 1");
ThreadPool.QueueUserWorkItem(DoWork, "任务 2");
Console.WriteLine("主线程完成");
Thread.Sleep(3000); // 等待线程池中的任务完成
}
static void DoWork(object state)
{
string taskName = (string)state;
Console.WriteLine($"{taskName} 开始执行 - 线程ID: {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000); // 模拟耗时操作
Console.WriteLine($"{taskName} 执行完成 - 线程ID: {Thread.CurrentThread.ManagedThreadId}");
}
}
运行结果QueueUserWorkItem自动分配线程来执行任务。
QueueUserWorkItem
方法将任务排入线程池。它接收一个委托(即方法)和一个可选的状态对象(传递给方法的数据)。
DoWork
方法接受一个参数 state
,这是从 QueueUserWorkItem
传递的。
Thread.Sleep(3000)
确保主线程不会立即退出,使得线程池中的任务有机会完成。
使用带返回值的线程池任务
C# 中的 ThreadPool
通常不直接支持返回值。如果需要获得任务结果,可以使用 Task
,因为 Task
本质上也是线程池的一部分。Task
更适合于带返回值的异步操作。这里使用 Task.Run
来代替 ThreadPool
:
static void Main(string[] args)
{
//使用tesk多线程
int a= Task.Run(() =>
{
int a = Dowload();
return a;
}).Result;
Task<int> task = Task<int>.Run(()=>{
int a = Dowload();
return a;
});
//初始化一个CancellationTokenSource实例
CancellationTokenSource source = new CancellationTokenSource();
//task.Start();
task.Wait(1000);
source.Cancel();
int result = task.Result;
Console.WriteLine(result);
Console.WriteLine($"tesk返回值{a}");
}
static int Dowload() {
int a = 0;
for (int i = 0; i < 10; i++)
{
a= a + i + 1;
}
int? id= Task.CurrentId;
Console.WriteLine("Current thread ID: " + id);
return a;
}
线程池的限制
- 任务运行时间过长:线程池中的线程本质上是共享资源,如果某个任务运行时间太长,将会占用线程池中的线程,导致其他任务无法及时执行。
- 不适合实时系统:线程池中的任务调度是由系统管理的,无法保证精确的实时性。
- 有限的线程数量:在高并发场景中,如果线程池中的线程全部被占用,新的任务将会等待,直到有线程可用。
线程池总结
线程池是一种高效的并发处理方式,适合于大多数轻量级的后台任务。在现代 C# 编程中,建议使用 Task
和 async/await
进行异步操作,因为它们能简化代码,并且使用底层的线程池来管理线程。如果需要精确控制线程的执行,通常建议使用手动管理的 Thread
等。