在C#开发中,多线程技术可以让程序同时执行多个任务,避免单线程执行耗时操作时界面卡顿或资源利用率低的问题。掌握多线程的创建和管理方法,是提升程序性能的重要基础。

C#多线程的基础创建方式:Thread类
Thread类是C#中最基础的多线程实现方式,通过实例化Thread对象并传入线程执行的方法,就可以启动一个独立的线程。使用Thread类创建线程的步骤比较简单,适合理解多线程的基本运行逻辑。
下面是一个使用Thread类创建和启动线程的示例:
using System;
using System.Threading;
namespace MultiThreadDemo
{
class Program
{
static void Main(string[] args)
{
// 创建线程,传入线程执行的方法
Thread thread = new Thread(WorkerMethod);
// 启动线程
thread.Start();
// 主线程执行自己的逻辑
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"主线程执行:{i}");
Thread.Sleep(100);
}
// 等待子线程执行完成
thread.Join();
Console.WriteLine("所有线程执行完成");
}
static void WorkerMethod()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"子线程执行:{i}");
Thread.Sleep(150);
}
}
}
}
上述代码中,我们定义了一个WorkerMethod作为子线程的执行方法,实例化Thread对象时传入该方法,调用Start方法启动线程。主线程和子线程会交替执行,最后通过Join方法等待子线程执行完毕,避免主线程提前退出。
Thread类的常用属性和方法
- Start():启动线程,让线程进入就绪状态,等待CPU调度执行。
- Join():阻塞当前线程,直到调用的线程执行完成。
- IsAlive:判断线程是否还在执行中,返回布尔值。
- Priority:设置线程的优先级,取值包括
Lowest、BelowNormal、Normal、AboveNormal、Highest,默认是Normal。 - Name:设置线程的名称,方便调试时区分不同的线程。
更推荐的多线程方式:Task类
Thread类虽然简单,但是直接操作线程的开销比较大,而且管理起来不够方便。.NET Framework 4.0之后引入了Task类,基于任务的多线程方式更加高效,也更符合现代C#的异步编程习惯。Task底层使用了线程池,不需要手动管理线程的创建和销毁。
下面是使用Task创建多线程的示例:
using System;
using System.Threading.Tasks;
namespace MultiThreadDemo
{
class Program
{
static void Main(string[] args)
{
// 方式1:使用Task.Run创建并启动任务
Task task1 = Task.Run(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"Task1执行:{i}");
Task.Delay(100).Wait();
}
});
// 方式2:使用Task.Factory.StartNew创建任务
Task task2 = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"Task2执行:{i}");
Task.Delay(120).Wait();
}
});
// 等待所有任务执行完成
Task.WaitAll(task1, task2);
Console.WriteLine("所有Task执行完成");
}
}
}
Task相比Thread的优势非常明显,首先不需要手动管理线程生命周期,其次支持任务的链式调用、返回值获取、异常处理等高级功能。如果需要在任务中获取返回值,可以使用Task<TResult>泛型类。
using System;
using System.Threading.Tasks;
namespace MultiThreadDemo
{
class Program
{
static void Main(string[] args)
{
// 创建有返回值的Task
Task<int> calcTask = Task.Run(() =>
{
int sum = 0;
for (int i = 1; i <= 100; i++)
{
sum += i;
}
return sum;
});
// 等待任务完成并获取结果
int result = calcTask.Result;
Console.WriteLine($"1到100的和是:{result}");
}
}
}
线程池的使用
线程池是.NET中维护的一个线程集合,里面存放了多个可复用的线程。当需要执行多线程任务时,不需要每次都创建新的线程,而是从线程池中获取一个空闲线程执行任务,任务执行完成后线程会回到线程池等待下一次使用,这样可以大大减少线程创建和销毁的开销。
Thread类和Task类底层其实都使用了线程池,我们也可以直接使用ThreadPool类来提交任务到线程池。
using System;
using System.Threading;
namespace MultiThreadDemo
{
class Program
{
static void Main(string[] args)
{
// 向线程池提交工作项
ThreadPool.QueueUserWorkItem(state =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"线程池任务执行:{i}");
Thread.Sleep(100);
}
});
// 主线程等待,避免提前退出
Thread.Sleep(600);
Console.WriteLine("主线程执行完成");
}
}
}
线程池适合执行大量短时间的任务,但是不适合执行长时间运行的任务,因为长时间占用线程池的线程会导致线程池资源紧张,影响其他任务的执行。如果需要执行长时间的任务,还是建议使用单独的Thread或者Task。
多线程的同步与线程安全
当多个线程同时操作同一个共享资源时,很容易出现线程安全问题,比如多个线程同时修改一个变量,可能会导致数据错误。这时候就需要使用线程同步机制来保证同一时间只有一个线程访问共享资源。
常用的线程同步方式包括lock关键字、Monitor类、Mutex、Semaphore等,其中lock是最常用的轻量级同步方式。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace MultiThreadDemo
{
class Program
{
// 共享资源
private static int count = 0;
// 用于lock的私有对象,必须是引用类型
private static readonly object lockObj = new object();
static void Main(string[] args)
{
// 创建10个任务同时修改count
Task[] tasks = new Task[10];
for (int i = 0; i < 10; i++)
{
tasks[i] = Task.Run(() =>
{
for (int j = 0; j < 1000; j++)
{
// 使用lock保证同一时间只有一个线程进入代码块
lock (lockObj)
{
count++;
}
}
});
}
Task.WaitAll(tasks);
// 正确结果应该是10*1000=10000
Console.WriteLine($"最终count的值:{count}");
}
}
}
如果不使用lock关键字,多个线程同时修改count变量,最终的结果会小于10000,因为会出现多个线程同时读取到相同的count值,然后各自加1再写回,导致部分修改丢失。使用lock之后,同一时间只有一个线程可以执行count++的操作,保证了结果的正确性。
多线程的注意事项
- 不要滥用多线程,线程的创建和切换本身有一定的开销,如果任务本身很简单,单线程执行可能效率更高。
- 避免死锁,当多个线程互相持有对方需要的锁时,就会出现死锁,编写同步代码时要注意锁的获取顺序。
- 不要在非UI线程直接操作UI控件,如果是桌面程序,需要跨线程更新UI时,要使用控件的Invoke方法回到UI线程执行操作。
- 合理设置线程的优先级,不要随意设置过高的优先级,可能会导致其他线程无法得到执行机会。
多线程是C#开发中非常重要的技术,掌握Thread、Task、线程池的使用方法,以及线程同步的相关技巧,能够帮助你写出更高效、更稳定的程序。在实际项目中,优先推荐使用Task和异步编程模式,减少手动管理线程的成本。