C#中async/await的线程ID变化情况

news2024/11/20 6:28:30

    

一、简单的起步

    Console.WriteLine($"主线程开始ID:{Thread.CurrentThread.ManagedThreadId}");//a
    await Task.Delay(100);//c
    Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//b


    结果:
        主线程开始ID:1
        主线程结束ID:4        
    
    1、问:async/await会创建新线程吗?
        答:async和await并不会直接创建新的线程,而是通过利用异步机制来实现非阻塞的异步操作。
        
            C#中的async和await关键字并不会创建新的线程。它们实际上是用于异步编程的语法糖。

            当使用async关键字修饰一个方法时,该方法可以被视为一个异步方法。在异步方法内部,可以使用await关键字来等待其他异步操作的完成。

            当遇到await关键字时,异步方法会暂时挂起,让出当前线程的控制权,而不会阻塞线程。当被await的异步操作完成后,异步方法会恢复执行,并返回结果。

            在大多数情况下,异步操作并不会创建新的线程,而是通过利用I/O完成端口或其他异步机制来实现异步操作。这样可以避免创建额外的线程,提高程序的性能和资源利用率。

            注意,如果使用了Task.Run等方法来包装一个同步的阻塞操作,那么它可能会在新的线程上执行。这样做的目的是为了将阻塞操作转换为异步操作,以避免阻塞主线程。


    2、问:异步方法会暂时挂起,是什么意思?
        答:在遇到await Task.Delay(100)时,异步方法会暂时挂起,并让出当前线程的控制权。这里的挂起并不是指线程被挂起或阻塞,而是指异步方法暂时停止执行,并将控制权返回给调用它的线程(主线程)。
        
            当遇到await Task.Delay(100)时,它会创建一个延时任务,该任务会在指定的时间(这里是100毫秒)后完成。然后,异步方法会注册一个回调函数,告诉任务完成后要继续执行下一步。
            在挂起期间,异步方法不会占用线程资源,而是让线程可以执行其他任务。这样可以提高程序的并发性和资源利用率。
            
            一旦延时任务完成,异步方法会被唤醒,并继续执行后续的代码。这时,并不是创建新的线程来执行延时操作,而是通过异步机制来实现非阻塞的延时操作。
            
            具体来说,当异步方法遇到await Task.Delay(100)时,它会将延时任务交给.NET运行时的任务调度器(Task Scheduler)管理。任务调度器会将延时任务放入等待队列中,并继续执行其他任务。
            
            在指定的时间(100毫秒)后,任务调度器会将延时任务标记为完成,并将其添加到就绪队列中。当调度器调度到该任务时,它会通知异步方法继续执行,并返回到原来的线程(主线程)上。
            
            总之,异步方法的挂起并不是线程的挂起或阻塞,而是暂时停止执行,并让出当前线程的控制权。在挂起期间,线程可以执行其他任务。异步方法通过异步机制来实现非阻塞的延时操作,让出当前线程的控制权,并在延时任务完成后继续执行。
        
        
    3、问:遇到await时主线程在做啥,玩泥巴吗?
        答:是的,它不是阻塞,而是去干其它事去了。
        
            当遇到await关键字时,主线程会暂时挂起(挂起点),这并不会阻塞主线程的执行,而是让出当前线程的控制权,允许主线程去执行其他任务。
            
            同时,await关键字会将异步操作交给任务调度器来管理。任务调度器会根据当前的线程池状态和调度策略,将异步操作分配给适当的线程执行。
            
            当异步操作完成后,任务调度器会通知异步方法继续执行。这时,可能会发生线程切换,执行剩下的代码的线程可能是之前执行异步操作的线程,也可能是其他线程。
            
            这种机制使得异步方法能够以非阻塞的方式执行,并允许主线程在等待异步操作完成时继续执行其他任务,提高了程序的并发性和响应性。
            
            注意,异步方法的挂起和恢复是由任务调度器来管理和控制的,具体的线程调度和切换机制是由.NET运行时来处理的。开发人员并不需要显式地关注线程的创建和管理,而是通过使用async和await来编写简洁、清晰的异步代码。

    
    4、问:await也要开线程吧?
        答:不一定,大多数情况下,异步操作并不会创建新的线程,而是利用异步机制(如I/O完成端口)来实现非阻塞的异步操作。
        
            通过使用异步机制,可以将阻塞的I/O操作转换为异步的操作,而不需要创建新的线程。这样可以避免线程的创建和销毁,提高程序的性能和资源利用率。
            
            除了线程池中的线程,调度器也可以使用其他的执行上下文,比如使用事件触发器或计时器来执行异步操作。这种情况下,调度器会将异步操作添加到事件队列或计时器队列中,并在适当的时候触发事件或计时器来执行异步操作。
            
            然而,有些情况下,异步操作可能会创建新的线程。例如:

            (1)使用Task.Run等方法:
            如果使用Task.Run等方法来包装一个同步的阻塞操作,那么它可能会在新的线程上执行。这样做的目的是为了将阻塞操作转换为异步操作,以避免阻塞主线程。

            (2)自定义线程池:
            在某些情况下,开发人员可以自定义线程池来控制异步操作的执行。这可能涉及到线程的创建和管理,以满足特定的需求。

            注意,创建新的线程可能会增系统资源的开销,并且需要进行线程同步和管理。因此,在设计和实现异步操作时,该根据实际情况和需求来选择合适的方式,以平衡性能、资源利用率和代码复杂性。
            
            总结,大多数情况下,异步操作不会创建新的线程,而是利用异步机制来实现非阻塞的操作。但在某些情况下,可能会涉及到创建新的线程来执行异步操作,以满足特定的需求。
    
    
    5、问: await完成后,主线程的ID可能不是1了?
        答:是的。
        
            当遇到await关键字时,主线程会暂时挂起(挂起点),这并不会阻塞主线程的执行,而是让出当前线程的控制权,允许主线程去执行其他任务。
            
            同时,await关键字会将异步操作交给任务调度器来管理。任务调度器会根据当前的线程池状态和调度策略,将异步操作分配给适当的线程执行。
            
            当异步操作完成后,任务调度器会通知异步方法继续执行。这时,可能会发生线程切换,执行剩下的代码的线程可能是之前执行异步操作的线程,也可能是其他线程。
            
            这种机制使得异步方法能够以非阻塞的方式执行,并允许主线程在等待异步操作完成时继续执行其他任务,提高了程序的并发性和响应性。
            
            具体的来说就是:
            当异步操作完成后,任务调度器会通知异步方法继续执行,具体是通过将执行权从之前的线程切换回到原来挂起点的代码,然后继续执行下面的代码。
            
            在异步方法中,遇到await关键字时,会将await之后的代码封装为一个延续(continuation),并注册到异步操作的完成事件上。
            
            当异步操作完成后,任务调度器会将延续添加到就绪队列中,等待调度执行。一旦调度器调度到该延续,它会通知异步方法继续执行,切换回原来的挂起点。
            这个通知是通过线程切换和调度机制实现的。具体来说,任务调度器会选择一个可用的线程(可能是之前执行异步操作的线程,也可能是其他线程),并将执行权转移给该线程。这样,异步方法就可以继续执行await之后的代码。
            
            注意,异步方法的继续执行并不是立即发生的,而是在调度器选择并分配线程之后才会发生。具体的线程调度和切换机制是由.NET运行时和任务调度器来处理的,开发人员不需要显式地管理和控制。
            
            总之,异步操作完成后,任务调度器会通过线程切换和调度机制将执行权切换回原来的挂起点,通知异步方法继续执行下面的代码。这样可以实现非阻塞的异步操作和代码的顺序执行。
    

    6、问:那上面的的await应该有答案了吗?
        答:是的,上面可以知道在c处挂起,主线程玩泥巴,这个延时交给任务调度器(不一定是创建线程,也可能是事件回调机制),延时完成后回来,任务调度器会选择一个可用线程(可能是主线程,可能是前面异步操作线程,也有可能是新的其它线程,谁闲谁知道呢,服从领导就OK啦),继续执行c处后面的代码。所以d的ID是随机的,谁也说不准。
    
    
    7、问:什么是上下文?
        答:人话就是,上下文(Context)是指执行代码时所处的环境和状态。它包含了一些与执行相关的信息,如线程调度器、同步上下文、同步上下文流动等。
            比如,你工作的场所,场景,环境。有电脑,笔,桌子,办公室,等等。
    
    
    8、问:所有线程都有上下文?
        答:在异步编程中,线程执行时都会有上下文。上下文提供了执行环境和状态,包括线程的调度、同步上下文、同步上下文流动等。
            人话就是:所有鱼都有自己的生存环境。
    
    
    9、问:上下文的切换都会有消耗资源?
        答:对的。
            切换线程上下文可能会涉及一些开销,包括线程的切换、上下文的保存和恢复等。这是因为不同的线程可能具有不同的执行环境和状态,需要进行一些额外的操作来确保正确的执行。
            人话就是:如果你原来在A处办公,现在调整到B处去办公,你当然需要搬运、布置,打扫等工作,肯定有些许时间的消耗。
        
  


二、再添加一个异步

    Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
    Task task = Task.Run(() =>
    {
        Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
        Thread.Sleep(10);//c
        Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
    });
    await Task.Delay(100);//e
    Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f
    Console.ReadKey();    


    结果:
        主线程开始ID:1
        异步线程ID开始:3
        异步线程ID结束:3
        主线程结束ID:4    
    a处为主线程ID为1,然后新开一个异步线程task,一闪而过去执行e处,又是一个异步线程但有await。所以f是随机的,可能是1,可能是4,但不可能是3,因为此时3被c处占用。
    对于b和d,因为c是同步线程,所以b和d都是在同一个线程中执行,它们的ID是相同的为3.
    
    
    修改一:将C的10毫秒改变为1000毫秒。

    Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
    Task task = Task.Run(() =>
    {
        Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
        Thread.Sleep(1000);//c
        Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
    });
    await Task.Delay(100);//e
    Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f
    Console.ReadKey();    


    结果:
        主线程开始ID:1
        异步线程ID开始:3
        主线程结束ID:4
        异步线程ID结束:3    
    同样b和d仍然同一线程内,肯定相同,所以两者为3。
    e处之后,f的ID是随机的,但它不可能是3,此是3仍然在c处占用.
    
    
    修改二:把e处改为thread.Sleep(1000)

    Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
    Task task = Task.Run(() =>
    {
        Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
        Thread.Sleep(10);//c
        Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
    });
    Thread.Sleep(1000);//e
    Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f


    结果:
        主线程开始ID:1
        异步线程ID开始:3
        异步线程ID结束:3
        主线程结束ID:1    
    bcd处一样为3.
    e处为同步。由主线程执行ID为1,所以后面的f处也为1.
    


三、新加异步中的Await


    1、两个线程,第一个异步中异步,第二同步:

        Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
        Task task = Task.Run(async () =>
        {
            Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
            await Task.Delay(10);//c
            Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
        });
        Thread.Sleep(1000);//e
        Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f    


        结果:
            主线程开始ID:1
            异步线程ID开始:3
            异步线程ID结束:4
            主线程结束ID:1
        e处为同步线程,在主线程中,所以到了f时是主线程。
        b处由Task.Run申请的线程,ID为3,经过c后,原ID为3的挂起,在延时做完后,恢复执行后面代码时,由调度器选择线程来执行后面的d处,所以d的ID是随机的,但不可能是1.
        
        
    2、两个线程,第一个异步中异步,第二次await异步。

        Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
        Task task = Task.Run(async () =>
        {
            Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
            await Task.Delay(10);//c
            Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
        });
        await Task.Delay(1000);//e
        Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f


        结果: 
            主线程开始ID:1
            异步线程ID开始:3
            异步线程ID结束:5
            主线程结束ID:3    
        b处为异步线程ID为3,经过C处后,d处随机,显示为5.
        e处返回时,b,d使用的线程(3和5)已经返回给线程池,也即线程池是有可能再次给e后面分配1,3,5等,所以这里显示是3.
        
    
    3、修改上面,把延时调整一下,c处占久点

        Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
        Task task = Task.Run(async () =>
        {
            Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
            await Task.Delay(1000);//c
            Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
        });
        await Task.Delay(10);//e
        Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f


        结果:
            主线程开始ID:1
            异步线程ID开始:3
            主线程结束ID:5
            异步线程ID结束:6
        b处分配ID为3,然后c处等待,在d处随机得到分配的ID,由于它是1000毫秒后,即所有任务都执行完成了,只有它,所以它的随机分配是任何可能,可以是3,5,6等。
        e处过后,返回时由于b处占用了3(哪怕是挂起),所以在f处调度器随机分配线程不可能ID为3,所以上面分配的是5.


    
四、Configureawait的失效

    Console.WriteLine($"主线程开始ID:[{Environment.CurrentManagedThreadId}]");//a
    await Task.Run(async () =>
    {
        Console.WriteLine($"异步线程开始ID:[{Environment.CurrentManagedThreadId}]");//b
        await Task.Delay(1000);//c
        Console.WriteLine($"异步线程结束ID:[{Environment.CurrentManagedThreadId}]");//d
    }).ConfigureAwait(true);//e
    Console.WriteLine($"主线程结束ID:[{Environment.CurrentManagedThreadId}]");//f


    结果:
        主线程开始ID:[1]
        异步线程开始ID:[3]
        异步线程结束ID:[4]
        主线程结束ID:[4]
    上面b处ID结果为3,然后经c处后,在d处随机分配。结果是4.
    f处由于前面await的原因,同样也是随机分配,它是最后执行,所以ID有任意的可能(看调度器的分配了)。
    
    这里,说明的是无论Configiure为True还是False,最后都要以await结束,都要返回到主线程的上下文中,所以它“失效了”.
    

    Task.Run会将异步操作放入线程池中执行,而await会在异步操作完成之前阻塞主线程。当异步操作完成后,会尝试切换回主调线程执行await之后的代码。无论e处的参数是true还是false,异步操作完成后,恢复时都会尝试切换回主调线程执行d处或者f处的代码。
    
    
    
    问:为什么d与f处的线程大多数是一样的?
    答:这个不是绝对的。按优化概率可能是这样。
        调度器通常会尽量将执行权切换回刚完成的异步线程,以继续执行原先挂起的代码。这种方式可以减少线程切换和上下文切换的成本,提高执行效率。
        
        当一个异步任务完成后,调度器会考虑以下几个因素来决定是否将执行权切换回刚完成的异步线程:

        (1)异步线程的可用性:
        如果刚完成的异步线程仍然可用,调度器会优先选择它来执行后续代码,因为这样可以避免线程切换的开销。

        (2)异步线程的负载:
        如果刚完成的异步线程当前正在执行其他任务,调度器可能会选择一个空闲的线程来执行后续代码,以平衡负载。

        (3)上下文切换的成本:
        如果切换到刚完成的异步线程的上下文比切换到其他线程的上下文更低廉,调度器可能会优先选择它来执行后续代码。

        注意,具体的调度策略和行为取决于调度器的实现和配置。不同的调度器可能有不同的优化策略和行为。因此,在实际应用中,可能会出现一些例外情况,导致执行权并不会立即切换回刚完成的异步线程。
    
    
    
    改变一下它的优化,再增加一句await:

    Console.WriteLine($"主线程开始ID:[{Environment.CurrentManagedThreadId}]");//a
    await Task.Run(async () =>
    {
        Console.WriteLine($"异步线程开始ID:[{Environment.CurrentManagedThreadId}]");//b
        await Task.Delay(1000);//c
        Console.WriteLine($"异步线程结束ID:[{Environment.CurrentManagedThreadId}]");//d
    }).ConfigureAwait(true);//e
    await Task.Delay(1000);
    Console.WriteLine($"主线程结束ID:[{Environment.CurrentManagedThreadId}]");//f    


    结果:
        主线程开始ID:[1]
        异步线程开始ID:[3]
        异步线程结束ID:[4]
        主线程结束ID:[3]    
    f处变成了3,看来调试器总是在当前比较优闲的ID(大概猜测就是1,3,4中选秀)。
    
    而且,最难得的是大约有10次运行,终于找到一个难得的截图:
  


    再次证明,优化是有规则的,所以中奖概率高,但并非绝对的。
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1000873.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL复合查询(查询直接看这里)

回顾基本查询 查询工资高于500或岗位为TOM的雇员,同时还要满足他们的姓名首字母为大写的J select * from EMP where(sale > 500 or job TOM) and ename like J%;按照部门号升序而雇员的工资降序排序 select * from EMP order by deptno, sal desc;最后&#…

CANoe中的工作模式之争:由一段简单的代码引出的问题

1、引子 有网友问我一个CAPL中timer定时器的代码问题。他在CANoe工程中写了一段代码:每5秒循环触发一次定时器事件程序,输出一句文本信息到Write窗口。但是执行后发现并不是每5秒触发一次定时器事件程序,而是非常快的触发定时器事件程序。当他把这段代码复制到一个新的CANo…

【integrin + vWFa vWF】

CR3 ; CR4 ; 也有 vWFA CD11 CD18 vWF --Crystal structure and substrate-induced activation of ADAMTS13 神奇! HGNC:12726 vWF 12p13.31 HGNC: 7 a2M 12p13.31

微服务之流控、容错组件sentinel

背景 2012年阿里巴巴研发的流量治理组件,核心功能流控、容错 有什么功能 流量控制 流量控制 网关控制 黑白名单 熔断降级 熔断 保护分布式系统防止因为调用下有服务时产生故障或者请求超时等异常影响上游服务,使用熔断方案,类似断路器…

城市内涝监测预警系统:有效降低内涝风险,保障城市安全

近日,受台风“海葵”的影响,福建广东多地遭遇了持续性强降雨的袭击,道路积水严重,“城市看海”模式再次开启,不少网友纷纷调侃房子已经升级为海景房。近年来受极端天气影响,城市内涝灾害越发凸显&#xff0…

vscode 画流程图

文章目录 1、安装插件 draw2、新建文件3、开始画图4、另存为图片 vscode可以画流程图了,只需要安装插件就可以了。 1、安装插件 draw 2、新建文件 3、开始画图 4、另存为图片

小程序中如何查看会员的等级及变更记录

会员等级变更记录是了解用户购买行为和消费习惯的重要依据。下面就将介绍如何怎么查看会员的等级以及等级变更记录。 1. 找到指定的会员卡。在管理员后台->会员管理处,找到需要查看等级和记录的会员卡。也支持对会员卡按卡号、手机号和等级进行搜索。在这个页面…

后端太卷,我不玩了!

作者:阿秀 InterviewGuide大厂面试真题网站:https://top.interviewguide.cn 这是阿秀的第「303」篇原创 小伙伴们大家好,我是阿秀。 校招岗位形势是一直在变化的,并不是一成不变的,从18、19、20年这三年里的算法岗大热…

1-2 AUTOSAR规范文档

目录 一、AUTOSAR文档下载 二、AUTOSAR文档分类 三、软件设计规范文档解读(SWS) 一、AUTOSAR文档下载 AUTOSAR规范文档下载可以到AUTOSAR官网(Home AUTOSAR)进行下载。 下载操作如下图所示: 二、AUTOSAR文档分类 AU…

golang面试题:json包变量不加tag会怎么样?

问题 json包里使用的时候,结构体里的变量不加tag能不能正常转成json里的字段? 怎么答 如果变量首字母小写,则为private。无论如何不能转,因为取不到反射信息。如果变量首字母大写,则为public。 不加tag&#xff0c…

【C++11】{}初始化、std::initializer_list、decltype、STL新增容器

文章目录 1. C11简介2. 统一的列表初始化2.1 {}初始化2.2 std::initializer_list 3. 声明3.1 auto3.2 decltype 4. nullptr5. 范围for循环6. 智能指针7. C11STL中的一些变化8. 演示代码 1. C11简介 在2003年C标准委员会曾经提交了一份技术勘误表(简称TC1…

Kotlin面向对象基础使用方法(继承、接口、Lambda、空指针检查机制等)

三、面向对象 1、继承 1.1 open改变类的继承属性 在kotlin设计时默认所有的非抽象类是无法被继承的,如果想要使得一个非抽象类可以被继承,我们需要使用open关键字。 open class Person {var name "";var age 0;fun eat() {println(name …

入门人工智能 ——自然语言处理介绍,并使用 Python 进行文本情感分析(5)

入门人工智能 ——自然语言处理介绍,并使用 Python 进行文本情感分析(5)) 入门人工智能 ——自然语言处理介绍,并使用 Python 进行文本情感分析介绍自然语言处理的挑战NLP的基本任务NLP的基本技术NLP的应用领域 使用 P…

RHCSA Linux环境搭建

目录 一、安装Linux操作系统 二、创建虚拟机 1、成功激活后,开始“创建新的虚拟机” 新建虚拟机 2、自定义--根据我们的需求来创建 3、默认即可 4、选择稍后安装操作系统(可自定义设置某些选项) 5、选择Linux操作系统,版本…

Linux dup dup2函数

/*#include <unistd.h>int dup2(int oldfd, int newfd);作用&#xff1a;重定向文件描述符oldfd 指向 a.txt, newfd 指向b.txt,调用函数之后&#xff0c;newfd和b.txt close&#xff0c;newfd指向a.txtoldfd必须是一个有效的文件描述符 */ #include <unistd.h> #i…

Fourier傅里叶变换的线性性质和位移性质

Fourier傅里叶变换的线性性质和位移性质 为了阐述方便, 假定在这些性质中, 凡是需要求Fourier变换的函数都满足Fourier积分定理中的条件。在证明这些性质时, 不再重述这些条件。 一、线性性质 设 F 1 ( ω ) F [ f 1 ( t ) ] {F_1}(\omega ) {\mathscr F}[{f_1}(t)] F1​(…

2023/9/11 -- C++/QT

作业 仿照string类&#xff0c;完成myString 类 02mystring.h: #ifndef __02MYSTRING_H__ #define __02MYSTRING_H__#include <iostream> #include <cstring>using namespace std;class myString{ private:char *str;int size; public://无参构造myString();//有…

C++算法 —— 动态规划(5) 子序列

文章目录 1、动规思路简介2、最长递增子序列3、摆动序列4、最长递增子序列的个数5、最长数对链6、最长定差子序列7、最长斐波那契子序列的长度8、最长等差数列9、等差数列划分 II 每一种算法都最好看完第一篇再去找要看的博客&#xff0c;因为这样会帮你梳理好思路&#xff0c;…

Python 图形化界面基础篇:添加标签( Label )到 Tkinter 窗口

Python 图形化界面基础篇&#xff1a;添加标签&#xff08; Label &#xff09;到 Tkinter 窗口 引言什么是 Tkinter 标签&#xff08; Label &#xff09;&#xff1f;步骤1&#xff1a;导入 Tkinter 模块步骤2&#xff1a;创建 Tkinter 窗口步骤3&#xff1a;创建标签&#x…

kafka管理工具之kafka-ui的环境搭建笔记

由于项目需要kafka支持认证功能&#xff0c;就把kafka升级到3.2.0了。之前一直使用的kafka tools(现在叫Offset Explorer&#xff0c;个人使用免费&#xff0c;商用付费)&#xff0c;开了认证之后就不好用了&#xff0c;卡的很&#xff0c;一点也不丝滑了&#xff0c;于是只好重…