C#中async/await的线程ID变化情况
一、简单的起步
Console.WriteLine($"主线程开始ID:{Thread.CurrentThread.ManagedThreadId}");//aawait Task.Delay(100);//cConsole.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}");//aTask task = Task.Run(() =>{Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//bThread.Sleep(10);//cConsole.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d});await Task.Delay(100);//eConsole.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//fConsole.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}");//aTask task = Task.Run(() =>{Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//bThread.Sleep(1000);//cConsole.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d});await Task.Delay(100);//eConsole.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//fConsole.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}");//aTask task = Task.Run(() =>{Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//bThread.Sleep(10);//cConsole.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d});Thread.Sleep(1000);//eConsole.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}");//aTask task = Task.Run(async () =>{Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//bawait Task.Delay(10);//cConsole.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d});Thread.Sleep(1000);//eConsole.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}");//aTask task = Task.Run(async () =>{Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//bawait Task.Delay(10);//cConsole.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d});await Task.Delay(1000);//eConsole.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}");//aTask task = Task.Run(async () =>{Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//bawait Task.Delay(1000);//cConsole.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d});await Task.Delay(10);//eConsole.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}]");//aawait Task.Run(async () =>{Console.WriteLine($"异步线程开始ID:[{Environment.CurrentManagedThreadId}]");//bawait Task.Delay(1000);//cConsole.WriteLine($"异步线程结束ID:[{Environment.CurrentManagedThreadId}]");//d}).ConfigureAwait(true);//eConsole.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}]");//aawait Task.Run(async () =>{Console.WriteLine($"异步线程开始ID:[{Environment.CurrentManagedThreadId}]");//bawait Task.Delay(1000);//cConsole.WriteLine($"异步线程结束ID:[{Environment.CurrentManagedThreadId}]");//d}).ConfigureAwait(true);//eawait Task.Delay(1000);Console.WriteLine($"主线程结束ID:[{Environment.CurrentManagedThreadId}]");//f
结果:
主线程开始ID:[1]
异步线程开始ID:[3]
异步线程结束ID:[4]
主线程结束ID:[3]
f处变成了3,看来调试器总是在当前比较优闲的ID(大概猜测就是1,3,4中选秀)。
而且,最难得的是大约有10次运行,终于找到一个难得的截图:
再次证明,优化是有规则的,所以中奖概率高,但并非绝对的。
相关文章:

C#中async/await的线程ID变化情况
一、简单的起步 Console.WriteLine($"主线程开始ID:{Thread.CurrentThread.ManagedThreadId}");//aawait Task.Delay(100);//cConsole.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//b 结果: …...

网络安全—黑客技术—自学笔记
目录梗概 一、自学网络安全学习的误区和陷阱 二、学习网络安全的一些前期准备 三、网络安全学习路线 四、学习资料的推荐 想自学网络安全(黑客技术)首先你得了解什么是网络安全!什么是黑客! 网络安全可以基于攻击和防御视角来…...

功夫再高也怕菜刀。多年经验,会独立开发的机器视觉工程师,技术太强,但是找工作能力差劲
功夫再高也怕菜刀,专业的事情交给专业的人去做。 今年7月份中旬的时候,遇到一位老朋友,向我咨询某公司的信息,其实我根本不了解这家公司的情况与实力,向他说了,抱歉,我查下,等我晚上…...
numpy的多项式函数: `poly1d`
Python numpy.poly1d() numpy.poly1d()函数有助于定义一个多项式函数。它使得在多项式上应用 "自然操作 "变得容易。 语法: numpy.poly1d (arr, root, var) 参数 : arr : [array_like] 多项式系数按照幂的递减顺序给出。如果第二个参数(根)被…...

Python灰帽编程——错误异常处理和面向对象
文章目录 1. 错误和异常1.1 基本概念1.1.1 Python 异常 1.2 检测(捕获)异常1.2.1 try except 语句1.2.2 捕获多种异常1.2.3 捕获所有异常 1.3 处理异常1.4 特殊场景1.4.1 with 语句 2. 内网主机存活检测程序2.1 scapy 模块2.1.1 主要功能2.1.2 scapy 安装…...

【20230919】win11无法删除Chrome注册表项
win11无法删除Chrome注册表项 删除以下注册表项发生错误: 计算机\HKEY_LOCAL_MACHINE\SOFTWAR\Google计算机\HKEY_CURRENT_USER\Software\Google 尝试了很多删除注册表方法(例如:编辑remove.reg文件),都不行。 无法…...

TCP/IP客户端和服务器端建立通信过程
客户端和服务器端建立通信过程 使用Qt提供的类进行基于TCP的套接字通信需要用到两个类: QTcpServer:服务器类,用于监听客户端连接以及和客户端建立连接。 QTcpSocket:通信的套接字类,客户端、服务器端都需要使用。服务…...

Python ---使用Fake库向clickhouse造数据小案例
每次insert太麻烦了 先在clickhosue中建表 test_user表 CREATE TABLE dwh.test_user (name String,age Int32,address String,phone String,email String ) ENGINE MergeTree() ORDER BY name; 此时表中暂无数据 用Python脚本来造一些数据 from faker import Faker from c…...

09MyBatisX插件
MyBatisX插件 在真正开发过程中对于一些复杂的SQL和多表联查就需要我们自己去编写代码和SQL语句,这个时候可以使用MyBatisX插件帮助我们简化开发 安装MyBatisX插件: File -> Settings -> Plugins -> 搜索MyBatisx插件搜索安装然后重启IDEA 跳转文件功能 由于一个项…...
使用 Messenger 跨进程通信
什么是Messenger Messenger 也是IPC的方案之一,是基于消息的跨进程通信。基于消息是什么意思?Handler是我们最常用的消息机制,所以 Messenger 对于使用者来说就像是使用 Handler。实际上 Messenger 就是 AIDL 的上层封装而已,它们…...
Spring Cloud Gateway
路由谓词工厂 Route Predicate Factory 1. The After Route Predicate Factory spring:cloud:gateway:routes:- id: after_routeuri: https://example.orgpredicates:- After2017-01-20T17:42:47.789-07:00[America/Denver]# 用日期时间匹配 2. The Before Route Pr…...
JVM 优化技术
文章目录 JVM 优化技术概述方法内联优化说明优点内联条件 栈帧之间数据共享说明优点栈帧之间数据共享条件 JVM 优化技术 概述 JVM常见的优化技术: 方法内联优化。栈帧之间数据共享。 方法内联优化 说明 方法内联(Method Inlining)是JVM…...

【MySQL系列】- MySQL自动备份详解
【MySQL系列】- MySQL自动备份详解 文章目录 【MySQL系列】- MySQL自动备份详解一、需求背景二、Windows mysql自动备份方法2.1 复制date文件夹备份实验备份环境创建bat直接备份脚本 2 .2 mysqldump备份成sql文件创建mysqldump备份脚本 2 .3 利用WinRAR对MySQL数据库进行定时备…...

指针笔试题讲解-----让指针简单易懂(2)
目录 回顾上篇重点 : 一.笔试题 ( 1 ) 二.笔试题 ( 2 ) 科普进制知识点 (1) 二进制 (2) 八进制 (3)十六进制 三.笔试题( 3 ) 四.笔试题( 4 ) 五.笔试题( 5 ) 六.笔试题( …...
使用windbg分析dump文件的方法
https://zhuanlan.zhihu.com/p/613434365 一般操作如下: 准备工作。 打开dump文件。指定符号表文件的路径。指定可执行文件的路径。指定源码文件的路径。在windbg的命令行,输入并执行如下命令 .reload,重新加载前述数据文件。!analyze -v&a…...

【论文阅读 07】Anomaly region detection and localization in metal surface inspection
比较老的一篇论文,金属表面检测中的异常区域检测与定位 总结:提出了一个找模板图的方法,使用SIFT做特征提取,姿态估计看差异有哪些,Hough聚类做描述符筛选,仿射变换可视化匹配图之间的关系…...

SSM - Springboot - MyBatis-Plus 全栈体系(十一)
第二章 SpringFramework 五、Spring AOP 面向切面编程 6. Spring AOP 基于 XML 方式实现(了解) 6.1 准备工作 加入依赖和基于注解的 AOP 时一样。准备代码把测试基于注解功能时的 Java 类复制到新 module 中,去除所有注解。 6.2 配置 Sp…...
深度剖析贪心算法:原理、优势与实战
概述 贪心算法是一种通过每一步的局部最优选择来寻找整体最优解的方法。在每个步骤中,贪心算法选择当前状态下的最佳选项,而不考虑未来可能的影响。尽管它不能保证一定能找到全局最优解,但贪心算法通常简单且高效,适用于许多实际…...

Docker搭建DNS服务器--use
前言 DNS服务器是(Domain Name System或者Domain Name Service)域名系统或者域名服务,域名系统为Internet上的主机分配域名地址和IP地址。 安装 2.1 实验环境 IP 系统版本 角色 192.168.40.121 Ubuntu 22.10 DNS服务器 192.168.40.122 Ubuntu 22.10 测试机器 2.2 …...

“顽固”——C语言用栈实现队列
解题图解: 1、 先用stack1存储push来的数据 2、每当要pop数据时,从stack2中取,如果 stack2为空,就先从stack1中“倒”数据到stack2。 这就是用栈实现队列的基本操作 这道题看起来比较容易,但是!如果你用C语…...

Linux应用开发之网络套接字编程(实例篇)
服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …...

CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以? 在 Golang 的面试中,map 类型的使用是一个常见的考点,其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...

.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...
postgresql|数据库|只读用户的创建和删除(备忘)
CREATE USER read_only WITH PASSWORD 密码 -- 连接到xxx数据库 \c xxx -- 授予对xxx数据库的只读权限 GRANT CONNECT ON DATABASE xxx TO read_only; GRANT USAGE ON SCHEMA public TO read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only; GRANT EXECUTE O…...

屋顶变身“发电站” ,中天合创屋面分布式光伏发电项目顺利并网!
5月28日,中天合创屋面分布式光伏发电项目顺利并网发电,该项目位于内蒙古自治区鄂尔多斯市乌审旗,项目利用中天合创聚乙烯、聚丙烯仓库屋面作为场地建设光伏电站,总装机容量为9.96MWp。 项目投运后,每年可节约标煤3670…...

vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...

云原生玩法三问:构建自定义开发环境
云原生玩法三问:构建自定义开发环境 引言 临时运维一个古董项目,无文档,无环境,无交接人,俗称三无。 运行设备的环境老,本地环境版本高,ssh不过去。正好最近对 腾讯出品的云原生 cnb 感兴趣&…...

sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!
简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求,并检查收到的响应。它以以下模式之一…...