一、什么是线程
程序执行的最小单元
一次页面的渲染、一次点击事件的触发、一次数据库的访问、一次登录操作都可以看作是一个一个的进程
在一个进程中同时启用多个线程并行操作,就叫做多线程
由CPU来自动处理
线程有运行、阻塞、就绪三态
代码示例:
class Program
{
static void Main(string[] args)
{
Thread thread = new Thread(() =>
{
Print1();
});
thread.Start();
for(int i = 0; i < 1000; i++)
{
Console.Write(0);
}
Console.Read();
}
static void Print1()
{
for (int i = 0;i < 1000; i++)
{
Console.Write(1);
}
}
}
运行结果为
可以看到,在结果中,0和1的输出是交织在一起的,原因为两个线程交替着被运行,不断反复直到结束。
另外一个常用操作为Sleep();
让时间暂停,使得线程进入静默状态。
二、前台线程、后台线程与线程池托管
代码举例:
class Program
{
static void Main(string[] args)
{
Thread thread = new Thread(PrintHello);
thread.Start();
Console.WriteLine("退出主程序");
}
private static void PrintHello(object? obj)
{
while (true)
{
Thread.Sleep(1000);
Console.WriteLine("Hello from PrintHello!");
}
}
}
运行会发现,即使主线程运行结束了,子线程依旧在持续运行;持续运行的子线程就称为前台线程。
一般来说,只有等待前台线程运行完毕后,程序才可以进行关闭。
与之对应的是 后台线程,可以通过thread.IsBackground = true;//切换为后台线程
将前台 线程切换到后台线程,这样再次运行会发现,当主线程结束后,后台线程就会被强制结束。
一般来说,前台线程用于需要时间比较长的等待业务,比如监听客户端请求,而后台线程适用于时较短的业务比如执行客户端发来的请求,后台进程不会影响程序的终止。
托管在线程池中的线程全部为后台线程。
所有使用new Thread创建的线程默认均为前台线程。
三、线程池
示例代码
for(int i = 0;i < 100; i++)
{
ThreadPool.QueueUserWorkItem((o) =>
{
Console.WriteLine($"循环次数{
i} 线程id {
Thread.CurrentThread.ManagedThreadId}");
});
}
可以看到执行结果出现了id重复的状况,原因就是线程池会重复使用已经完成的线程,极大节约硬件资源。
另外,可以看到,for循环有100次,但是从输出结果来看,只执行了十几次,原因为线程池创建的线程均为后台线程,只要主程序退出,线程池的后台线程就会被停止,而主程序main执行的时间很短,因此线程池内线程没有来得及执行就被停止了。
对于重要的并发量小的线程,需要手动创建管理,对于并发量大而又不太重要的线程,最好托管到线程池中。
四、结束线程与CancellationToken
不管程序有多少个进程,进程内部的资源都是被共享的。所以C#对进程的取消代码作了更高层次的抽象,把进程的取消过程封装成为了Token的形式,也就是CancellationToken(取消令牌)。不仅可以使用在多线程中,还可以用于异步操作。
class Program
{
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
Thread thread = new Thread(() => {
PrintHello(cts.Token); });
thread.Start();
//下载文件
Thread.Sleep(5000);
//关闭子进程
//cts.Cancel();
cts.CancelAfter(