展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > Go协程的底层原理及分析

Go协程的底层原理及分析

Go协程底层由g结构体表示,包含栈和程序计数器。线程通过m结构体执行调度循环,依次运行协程。多线程引入p结构体,形成G-M-P模型,p维护本地队列,减少全局锁竞争,提升并发性能。

本文目录

    在并发编程的演进中,“协程”这个概念越来越被关注,尤其是Go语言中的Goroutine,已经成为很多开发者讨论高性能架构时绕不开的话题。不过,协程到底是怎么工作的?底层调度逻辑长什么样?今天这篇文章,我们就从头开始,一步步把Go协程的底层原理拆清楚。这不算一篇新手入门帖,更像一份带着源码分析路径的深度解析。

    为什么要有协程

    什么是进程

    Go协程的底层原理及分析

    • 操作系统里“程序”的最小单位
    • 进程用于占用内存空间
    • 打个比方,进程像是工厂的一个厂房,占用了工厂的空间

    什么是线程

    如果把进程比作厂房,线程就是厂房里的生产线:

    Go协程的底层原理及分析

    • 每个进程可以拥有多个线程
    • 线程使用系统分配给进程的内存,且线程之间共享内存

    CPU会在线程之间来回切换:

    Go协程的底层原理及分析

    • 线程用于占用CPU时间
    • 线程的调度依赖操作系统,开销比较大
    • 线程像厂房里的生产线,占用的是工人(CPU)工时
    • 线程里跑的代码就是生产流程

    线程的问题

    • 线程本身占用的资源不小
    • 线程的创建和销毁开销大
    • 线程切换需要陷入内核,开销也很大

    什么是协程

    协程的思路很有意思:把一段程序的运行状态打包,让它可以在线程间灵活调度:

    Go协程的底层原理及分析

    • 协程本质上是将一段程序的运行状态打包,可以在线程之间调度
    • 就好比把生产流程打包,让流程不固定在某一条生产线上
    • 注意,协程并不是要取代线程,它仍然需要在线程上运行
    • 线程是协程的资源,协程利用线程来执行

    协程的优势

    • 资源利用率更高
    • 调度速度更快
    • 能够支撑超高并发

    总结

    • 进程负责分配内存空间
    • 线程负责分配CPU时间
    • 协程用于更精细地利用线程
    • 协程的本质是一段包含了运行状态的程序

    协程的本质

    func do1() {  
       func2()  
    }  
      
    func do2() {  
       func3()  // 在这里打个断点
    }  
      
    func do3() {  
       fmt.Print("dododo")  
    }  
      
    func main() {  
       go func1()  
       time.Sleep(time.Minute)  
    }
    

    运行这段代码并调试,打开debugger观察栈信息。协程从do1调用到do2,下一步会调用do3,不过因为打了断点,do3还没有被调用进来:

    Go协程的底层原理及分析

    在Go语言内部,协程是用g结构体表示的。路径是runtime/runtime2.go:

    type g struct {  
       stack       stack   // offset known to runtime/cgo
    }
    

    这里面的stack就是协程栈,也就是上面调试时看到的栈信息。我们再看看stack结构体内部的细节:

    type stack struct {  
       lo uintptr  // 栈的上限
       hi uintptr  // 栈的下限
    }
    

    可以简单理解,这个栈有两个指针——高指针和低指针,指向栈在内存中的占用区域,用来存储栈帧信息。接着看g结构体里的另一个关键参数:

    type g struct {  
       stack     stack   // offset known to runtime/cgo
       sched     gobuf
    }
    

    sched的类型是gobuf,它里面有两个重要的成员:sp(栈指针)指向当前运行到的方法栈帧,pc(程序计数器)记录当前运行到了哪行代码:

    type gobuf struct {  
       sp   uintptr
       pc   uintptr
       g    guintptr  
       ctxt unsafe.Pointer  
       ret  uintptr  
       lr   uintptr  
       bp   uintptr // for framepointer-enabled architectures  
    }
    

    继续看g结构体,还有其他重要的参数:atomicstatus表示协程状态,goid是协程ID:

    type g struct {  
       stack     stack   // offset known to runtime/cgo
       sched     gobuf
       atomicstatus uint32 
       goid         int64
    }
    

    综合来看,协程的底层结构可以这样总结:一个g结构体包含多个字段,其中stack表示协程栈,指向stack结构体,内部有lo和hi两个指针分别指向栈的低地址和高地址,用来记录协程的执行位置;shed是gobuf结构体,包含sp(栈指针,指向当前运行到的栈帧)和pc(程序计数器,记录当前执行到哪一行)。

    Go协程的底层原理及分析

    • runtime中,协程的本质是一个g结构体
    • stack:记录堆栈地址
    • gobuf:记录当前程序运行现场
    • atomicstatus:记录协程状态

    协程的结构清楚了,那它在底层是如何与线程关联的?在runtime/runtime2.go中,还有一个m结构体用来表示操作系统线程:

    type m struct {  
       g0      *g     // 调度协程而产生的协程
       curg    *g     // 当前运行的协程
       id      int64  // 线程的id
       mOS            // 记录不同操作系统底层对线程的额外描述信息
    }
    
    • runtime中将操作系统线程抽象为m结构体
    • g0:用于调度的特殊协程
    • curg:当前线程正在运行的协程
    • mOS:记录操作系统线程的信息

    协程是如何执行的

    单线程循环(Go 0.x)

    Go里的每个线程会从schedule()方法开始运行。这个方法是在g0栈上执行的。g0是专门为调度而生的协程,它有自己的栈空间,用来记录方法调用跳转信息。为什么不直接用普通协程的栈呢?原因有两个:第一,普通协程的栈只能记录业务方法的调用关系;第二,在线程还没拿到协程去运行时,根本没有普通协程栈。所以线程一开始用的就是g0栈——可以理解为在内存的栈区里单独留了一块区域,专门记录线程执行的方法。

    Go协程的底层原理及分析

    schedule()方法位于runtime/proc.go,我们来简单拆一下:

    func schedule() {
        var gp *g  // 新建了一个变量叫gp,就是即将要运行的协程
        ...        // 在全局的各种队列或本地的各种队列尝试去拿一个可以运行的协程
        execute(gp, inheritTime)  // 调用了execute方法
    }
    

    execute方法的逻辑如下:

    func execute(gp *g, inheritTime bool) {
        ... // 给即将要执行的gp协程里面的一些字段赋了值
        gogo(&gp.sched)  // 调用了gogo方法
    }
    

    gogo方法只有函数声明:

    func gogo(buf *gobuf)
    

    它是汇编实现的。每个平台都有对应的gogo实现,说明这个方法的逻辑是平台相关的。我们看看runtime/asm_amd64.s中gogo的实现:

    // func gogo(buf *gobuf) 
    // restore state from Gobuf; longjmp  
    TEXT runtime·gogo(SB), NOSPLIT, $0-8  
       MOVQ   buf+0(FP), BX     // gobuf  
       MOVQ   gobuf_g(BX), DX  
       MOVQ   0(DX), CX     // make sure g != nil  
       JMP    gogo<>(SB)
    

    传进来的是一个gobuf指针。它最后又跳转到另一个gogo:

    TEXT gogo<>(SB), NOSPLIT, $0  
       get_tls(CX)  
       MOVQ   DX, g(CX)  
       MOVQ   DX, R14       // set the g register  
       MOVQ   gobuf_sp(BX), SP   // 人为插入了一个goexit方法的栈帧
       MOVQ   gobuf_ret(BX), AX  
       MOVQ   gobuf_ctxt(BX), DX  
       MOVQ   gobuf_bp(BX), BP  
       MOVQ   $0, gobuf_sp(BX)   // clear to help garbage collector  
       MOVQ   $0, gobuf_ret(BX)  
       MOVQ   $0, gobuf_ctxt(BX)  
       MOVQ   $0, gobuf_bp(BX)  
       MOVQ   gobuf_pc(BX), BX  // 将我们协程运行到了哪行代码的位置取出来 
       JMP    BX // 跳转到那个位置进行执行
    

    这里的两个关键操作:MOVQ gobuf_sp(BX), SP是在协程栈中插入了一个goexit方法的栈帧;MOVQ gobuf_pc(BX), BX和JMP BX则是跳转到程序计数器指示的位置继续执行。

    换句话说,流程是这样的:首先在协程栈里插入goexit的栈帧,然后跳转到g结构体里gobuf指示的程序计数器位置执行。业务方法从do1开始执行,do1调用do2,do2调用do3,依次执行。这个过程中使用的是协程自己的栈(g stack),目的是让每个g结构体都能记录自己的执行现场。业务代码执行到do3后打印文本,然后逐步返回,最终退到goexit方法。

    goexit也只有声明:

    func goexit(neverCallThisFunction)
    

    看看runtime/asm_amd64.s中的实现:

    // The top-most function running on a goroutine  
    // returns to goexit+PCQuantum.  
    TEXT runtime·goexit(SB),NOSPLIT|TOPFRAME,$0-0  
       BYTE   $0x90  // NOP  
       CALL   runtime·goexit1(SB)    // does not return  
       // traceback from goexit1 must hit code range of goexit   BYTE   $0x90  // NOP
    

    核心是CALL runtime·goexit1(SB)。这个goexit1在runtime/proc.go中:

    // Finishes execution of the current goroutine.  
    func goexit1() {  
       mcall(goexit0) 
    }
    

    mcall会切换到g0栈执行函数。后面的调用关系由g0栈记录:

    // mcall switches from the g to the g0 stack and invokes fn(g)
    func mcall(fn func(*g))
    

    goexit0的逻辑会重置协程的各种参数,最终重新调用schedule():

    // goexit continuation on g0.  
    func goexit0(gp *g) {
        ... // 设置协程中的各项参数
        schedule() 
    }
    

    这就形成了一个循环:Go的业务逻辑在线程上执行,线程内部有一个循环。在业务方法之外(协程逻辑之外),用g0栈记录函数调用和跳转关系;进入业务方法后,使用协程自己的栈记录调用和跳转信息,同时还记录本地变量。业务方法执行完成后,退回到强行插入的goexit栈帧,进入goexit0切换到schedule,然后继续循环。

    Go协程的底层原理及分析

    用一个更抽象的图来表示:M表示系统线程,G表示协程,有一个协程队列或池子。线程会一个个将协程取出来执行,执行完后寻找下一个,如此循环。这个单线程循环的逻辑在Go 0.x版本中就已经实现了,那时候Go甚至还没有正式对外发布。

    多线程循环(Go 1.0)

    现在的CPU是多核多线程的,如果还用单线程版本就太浪费资源了。所以从Go 1.0开始引入多线程循环。以下面两个线程为例,它们执行的逻辑完全一样:从协程队列里获取可执行的协程,送入gogo执行其业务方法,然后退出,再取下一个,循环往复。如果有8个线程,那就跑8个这样的循环。

    Go协程的底层原理及分析

    但问题也来了:所有线程都从同一个全局协程队列获取,会引发并发冲突。需要保证一个协程不会同时被多个线程取走,所以全局队列必须加锁:

    Go协程的底层原理及分析

    用抽象图来表示:M是系统线程,G是协程,下方是一个全局队列。每个线程都要从这个全局队列里抓取协程执行,执行完后丢弃,再抓取新的。抢锁成为性能瓶颈:

    Go协程的底层原理及分析

    总结

    线程循环:

    • 操作系统并不知道Goroutine的存在
    • 操作系统线程执行一个调度循环,顺序执行Goroutine
    • 调度循环非常像线程池

    问题:

    • 协程顺序执行,无法并发
    • 多线程并发时,会抢夺协程队列的全局锁

    关键结论:

    • 协程的本质是一个g结构体
    • g结构体记录了协程栈和PC信息
    • 最简场景下,线程执行标准调度循环,依次执行协程

    G-M-P调度模型

    本地队列

    Go协程的底层原理及分析

    为了解决全局锁的竞争问题,Go引入了本地队列:线程不再一次只拿一个协程,而是先拿一批放到本地队列里执行,执行完后再拿下一批。这样能大大降低全局锁的冲突频率。

    这个本地队列在Go底层由p结构体表示,路径同样是runtime/runtime2.go:

    type p struct {
        m        muintptr   // 指向它服务的那个线程
        // 它是可执行的协程的队列,可以无锁进行访问
        runqhead uint32  // 队列的头
        runqtail uint32  // 队列的尾
        runq     [256]guintptr  // 256长度的指针,每个指针会指向一个g结构体
        runnext guintptr  // 指向下一个可用的协程的指针
    }
    

    用图来表示p结构体:m指向它服务的线程,runq是一个可容纳256个可执行协程的队列,runqhead和runqtail分别指向队列的头和尾,runnext指向下一个要执行的协程:

    Go协程的底层原理及分析

    G-M-P模型

    有了G、M和P,就构成了经典的G-M-P调度模型。每个P服务于一个M,职责是维护一个本地协程队列。M每次获取协程时,优先从本地队列取。只有当本地队列都执行完了,M才会尝试从全局队列拿一批放到本地,从而再次获得无锁执行的能力。这就是最朴素的G-M-P模型。

    Go协程的底层原理及分析

    进一步分析P的作用及底层逻辑

    窃取式工作分配机制

    • P是M和G之间的中间层(类似送料器)
    • P持有G的队列,M每次获取G时不用每次都去全局找
    • 大大减少了并发冲突

    这部分逻辑在schedule方法中体现,我们之前省略了获取协程的具体过程:

    func schedule() {
        var gp *g  // 新建了一个变量叫gp,就是即将要运行的协程
          
        if gp == nil {  
            gp, inheritTime = runqget(_g_.m.p.ptr())  
       }
    }
    

    如果本地拿不到协程,就调用runqget方法,从可执行队列获取一个协程。入参_g_是当前正在运行的协程,.m到当前线程结构体,.p到本地队列指针。runqget的逻辑很简单:先看runnext,如果有就返回:

    func runqget(_p_ *p) (gp *g, inheritTime bool) {
        next := _p_.runnext
        if next != 0 && _p_.runnext.cas(next, 0) {  
            return next.ptr(), true  
        }
    }
    

    如果本地队列也没有了,就尝试从全局队列获取。对应schedule里的findrunnable:

    func schedule() {
        var gp *g  // 新建了一个变量叫gp,就是即将要运行的协程
          
        if gp == nil {  
            gp, inheritTime = runqget(_g_.m.p.ptr())  
        }
        if gp == nil {  
            gp, inheritTime = findrunnable()
        }
    }
    

    findrunnable尝试从全局队列获取:

    func findrunnable() (gp *g, inheritTime bool) {
        if sched.runqsize != 0 {  
            lock(&sched.lock)  
            gp := globrunqget(_p_, 0)  
            unlock(&sched.lock)  
            if gp != nil {  
                return gp, false  
            }  
        }
    }
    

    全局队列里也找不到呢?这时候线程不会闲着——它还能去“偷”。findrunnable内部调用了stealWork:

    gp, inheritTime, tnow, w, newWork := stealWork(now)  
    

    stealWork的作用是从其他P的队列里偷一些协程过来:

    // stealWork attempts to steal a runnable goroutine or timer from any P
    func stealWork(now int64) (gp *g, inheritTime bool, rnow, pollUntil int64, newWork bool) {
        
    }
    

    本地没有,全局也没有,就从别的P上偷。这就是窃取式工作分配:

    Go协程的底层原理及分析

    左侧线程的本地和全局队列都空了,但右侧线程对应的P队列上还有任务,左侧线程就会偷一些过来执行:

    Go协程的底层原理及分析

    窃取式工作分配:

    • 本地和全局队列都找不到G时
    • 去其他P里“偷”
    • 有效提升线程利用率

    新建协程的放置

    • 随机寻找一个P
    • 将新协程放入P的runnext(插队)
    • 若P本地队列满了,再放入全局队列

    Go会认为新创建的协程优先级最高,尽量往前排,优先执行。对应的代码在runtime/proc.go的newproc方法中:

    func newproc(fn *funcval) {
        newg := newproc1(fn, gp, pc)  // 新建一个协程
        _p_ := getg().m.p.ptr()  
        runqput(_p_, newg, true) // 寻找p,放到本地队列或全局队列
    }
    

    如何实现协程并发

    G-M-P模型解决了全局锁竞争问题,但还有一个问题没解决:协程顺序执行,无法并发怎么办?

    协程饥饿问题

    如果有两个线程,各自运行的协程执行时间特别长,就会导致后面时间敏感的协程迟迟无法得到调度:

    Go协程的底层原理及分析

    解决办法是什么?回到单线程循环模型,如果遇到超长协程,可以引入某种轮换机制:执行一段时间后将这个协程暂停,切换执行后面的协程,防止时间敏感的协程饿死。

    Go协程的底层原理及分析

    所以,当协程执行到业务轮换点时,保存现场(业务数据和代码执行行数),放到自己的协程结构体中,然后放回队列或进入休眠,让出线程去处理优先级更高的协程:

    Go协程的底层原理及分析

    用抽象图表示,这就是通过本地队列的小循环来解决本地队列中协程的饥饿问题:

    Go协程的底层原理及分析

    但问题还没完:如果本地队列的协程循环多次还没执行完,全局队列的协程也会被饿死。所以本地小循环在适当的时候,会从全局队列中拿一些协程进来一起执行:

    Go协程的底层原理及分析

    回到schedule方法,可以看到这个逻辑的具体实现——每执行61次线程循环,就从全局队列中拿一个协程到本地队列:

    func schedule() {
        if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {  
            lock(&sched.lock)  
            gp = globrunqget(_g_.m.p.ptr(), 1)  
            unlock(&sched.lock)  
        }
    }
    

    切换的时机

    那超大协程的切换点到底是怎么判断的呢?主要有两种情况:

    • 主动挂起(runtime.gopark)
    • 系统调用完成时

    主动挂起(runtime.gopark)

    Go协程的底层原理及分析

    业务方法中调用gopark(),会直接从线程循环跳到schedule(),重新从队列取协程执行:

    // 让我们现在正在运行的这个协程进入等待状态
    func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {  
       ......
       mcall(park_m)  
    }
    

    park_m中进行状态维护后调用schedule():

    // park continuation on g0.  
    func park_m(gp *g) {  
       ......
       schedule()  
    }
    

    这里有两个点值得留意:第一,gopark是小写开头的,我们自己不能直接调用,但业务中使用的逻辑如锁、channel、sleep等会主动调用它。第二,调用gopark后协程会进入waiting状态,不能立即被再次调度,比如sleep结束后系统才会把状态改为runnable,调度器才会再次调度它。

    系统调用完成时

    Go协程的底层原理及分析

    业务程序难免会做系统调用,比如检查网络状态、硬件状态。系统调用完成后,协程会在exitsyscall()方法中进行切换:

    //go:nosplit  
    //go:nowritebarrierrec  
    //go:linkname exitsyscall  
    func exitsyscall() {
        ......
        Gosched()
        ......
    }
    

    Gosched用mcall调用gosched_m:

    func Gosched() {  
       checkTimeouts()  
       mcall(gosched_m)  
    }
    

    gosched_m调用goschedImpl,最终再次调用schedule():

    func goschedImpl(gp *g) {  
       ......
       schedule()  
    }
    

    系统调用的细节不用太在意,只需要知道:一旦涉及系统调用,会进入exitsyscall,线程会停止当前协程,返回线程循环的起始位置执行其他协程。这样做的主要目的就是让协程在执行完系统调用后找一个时机切换,防止它一直霸占线程。

    总结

    • 协程顺序执行会导致饥饿问题
    • 在协程执行中间将其挂起,切换执行其他协程
    • 可以在完成系统调用时挂起,也可以主动挂起
    • 为防止全局队列饥饿,本地队列会随机从全局队列中抽取协程

    抢占式调度

    如果有一个超大业务的协程,既不主动挂起也不做系统调用,那该怎么办?

    基于协作的抢占式调度

    思路

    有没有一个方法经常会被调用?可以在那里做点文章?答案是runtime.morestack_noctxt。

    回到之前的代码例子,用汇编看看:

    func do1() {  
       func2()  
    }  
      
    func do2() {  
       func3()  // 在这里打个断点
    }  
      
    func do3() {  
       fmt.Print("dododo")  
    }  
      
    func main() {  
       go func1()  
       time.Sleep(time.Minute)  
    }
    

    执行go build -gcflags -S main.go,会发现do1在调用do2前,do2在调用do3前,都调用了runtime.morestack_noctxt。只要方法中调用了其他方法,编译器都会插入这个调用:

    Go协程的底层原理及分析

    runtime.morestack_noctxt

    runtime.morestack_noctxt的本意是检查协程栈是否有足够空间,不够就扩容,本身和协程调度没有直接关系。但因为每次函数调用时都会被编译器插入,我们可以在这个方法里下个钩子。

    标记抢占

    怎么钩?系统监控到某个Goroutine运行超过10ms时,认为它是一个大协程,会引发其他协程饥饿。于是系统会把g结构体里的stackguard0字段值设为0xfffffade——这个值就是抢占标志。

    抢占

    当runtime.morestack_noctxt被调用时,会检查是否被抢占。如果是,就直接回到schedule方法。在runtime/stubs.go中只有声明,实现是汇编的:

    func morestack_noctxt()
    

    查看runtime/asm_amd64.s中的实现:

    // morestack but not preserving ctxt.  
    TEXT runtime·morestack_noctxt(SB),NOSPLIT,$0  
       MOVL   $0, DX  
       JMP    runtime·morestack(SB)
    

    跳转到runtime.morestack,它也由汇编实现,最终调用runtime.newstack:

    TEXT runtime·morestack(SB),NOSPLIT,$0-0  
       // Cannot grow scheduler stack (m->g0).  
       ......
       CALL   runtime·newstack(SB)  
       ......
    

    在runtime/stack.go的newstack方法中:

    //go:nowritebarrierrec  
    func newstack() {
        stackguard0 := atomic.Loaduintptr(&gp.stackguard0)
        preempt := stackguard0 == stackPreempt
        if preempt {  
            // Act like goroutine called runtime.Gosched
           gopreempt_m(gp) // never return  
        }
    }
    

    preempt就是抢占的意思。如果stackguard0被标记为0xfffffade,就会调用gopreempt_m,里面最终调用goschedImpl,再调用schedule,回到线程循环起点。

    总结一下:每次函数调用都会经过morestack,它会判断协程是否被标记了抢占。如果是,就回到schedule获取新协程执行,从而防止饥饿:

    Go协程的底层原理及分析

    但这种方法并不完美。

    基于信号的抢占式调度

    基于协作抢占的缺陷

    看下面这个例子:

    func do1() {  
       i := 0  
       for true {  
          i++  
       }  
    }  
      
    func main() {  
       go do1()  
    }
    

    这段代码既没有系统调用,也不会主动挂起,甚至不会被标记抢占——因为它没有任何函数调用,编译器不会插入morestack_noctxt。用汇编一看,确实没有。这个协程会永远占据这个线程。如果多开几个这种协程,其他协程全都要饿死。

    为了解决这个问题,大神们想出了基于信号的抢占式调度。这里的信号,指的是线程信号。

    线程信号

    • 操作系统中,有很多基于信号的底层通信方式
    • 比如Linux系统的SIGPIPE、SIGURG、SIGHUP等
    • 线程可以注册对应信号的处理函数

    实现

    • 注册不常用的SIGURG信号的处理函数
    • GC工作时,向目标线程发送信号(GC期间很多线程业务都停了,适合做抢占)
    • 线程收到信号,触发重新调度

    这个信号处理函数叫doSigPreempt。当GC发送SIGURG抢占信号后,陷入业务方法执行的线程会立即跳转到doSigPreempt,执行重新调度的循环:

    Go协程的底层原理及分析

    doSigPreempt在runtime/signal_unix.go中:

    // doSigPreempt handles a preemption signal on gp.  
    func doSigPreempt(gp *g, ctxt *sigctxt) {
        ctxt.pushCall(abi.FuncPCABI0(asyncPreempt), newpc)
    }
    

    核心是调用asyncPreempt,由汇编实现:

    // asyncPreempt is implemented in assembly.
    func asyncPreempt()
    

    asyncPreempt再调到asyncPreempt2,里面通过mcall调用preemptPark:

    func asyncPreempt2() {   
        ......
        mcall(preemptPark) 
        ...... 
    }
    

    preemptPark最终调用schedule(),回到线程循环的起点:

    func preemptPark(gp *g) {
        schedule()
    }
    

    总结

    • 基于系统调用和主动挂起,协程可能无法正常调度
    • 基于协作的抢占式调度:业务主动调用morestack()
    • 基于信号的抢占式调度:强制线程调用doSigPreempt()
    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发
    相关文章 更多
    精品专题 更多
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    IOS软件

    正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

    AI

    正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

    Mac软件 更多
    Postman macOS版
    Postman macOS版

    Postman是一套面向API全生命周期的开发与测试平台,可在Mac上发送和调试多种协议请求,管理集合、环境及认证信息,并完成脚本测试、模拟服务、监控、文档发布和团队协作。它适合开发、测试及运维人员使用,提供个人免费方案。

    Docker Desktop macOS版
    Docker Desktop macOS版

    Docker Desktop 是 Docker 官方为 Mac 提供的容器开发环境,集成 Docker Engine、命令行工具、Compose、Build、Kubernetes 与图形化管理界面。

    Android Studio macOS版
    Android Studio macOS版

    Android Studio 是 Google 面向安卓应用开发提供的官方集成开发环境,基于 IntelliJ IDEA 构建。它整合智能代码编辑、Gradle 构建、界面预览、设备模拟、性能分析、调试测试及 APK 分析等工具。

    Xcode macOS版
    Xcode macOS版

    Xcode 是苹果面向 Mac 提供的官方集成开发环境,将代码编辑、项目构建、SwiftUI 实时预览、设备模拟、自动化测试、断点调试、性能分析和应用分发整合在同一套工作流中,适合开发与维护 iPhone、iPad、Mac、Apple Wa

    JetBrains WebStorm macOS版
    JetBrains WebStorm macOS版

    JetBrains WebStorm 是面向 JavaScript、TypeScript 和现代 Web 技术的专业集成开发环境,可智能理解项目结构,提供代码补全、重构、错误检查、运行调试、测试及 Git 工具。

    PhpStorm macOS版
    PhpStorm macOS版

    PhpStorm 是 JetBrains 推出的专业 PHP 集成开发环境,可在 Mac 上完成代码编写、智能检查、重构、调试、测试和数据库管理。它支持主流 PHP 框架、Composer、Git、Docker 与远程解释器。

    JetBrains PyCharm macOS版
    JetBrains PyCharm macOS版

    JetBrains PyCharm 是面向 Python 开发者的集成开发环境,提供智能补全、代码检查、重构、调试、测试、版本控制及虚拟环境管理功能。Mac 版兼容 Intel 与 Apple 芯片,核心功能可免费使用。

    JetBrains IntelliJ IDEA macOS版
    JetBrains IntelliJ IDEA macOS版

    IntelliJ IDEA 是 JetBrains 推出的专业集成开发环境,重点服务于 Java、Kotlin 与 JVM 项目开发。软件集成代码补全、静态检查、重构、调试、测试、版本控制及构建工具,并可借助插件扩展语言和框架支持。

    Visual Studio Code macOS版
    Visual Studio Code macOS版

    Visual Studio Code 是微软提供的免费代码编辑器,支持 macOS,并为 Intel 与 Apple 芯片 Mac 提供适配版本。软件集代码编辑、调试、Git 源代码管理、集成终端和扩展市场于一体。

    REAPER macOS版
    REAPER macOS版

    REAPER是Cockos开发的数字音频工作站,提供多轨音频与MIDI录制、剪辑、处理、混音和母带制作工具。Mac版兼容Intel与Apple芯片,支持AU、VST、VST3、CLAP等插件格式,并提供高度可定制的工作流程。

    Adobe Audition macOS版
    Adobe Audition macOS版

    Adobe Audition 是面向播客、影视后期、音乐制作及内容创作者的专业音频工作站,提供波形精修、多轨录制与混音、频谱编辑、降噪修复、响度匹配和效果处理工具,并可与 Adobe Premiere 协同完成视频声音制作。

    Pro Tools macOS版
    Pro Tools macOS版

    Pro Tools 是 Avid 推出的专业数字音频工作站,覆盖多轨录音、无损剪辑、MIDI 编曲、插件处理、自动化混音及声音与画面同步等工作。它拥有 Intro、Artist、Studio 和 Ultimate 等方案。

    WINDOWS 更多
    PixPix 网页版
    PixPix 网页版

    PixPix 是面向品牌、卖家和内容团队的在线 AI 图片视频创作平台,提供图视频 Agent、无限画布、图片生成、视频生成、自然语言编辑及商品套图、产品精修和带货视频等电商工具。

    Postman Windows版
    Postman Windows版

    Postman 是面向 API 全生命周期的开发与协作平台。Windows 桌面版可用于构建和发送 HTTP、GraphQL、gRPC、WebSocket、MQTT 等请求,并提供环境变量、集合、脚本测试、模拟服务、监控、文档及团队工作区等

    Docker Desktop Windows版
    Docker Desktop Windows版

    Docker Desktop 为 Windows 开发者提供集成式容器环境,内置 Docker Engine、命令行工具、Compose、Build 与 Kubernetes 等组件,可在本机完成镜像构建、容器运行、日志查看和资源管理。

    Android Studio Windows版
    Android Studio Windows版

    Android Studio 是 Google 面向 Android 应用开发提供的官方集成开发环境,以 IntelliJ IDEA 为基础,集成智能代码编辑、Gradle 构建、设备模拟、界面预览、调试测试及性能分析工具,

    JetBrains WebStorm Windows版
    JetBrains WebStorm Windows版

    WebStorm 是 JetBrains 面向 JavaScript、TypeScript 和现代 Web 技术打造的集成开发环境。它提供项目级代码分析、智能补全、安全重构、导航搜索,并集成调试、测试、终端、Git 与常用前端工具,

    JetBrains PhpStorm Windows版
    JetBrains PhpStorm Windows版

    PhpStorm 是 JetBrains 面向 PHP 与现代 Web 开发打造的专业集成开发环境,提供代码补全、静态检查、重构、调试、测试、数据库、版本控制及前端语言支持。Windows 用户可直接下载安装程序,

    JetBrains PyCharm Windows版
    JetBrains PyCharm Windows版

    PyCharm 是 JetBrains 推出的 Python 集成开发环境,提供智能代码补全、错误检查、重构、调试、测试、Git、终端及 Jupyter Notebook 支持。统一版安装后可体验 30 天 Pro 功能,

    JetBrains IntelliJ IDEA Windows版
    JetBrains IntelliJ IDEA Windows版

    JetBrains IntelliJ IDEA 是以 Java、Kotlin 和 JVM 项目为核心的专业集成开发环境,提供智能补全、代码检查、安全重构、调试、测试、版本控制及构建工具集成。统一发行版的核心功能可免费用于商业和非商业项目,

    Visual Studio Code Windows版
    Visual Studio Code Windows版

    Visual Studio Code 是微软推出的免费代码编辑器,Windows 版集成智能补全、代码导航、调试、终端和版本控制,并可通过扩展添加语言、格式化及远程开发能力。它启动较快、配置灵活,

    REAPER Windows版
    REAPER Windows版

    REAPER 是 Cockos 推出的数字音频工作站,适用于录音、MIDI 编曲、音频编辑、混音、母带制作及声音设计。软件安装包小巧,支持多轨录制、灵活路由、自动化、第三方插件和脚本扩展,并提供完整功能的 60 天免费评估期。

    Adobe Audition Windows版
    Adobe Audition Windows版

    Adobe Audition 是面向播客、影视后期、广播与音乐制作的专业音频工作站,提供波形编辑、多轨混音、频谱分析、录音、降噪修复和效果处理等功能。Windows 版通过 Adobe Creative Cloud 安装和管理,

    Pro Tools Windows版
    Pro Tools Windows版

    Pro Tools 是 Avid 推出的专业数字音频工作站,提供多轨录音、音频剪辑、MIDI编曲、插件处理、自动化混音及影视声音制作能力。它适合音乐人、录音师、制作人和音频后期团队使用,并提供免费入门版、订阅方案及永久授权选择。