.NET进阶——深入理解线程(1)同步异步与单线程多线程的区分

今天我们来详细介绍一下多线程,多线程是.NET中非常重要的知识点,需要完全掌握。

一、什么是多线程?

在了解线程 之前,我们需要知道什么是进程 ,所谓进程 ,就是指操作系统中运行的程序,比如我们自己写的在运行的.NET程序,或者是正在运行的QQ、微信、浏览器等,这些都叫做一个进程,进程与进程之间保持独立,比如浏览器崩了不会影响微信。

如果操作系统是一个工厂,那么进程 就是一个车间 ,而车间里的工人就是线程 。如果一个进程 里只有一个工人,那就是单线程 ,由于只有一个工人,所以效率比较慢;如果一个进程 里有多个工人,那就是多线程,由于可以多个工人相互配合,所以效率相对较高。

二、什么是同步异步?

这里需要重点介绍一下同步异步 的知识,因为总有小伙伴会将同步异步单线程多线程搞混。

  • 同步 :你自己点外卖,拿着手机一直等:等商家接单、等商家做、等骑手送,期间你啥也干不了,就傻坐着等(这就是「线程阻塞」)。

👉 不管是 1 个人干(单线程)还是 2 个人干(多线程),只要你等事儿干完,就是同步。

  • 异步 :你用 APP 点完外卖,把手机一扔,该刷剧刷剧、该打游戏打游戏,等外卖到了(手机响通知)你再去拿 ------不等事儿干完,先干别的

👉 这就是异步,重点是「不傻等」,和 "几个人干活" 没关系。

三、单线程多线程与同步异步的区别

  1. 同步 / 异步:只看你「要不要等事儿干完」(做事的方式);
  2. 单线程 / 多线程:只看你「有几个人干活」(干活的人)。

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. 等室友,室友执行结束我才可以刷歌单(同步)

总结:

  • 不管是何种类型的同步异步,我们只要关心主线程是否会等待,换句话说,我们只要关心主线程是否会阻塞。
  • 不管是何种类型的单线程多线程,我们只要关心程序是运行在几个线程上的,即有没有创建新的线程并且启动新的线程。
相关推荐
编程乐趣4 小时前
qdrant-dotnet:官方提供的开源 .NET 客户端库,用于与 Qdrant 向量搜索引擎操作!
c#·.net
SmoothSailingT4 小时前
C#——单例模式
开发语言·单例模式·c#
Lv11770084 小时前
Visual Studio 中的字符串
ide·笔记·c#·visual studio
Lv11770084 小时前
Visual Studio中的 var 和 dynamic
ide·笔记·c#·visual studio
wuguan_4 小时前
C#之List数组
开发语言·c#·list
工程师0075 小时前
C# 反射与泛型深度结合详解
c#·反射·泛型
feifeigo1235 小时前
C#中实现控件拖动功能
开发语言·c#
曹牧5 小时前
C#:List<string>类型的集合转换成用逗号分隔的字符串
开发语言·c#·list
fengfuyao9855 小时前
基于C# WinForm的收银管理系统实现
开发语言·c#