皮皮网

皮皮网

【源码之家 棋牌】【ueditor 源码解析】【raspberry pi 源码】park源码

时间:2024-11-19 03:45:31 分类:探索

1.Rust并发:标准库sync::Once源码分析
2.Java 中LockSupport类在C#中的实现
3.从HotSpot源码,源码深度解读 park 和 unpark
4.线程池中空闲的源码线程处于什么状态?
5.Golang GMP 原理

park源码

Rust并发:标准库sync::Once源码分析

       一次初始化同步原语Once,其核心功能在于确保闭包仅被执行一次。源码常见应用包括FFI库初始化、源码静态变量延迟初始化等。源码

       标准库中的源码源码之家 棋牌Once实现更为复杂,其关键在于如何高效地模拟Mutex阻塞与唤醒机制。源码这一机制依赖于线程暂停和唤醒原语thread::park/unpark,源码它们是源码实现多线程同步对象如Mutex、Condvar等的源码基础。

       具体实现中,源码Once维护四个内部状态,源码状态与等待队列头指针共同存储于AtomicUsize中,源码利用4字节对齐优化空间。源码

       构造Once实例时,源码初始化状态为Incomplete。调用Once::call_once或Once::call_once_force时,分别检查是否已完成初始化,未完成则执行闭包,闭包执行路径标记为冷路径以节省资源,同时避免泛型导致的代码膨胀。

       闭包执行逻辑由Once::call_inner负责,线程尝试获取执行权限,未能获取则进入等待状态,获取成功后执行闭包,结束后唤醒等待线程。

       等待队列通过无锁侵入式链表实现,节点在栈上分配,以优化内存使用。Once::wait函数实现等待线程逻辑,WaiterQueue的drop方法用于唤醒所有等待线程,需按特定顺序操作栈节点,以避免use after free等潜在问题。

       思考题:如何在实际项目中利用Once实现资源安全共享?如何评估Once与Mutex等同步原语在不同场景下的性能差异?

Java 中LockSupport类在C#中的实现

          Java 之后提供优秀的并发库ncurrent Net中缺乏类似的功能 由于硬件体系发生了变化 多核时代来临 NET中缺乏并发类库显然不合时宜 缓解这一矛盾的其中一个办法就是在往C#中移植java的ncurrent

          java中的ncurrent包中提供了一个类LockSupport ncurrent包很多关键实现需要调用LockSupport 如果需要把java的ncurrent包迁移到C#中 LockSupport类的迁移是不可避免的问题

          在java中 LockSupport类有如下方法

          以下是引用片段

          

       

          public static void park(Object blocker) {   Thread t = Thread currentThread();   setBlocker(t  blocker);   unsafe park(false   L);   setBlocker(t  null);   }

          当一个线程调用LockSupport park之后 线程就会停下载 类似于Object wait 或者 NET中的System Threading Monitor Wait 但问题是java中的Object wait和 NET中的Monitor wait 都需要一个waitObject 这个问题曾经困扰我 为此翻了一遍JDK 实现源码 到最后发现的解决办法却是很简单 也无需了解JDK的底层实现源码

          以下是引用片段

          

          public class LockSupport   {   private static LocalDataStoreSlot slot = Thread GetNamedDataSlot ( LockSupport Park );   public static void Park(Object blocker)   {   Thread thread = Thread CurrentThread;   Thread SetData(slot  blocker);   lock (thread)   {   Monitor Wait(thread);   }   }   public static void Unpark(Thread thread)   {   if (thread == null) return;   lock (thread)   {   Monitor Pulse(thread);   }   }   }

lishixinzhi/Article/program/net//

从HotSpot源码,深度解读 park 和 unpark

       我最近建立了一个在线自习室(App:番茄ToDO)用于相互监督学习,感兴趣的小伙伴可以加入。自习室加入码:D5A7A

       Java并发包下的ueditor 源码解析类大多基于AQS(AbstractQueuedSynchronizer)框架实现,而AQS线程安全的实现依赖于两个关键类:Unsafe和LockSupport。

       其中,Unsafe主要提供CAS操作(关于CAS,在文章《读懂AtomicInteger源码(多线程专题)》中讲解过),LockSupport主要提供park/unpark操作。实际上,park/unpark操作的最终调用还是基于Unsafe类,因此Unsafe类才是核心。

       Unsafe类的实现是由native关键字说明的,这意味着这个方法是原生函数,是用C/C++语言实现的,并被编译成了DLL,由Java去调用。

       park函数的作用是将当前调用线程阻塞,而unpark函数则是唤醒指定线程。

       park是等待一个许可,unpark是为某线程提供一个许可。如果线程A调用park,除非另一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。每次调用一次park,需要有一个unpark来解锁。

       并且,unpark可以先于park调用,但不管unpark先调用多少次,都只提供一个许可,不可叠加。只需要一次park来消费掉unpark带来的许可,再次调用会阻塞。

       在Linux系统下,park和unpark是通过Posix线程库pthread中的mutex(互斥量)和condition(条件变量)来实现的。

       简单来说,mutex和condition保护了一个叫_counter的信号量。当park时,这个变量被设置为0,当unpark时,raspberry pi 源码这个变量被设置为1。当_counter=0时线程阻塞,当_counter>0时直接设为0并返回。

       每个Java线程都有一个Parker实例,Parker类的部分源码如下:

       由源码可知,Parker类继承于PlatformParker,实际上是用Posix的mutex和condition来实现的。Parker类里的_counter字段,就是用来记录park和unpark是否需要阻塞的标识。

       具体的执行逻辑已经用注释标记在代码中,简要来说,就是检查_counter是不是大于0,如果是,则把_counter设置为0,返回。如果等于零,继续执行,阻塞等待。

       unpark直接设置_counter为1,再unlock mutex返回。如果_counter之前的值是0,则还要调用pthread_cond_signal唤醒在park中等待的线程。源码如下:

       (如果不会下载JVM源码可以后台回复“jdk”,获得下载压缩包)

线程池中空闲的线程处于什么状态?

       一:阻塞状态,线程并没有销毁,也没有得到CPU时间片执行;

       源码追踪:

       for (;;) {

       ...

        workQueue.take();

       ...

       }

       public E take()...{

       ...

       while (count.get() == 0) { / /这里就是任务队列中的消息数量

       notEmpty.await();

       }

       ...

       }

       public final void await()...{

       ...

       LockSupport.park(this);

       ...

       }

       继续往下:

       public static void park(Object blocker) {

       Thread t = Thread.currentThread();

       setBlocker(t, blocker);

       U.park(false, 0L);

       setBlocker(t, null);

       }

       private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();

       //线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。

       public native void park(boolean isAbsolute, long time);

       上面就是java线程池中阻塞的源码追踪;

       二.对比object的wait()方法:

       @FastNative

       public final native void wait(long timeout, int nanos) throws InterruptedException;

       还有Thread的sleep() 方法:

       @FastNative

       private static native void sleep(Object lock, long millis, int nanos)throws...;

       可见,线程池中使用的阻塞方式并不是Object中的wait(),也不是Thread.sleep() ;

       这3个方法最终实现都是通过c&c++实现的native方法.

       三.在<<Java虚拟机(第二版)>>中,对线程状态有以下介绍:

       .4.3 状态转换

       Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种

       状态,这5种状态分别如下。

       1)新建(New):创建后尚未启动的线程处于这种状态。

       2)运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是处于此

       状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。

       3)无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被

       其他线程显式地唤醒。按钮css源码以下方法会让线程陷入无限期的等待状态:

       ●没有设置Timeout参数的Object.wait()方法。

       ●没有设置Timeout参数的Thread.join()方法。

       ●LockSupport.park()方法。

       4)限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无

       须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程

       进入限期等待状态:

       ●Thread.sleep()方法。

       ●设置了Timeout参数的Object.wait()方法。

       ●设置了Timeout参数的Thread.join()方法。

       ●LockSupport.parkNanos()方法。

       ●LockSupport.parkUntil()方法。

       5)阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等

       待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状

       态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将

       进入这种状态。

       结束(Terminated):已终止线程的线程状态,线程已经结束执行。

Golang GMP 原理

       通常语义中的线程,指的是内核级线程,核心点包括:(1)它是操作系统最小调度单元;(2)创建、销毁、调度交由内核完成,cpu 需完成用户态与内核态间的切换;(3)可充分利用多核,实现并行。

       协程,又称为用户级线程,核心点如下:(1)与线程存在映射关系,为 M:1;(2)创建、销毁、调度在用户态完成,对内核透明,所以更轻;(3)从属同一个内核级线程,无法并行;一个协程阻塞会导致从属同一线程的所有协程无法执行。

       Goroutine,dota地图源码经 Golang 优化后的特殊“协程”,核心点包括:(1)与线程存在映射关系,为 M:N;(2)创建、销毁、调度在用户态完成,对内核透明,足够轻便;(3)可利用多个线程,实现并行;(4)通过调度器的斡旋,实现和线程间的动态绑定和灵活调度;(5)栈空间大小可动态扩缩,因地制宜。

       对比三个模型的各项能力:综上,goroutine 可说是博采众长之物。

       实际上,“灵活调度” 一词概括得实在过于简要,Golang 在调度 goroutine 时,针对“如何减少加锁行为”,“如何避免资源不均”等问题都给出了精彩的解决方案,这一切都得益于经典的 “gmp” 模型。

       GMP = goroutine + machine + processor(+ 一套有机组合的机制),下面先单独拆出每个组件进行介绍,最后再总览全局,对 GMP 进行总述。

       G = goroutine,是 Golang 中对协程的抽象;(2)g 有自己的运行栈、状态、以及执行的任务函数(用户通过 go func 指定);(3)g 需要绑定到 p 才能执行,在 g 的视角中,p 就是它的 cpu。

       P = processor,是 Golang 中的调度器;(2)p 是 gmp 的中枢,借由 p 承上启下,实现 g 和 m 之间的动态有机结合;(3)对 g 而言,p 是其 cpu,g 只有被 p 调度,才得以执行;(4)对 m 而言,p 是其执行代理,为其提供必要信息的同时(可执行的 g、内存分配情况等),并隐藏了繁杂的调度细节;(5)p 的数量决定了 g 最大并行数量,可由用户通过 GOMAXPROCS 进行设定(超过 CPU 核数时无意义)。

       M = machine,是 Golang 中对线程的抽象;(1)m 不直接执行 g,而是先和 p 绑定,由其实现代理;(3)借由 p 的存在,m 无需和 g 绑死,也无需记录 g 的状态信息,因此 g 在全生命周期中可以实现跨 m 执行。

       全局有多个 M 和多个 P,但同时并行的 G 的最大数量等于 P 的数量。G 的存放队列有三类:P 的本地队列;全局队列;和 wait 队列(图中未展示,为 io 阻塞就绪态 goroutine 队列)。

       M 调度 G 时,优先取 P 本地队列,其次取全局队列,最后取 wait 队列。这样的好处是,取本地队列时,可以接近于无锁化,减少全局锁竞争。为防止不同 P 的闲忙差异过大,设立 work-stealing 机制,本地队列为空的 P 可以尝试从其他 P 本地队列偷取一半的 G 补充到自身队列。

       核心数据结构定义于 runtime/runtime2.go 文件中,各个类的成员属性较多,这里只摘取核心字段进行介绍:g 的生命周期由以下几种状态组成:_Gidle(值为 0,为协程开始创建时的状态,此时尚未初始化完成);_Grunnable(值为 1,协程在待执行队列中,等待被执行);_Grunning(值为 2,协程正在执行,同一时刻一个 p 中只有一个 g 处于此状态);_Gsyscall(值为 3,协程正在执行系统调用);_Gwaiting(值为 4,协程处于挂起态,需要等待被唤醒. gc、channel 通信或者锁操作时经常会进入这种状态);_Gdead(值为 6,协程刚初始化完成或者已经被销毁,会处于此状态);_Gcopystack(值为 8,协程正在栈扩容流程中);_Greempted(值为 9,协程被抢占后的状态)。

       文字性总结难免有些过于含糊和空洞,对一些细节的描述总是不够精确的。下面照旧开启源码走读流程,从代码中寻求理论证明和细节补充。

       gmp 数据结构定义为 runtime/runtime2.go 文件中,由于各个类的成员属性较多,那么只摘取核心字段进行介绍:(1)m:在 p 的代理,负责执行当前 g 的 m;(2)sched.sp:保存 CPU 的 rsp 寄存器的值,指向函数调用栈栈顶;(3)sched.pc:保存 CPU 的 rip 寄存器的值,指向程序下一条执行指令的地址;(4)sched.ret:保存系统调用的返回值;(5)sched.bp:保存 CPU 的 rbp 寄存器的值,存储函数栈帧的起始位置。其中 g 的生命周期由以下几种状态组成:(1)_Gidle(值为 0,为协程开始创建时的状态,此时尚未初始化完成);(2)_Grunnable(值为 1,协程在待执行队列中,等待被执行);(3)_Grunning(值为 2,协程正在执行,同一时刻一个 p 中只有一个 g 处于此状态);(4)_Gsyscall(值为 3,协程正在执行系统调用);(5)_Gwaiting(值为 4,协程处于挂起态,需要等待被唤醒. gc、channel 通信或者锁操作时经常会进入这种状态);(6)_Gdead(值为 6,协程刚初始化完成或者已经被销毁,会处于此状态);(7)_Gcopystack(值为 8,协程正在栈扩容流程中);(8)_Greempted(值为 9,协程被抢占后的状态)。

       其中,goroutine 的类型可分为两类:(1)I 负责调度普通 g 的 g0,执行固定的调度流程,与 m 的关系为一对一;(2)II 负责执行用户函数的普通 g。m 通过 p 调度执行的 goroutine 永远在普通 g 和 g0 之间进行切换。

       主动调度是用户主动执行让渡的方式,主要方式是,用户在执行代码中调用了 runtime.Gosched 方法,此时当前 g 会当让出执行权,主动进行队列等待下次被调度执行。被动调度因当前不满足某种执行条件,g 可能会陷入阻塞态无法被调度,直到关注的条件达成后,g 才从阻塞中被唤醒,重新进入可执行队列等待被调度。

       正常调度指的是 g 中的执行任务已完成,g0 会将当前 g 置为死亡状态,发起新一轮调度。抢占调度指的是 g 执行系统调用超过指定的时长,且全局的 p 资源比较紧缺,此时将 p 和 g 解绑,抢占出来用于其他 g 的调度。

       调度流程的主干方法是位于 runtime/proc.go 中的 schedule 函数。在宏观调度流程中,我们可以尝试对 gmp 的宏观调度流程进行整体串联,包括:(1)以 g0 -> g -> g0 的一轮循环为例进行串联;(2)g0 执行 schedule() 函数,寻找到用于执行的 g;(3)g0 执行 execute() 方法,更新当前 g、p 的状态信息,并调用 gogo() 方法,将执行权交给 g;(4)g 因主动让渡(gosche_m())、被动调度(park_m())、正常结束(goexit0())等原因,调用 m_call 函数,执行权重新回到 g0 手中;(5)g0 执行 schedule() 函数,开启新一轮循环。

       在 Golang 中,调度流程的主干方法是位于 runtime/proc.go 中的 schedule 函数,此时的执行权位于 g0 手中。在 findRunnable 方法中,调度流程中,一个非常核心的步骤就是为 m 寻找到下一个执行的 g。在 execute 方法中,当 g0 为 m 寻找到可执行的 g 之后,接下来就开始执行 g。

       在 g 执行主动让渡时,会调用 mcall 方法将执行权归还给 g0,并由 g0 调用 gosched_m 方法。在 g 需要被动调度时,会调用 mcall 方法切换至 g0,并调用 park_m 方法将 g 置为阻塞态。当 g 执行完成时,会先执行 mcall 方法切换至 g0,然后调用 goexit0 方法。与 g 的系统调用有关的,视角切换回发生系统调用前,与 g 绑定的原 m 当中,此时执行权同样位于 m 的 g0 手中。在 m 需要执行系统调用前,会先执行位于 runtime/proc.go 的 reentersyscall 的方法。当 m 完成了内核态的系统调用之后,此时会步入位于 runtime/proc.go 的 exitsyscall 函数中。

       与 g 的系统调用有关的,视角切换回发生系统调用前,与 g 绑定的原 m 当中,在 m 需要执行系统调用前,会先执行位于 runtime/proc.go 的 reentersyscall 的方法。当 m 完成了内核态的系统调用之后,此时会步入位于 runtime/proc.go 的 exitsyscall 函数中。

       当 g 执行完成时,会先执行 mcall 方法切换至 g0,然后调用 goexit0 方法。当 m 完成了内核态的系统调用之后,此时会步入位于 runtime/proc.go 的 exitsyscall 函数中。

       对于抢占调度的执行者,不是 g0,而是一个全局的 monitor g,代码位于 runtime/proc.go 的 retake 方法中。与 g 的系统调用有关的,视角切换回发生系统调用前,与 g 绑定的原 m 当中,在 m 需要执行系统调用前,会先执行位于 runtime/proc.go 的 reentersyscall 的方法。当 m 完成了内核态的系统调用之后,此时会步入位于 runtime/proc.go 的 exitsyscall 函数中。

       在 Golang 中,调度流程的主干方法是位于 runtime/proc.go 中的 schedule 函数,此时的执行权位于 g0 手中。在 findRunnable 方法中,调度流程中,一个非常核心的步骤就是为 m 寻找到下一个执行的 g。在 execute 方法中,当 g0 为 m 寻找到可执行的 g 之后,接下来就开始执行 g。

       在 g 执行主动让渡时,会调用 mcall 方法将执行权归还给 g0,并由 g0 调用 gosched_m 方法。在 g 需要被动调度时,会调用 mcall 方法切换至 g0,并调用 park_m 方法将 g 置为阻塞态。当 g 执行完成时,会先执行 mcall 方法切换至 g0,然后调用 goexit0 方法。当 m 完成了内核态的系统调用之后,此时会步入位于