今天我们来详细介绍一下多线程,多线程是.NET中非常重要的知识点,需要完全掌握。
一、什么是多线程?
在了解线程 之前,我们需要知道什么是进程 ,所谓进程 ,就是指操作系统中运行的程序,比如我们自己写的在运行的.NET程序,或者是正在运行的QQ、微信、浏览器等,这些都叫做一个进程,进程与进程之间保持独立,比如浏览器崩了不会影响微信。
如果操作系统是一个工厂,那么进程 就是一个车间 ,而车间里的工人就是线程 。如果一个进程 里只有一个工人,那就是单线程 ,由于只有一个工人,所以效率比较慢;如果一个进程 里有多个工人,那就是多线程,由于可以多个工人相互配合,所以效率相对较高。
二、什么是同步异步?
这里需要重点介绍一下同步异步 的知识,因为总有小伙伴会将同步异步 和单线程多线程搞混。
- 同步 :你自己点外卖,拿着手机一直等:等商家接单、等商家做、等骑手送,期间你啥也干不了,就傻坐着等(这就是「线程阻塞」)。
👉 不管是 1 个人干(单线程)还是 2 个人干(多线程),只要你等事儿干完,就是同步。
- 异步 :你用 APP 点完外卖,把手机一扔,该刷剧刷剧、该打游戏打游戏,等外卖到了(手机响通知)你再去拿 ------不等事儿干完,先干别的。
👉 这就是异步,重点是「不傻等」,和 "几个人干活" 没关系。
三、单线程多线程与同步异步的区别
- 同步 / 异步:只看你「要不要等事儿干完」(做事的方式);
- 单线程 / 多线程:只看你「有几个人干活」(干活的人)。
3.1 区分同步异步
如何区分同步异步,只要看当前运行的线程会不会等待方法执行结束,比如我下面这两个例子:
cs
namespace ConsoleTest
{
public class Program
{
static void Main(string[] args)
{
{
Console.WriteLine("同步点外卖");
SyncOrder();
}
Console.WriteLine("============================================");
{
Console.WriteLine("异步点外卖");
AsyncOrder();
}
}
static void SyncOrder()
{
Console.WriteLine("下单成功");
Console.WriteLine("一直盯着手机等待骑手送达");
Thread.Sleep(2000);
Console.WriteLine("两秒钟后骑手送到");
}
static void AsyncOrder()
{
Console.WriteLine("下单成功");
Thread thread = new Thread(() =>
{
Console.WriteLine("让手机自己等待,外面到了再提醒我");
Thread.Sleep(2000);
Console.WriteLine("手机提示外卖到了");
});
thread.Start();
Console.WriteLine("我自己去洗碗了~~~~");
}
}
}
Thread.Sleep的作用是让当前正在执行的线程暂停(休眠)指定的毫秒数 ,期间释放 CPU 资源(让给其他线程干活),休眠时间到了再继续执行。我们返回刚刚的概念,如何区分同步异步,只要看当前运行的线程会不会等待方法执行结束 ,由于SyncOrder中,我让当前运行的线程睡眠了2秒,这个线程必须等待2秒,2秒后才可以继续运行其他代码,所以这里的SyncOrder是同步方法。
不同的是,当主线程开始执行AsyncOrder后,AsyncOrder会在内部新创建一个线程,这个新线程负责等待2秒,但是需要注意的是:主线程并不会等待新线程执行完毕 ,当方法内部执行了 thread.Start()后,新线程开始执行等待方法,而主线程继续往下走,执行Console.WriteLine("我自己去洗碗了~~~~")这个洗碗操作,所以这里是异步方法。
执行结果:
cs
同步点外卖
下单成功
一直盯着手机等待骑手送达
两秒钟后骑手送到
============================================
异步点外卖
下单成功
我自己去洗碗了~~~~
让手机自己等待,外面到了再提醒我
手机提示外卖到了
那这里的异步方法能不能也改成同步方法呢?答案是可以的,同步方法的核心不就是让主线程等待嘛,那我们也只需要让主线程等待新线程执行,不就把它变成同步方法了:
cs
static void AsyncOrder()
{
Console.WriteLine("下单成功");
Thread thread = new Thread(() =>
{
Console.WriteLine("让手机自己等待,外面到了再提醒我");
Thread.Sleep(2000);
Console.WriteLine("手机提示外卖到了");
});
thread.Start();
// 让主线程等待新线程执行完毕
thread.Join();
Console.WriteLine("我自己去洗碗了~~~~");
}
我们这里只加了一行代码:thread.Join();作用就是让主线程等待新线程执行完毕,随后再去执行下面的洗碗方法,这样改变后,这个AsyncOrder也就变成了一个同步方法。 修改后的结果:
cs
同步点外卖
下单成功
一直盯着手机等待骑手送达
两秒钟后骑手送到
============================================
异步点外卖
下单成功
让手机自己等待,外面到了再提醒我
手机提示外卖到了
我自己去洗碗了~~~~
原本下单成功就可以去洗碗,现在要等待外卖到了再去洗碗,可见AsyncOrder也变成了一个同步方法。虽然都是让主线程陷入等待,但是两个同步方法实现的等待逻辑有所不同:
- 一开始的同步方法只有一个主线程,主线程会等待是因为执行了
Thread.Sleep方法,相当于让它自己陷入了睡眠。 - 后面由异步方法改造而成的同步方法,主线程会等待是因为执行力
thread.Join();这个方法,导致主线程必须等待新线程执行完毕,相当于必须等待别人完成任务,你才可以开始。
新手小伙伴一定要好好看这两个代码,才能深入理解同步异步的区别!!!!
3.2 区分单线程多线程
相比之下区分单线程多线程就容易得多,只要看代码是否运行在多个线程上 。换句话说,也就是看也没有创建新的线程并且启动新的线程。
由于下面的代码中,我并没有创建新的线程,更没有调用,所以这个代码在主线程上运行:
cs
namespace SingleMultiThreadDemo
{
class Program
{
static void Main(string[] args)
{
// 单线程核心:所有代码都在主线程(线程ID固定)
Console.WriteLine($"主线程代码 ------ 线程ID:{Thread.CurrentThread.ManagedThreadId}");
// 调用普通方法,还是在主线程
DoSomething();
// 即使有循环/延时,依然是同一个线程
Thread.Sleep(1000);
Console.WriteLine($"延时后代码 ------ 线程ID:{Thread.CurrentThread.ManagedThreadId}");
}
static void DoSomething()
{
Console.WriteLine($"普通方法代码 ------ 线程ID:{Thread.CurrentThread.ManagedThreadId}");
}
}
}
调用结果:
主线程代码 ------ 线程ID:1
普通方法代码 ------ 线程ID:1
延时后代码 ------ 线程ID:1
下面的代码中,我创建了新的线程,并且启动了新的线程,所以这个代码在多个线程上运行:
cs
namespace SingleMultiThreadDemo
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"主线程代码 ------ 线程ID:{Thread.CurrentThread.ManagedThreadId}");
// 多线程核心:创建新线程,新线程有独立ID
Thread newThread = new Thread(() =>
{
Console.WriteLine($"新线程代码 ------ 线程ID:{Thread.CurrentThread.ManagedThreadId}");
});
newThread.Start();
// 线程池/Task.Run也是多线程(常用)
System.Threading.Tasks.Task.Run(() =>
{
Console.WriteLine($"线程池线程代码 ------ 线程ID:{Thread.CurrentThread.ManagedThreadId}");
});
// 等一下,避免程序提前退出
Thread.Sleep(1000);
}
}
}
执行结果:
cs
主线程代码 ------ 线程ID:1
新线程代码 ------ 线程ID:7
线程池线程代码 ------ 线程ID:6
多线程这里需要注意的是,如果创建了其他线程,但是没有启动,那么程序还是运行在主线程,所以还是单线程的。
3.3 异步与多线程
需要重点注意的是,异步不一定代表多线程,多线程只是实现异步的其中一种方法,单线程也可以实现异步,只要保障不让程序处于等待状态就行了,比如下面这个方法,你不需要能每行代码都理解,但是你需要理解异步不只可以由多线程实现这个思想。
cs
namespace AsyncNotMultiThreadDemo
{
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("===== 单线程异步(无多线程) =====");
// 打印主线程ID(全程唯一)
Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("1. 点击下载歌曲,手机开始处理下载请求");
// Task.Delay:异步等待(无新线程,靠操作系统定时器)
var downloadTask = Task.Delay(2000); // 模拟下载耗时2秒
// 异步核心:不等下载完成,直接干别的(刷歌单)
Console.WriteLine("2. 不等下载完成,继续刷歌单(异步)");
Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); // 还是主线程
// 等下载完成(全程无新线程)
await downloadTask;
Console.WriteLine("3. 下载完成!当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
}
}
}
Task.Delay()与Thread.Sleep()不同,Task.Delay 靠操作系统定时器实现,不阻塞当前线程,所以能单线程异步等待,Thread.Sleep()是只能单线程同步等待,所以在这里使用Task.Delay()时,主程序并不会停下来等待,而是立即执行后面的代码,当代码执行到await downloadTask;时,才会等待计时结束。于是就实现了非多线程的异步。那我们能不能把这个方法改成同步呢?
cs
namespace AsyncNotMultiThreadDemo
{
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("===== 单线程异步(无多线程) =====");
// 打印主线程ID(全程唯一)
Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("1. 点击下载歌曲,手机开始处理下载请求");
// Task.Delay:异步等待(无新线程,靠操作系统定时器)
var downloadTask = Task.Delay(2000); // 模拟下载耗时2秒
// 等下载完成(全程无新线程)
await downloadTask;
// 异步核心:不等下载完成,直接干别的(刷歌单)
Console.WriteLine("2. 不等下载完成,继续刷歌单(异步)");
Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); // 还是主线程
Console.WriteLine("3. 下载完成!当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
}
}
}
当我们把await downloadTask;这个等待指令放到执行前面后,主线程就无法往下执行代码,而是会卡在这里,于是这个就又变成了同步。
然鹅,我们实现异步最常用的还是多线程,我们再举一个例子:
cs
namespace AsyncNotMultiThreadDemo
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("===== 多线程异步 =====");
Console.WriteLine($"主线程ID:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("1. 喊室友帮我下载歌曲");
// 新建线程(多线程)实现异步
Thread newThread = new Thread(() =>
{
Console.WriteLine($"室友线程ID:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000); // 模拟下载2秒
Console.WriteLine("2. 室友:下载完成!");
});
newThread.Start();
// 异步核心:不等室友,自己刷歌单
Console.WriteLine("3. 不等室友,我继续刷歌单(异步)");
Thread.Sleep(3000); // 等室友完成,避免程序提前退出
}
}
}
这个代码中,我们并不关心是由线程是什么时候结束的,我们用Thread.Sleep(3000)在主线程等待了一个很长的时间来确保它一定可以结束。
cs
===== 多线程异步 =====
主线程ID:1
1. 喊室友帮我下载歌曲
3. 不等室友,我继续刷歌单(异步)
室友线程ID:7
2. 室友:下载完成!
但是,如果规定了室友线程什么时候结束呢?
cs
namespace AsyncNotMultiThreadDemo
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("===== 多线程同步 =====");
Console.WriteLine($"主线程ID:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("1. 喊室友帮我下载歌曲");
// 新建线程(多线程)实现异步
Thread newThread = new Thread(() =>
{
Console.WriteLine($"室友线程ID:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000); // 模拟下载2秒
Console.WriteLine("2. 室友:下载完成!");
});
newThread.Start();
newThread.Join();
// 异步核心:不等室友,自己刷歌单
Console.WriteLine("3. 等室友,室友执行结束我才可以刷歌单(同步)");
}
}
}
现在,我只是在调用新的线程后加了一行代码:newThread.Join(),这个代码告诉主线程,你必须在这里等待室友线程执行结束,你才可以继续往下执行,于是就变成了同步,这里的同步不是单线程同步,而是主线程同步等待其他线程执行完成。
执行结果:
cs
===== 多线程同步 =====
主线程ID:1
1. 喊室友帮我下载歌曲
室友线程ID:7
2. 室友:下载完成!
3. 等室友,室友执行结束我才可以刷歌单(同步)
总结:
- 不管是何种类型的同步异步,我们只要关心主线程是否会等待,换句话说,我们只要关心主线程是否会阻塞。
- 不管是何种类型的单线程多线程,我们只要关心程序是运行在几个线程上的,即有没有创建新的线程并且启动新的线程。