Golang的Goroutine(协程)与runtime

目录

Runtime 包概述

Runtime 包常用函数

1. GOMAXPROCS

2. Caller 和 Callers

3. BlockProfile 和 Stack

理解Golang的Goroutine

Goroutine的基本概念

特点:

Goroutine的创建与启动

示例代码

解释

Goroutine的调度

Gosched的作用

示例代码

输出

解释

Goroutine的性能优化

示例代码

解释

实际应用中的使用场景

总结

Runtime与Routine的关系

补充(代码)


Runtime 包概述

runtime 包提供了与 Go 运行时环境交互的功能,包括 goroutine 调度、内存管理、堆栈操作等。通过 runtime 包,我们可以更好地控制程序的运行行为。

Runtime 包常用函数

1. GOMAXPROCS

设置可以并行执行的最大 CPU 数,并返回之前的设置值。

fmt.Println(runtime.GOMAXPROCS(2))

2. Caller 和 Callers

runtime.Caller 和 runtime.Callers 用于获取调用栈信息。

// 返回调用栈的信息(文件名、行号等)。
// 参数:skip表示从调用Caller开始往回数多少层的调用栈信息。
// skip = 0: 返回调用runtime.Caller的函数(即直接包含runtime.Caller(0)调用的那行代码)的文件名、行号等信息。
// skip = 1: 返回调用runtime.Caller所在函数的直接调用者的文件名和行号。换句话说,就是调用了包含runtime.Caller(1)的函数的地方的调用栈信息。
// skip = 2: 返回上一步调用者的调用者信息,依此类推。每增加1,就向上追溯一个调用栈帧。
// 以此类推,随着skip值的增加,可以逐级向上追溯到更早的调用栈信息。如果指定的层级超出了实际存在的调用栈层数,则ok将为false,其他返回值(pc, file, line)将没有意义或为零值。
	
pc, file, line, ok := runtime.Caller(1)  
if !ok {  
    fmt.Println("runtime.Caller() failed")  
    return  
}  
fmt.Printf("PC: %v, File: %s, Line: %d\n", pc, file, line)  

3. BlockProfile 和 Stack

runtime.BlockProfile() 用于获取阻塞概述,而 runtime.Stack() 则可以打印当前 goroutine 或所有 goroutine 的堆栈跟踪信息。


理解Golang的Goroutine

Goroutine是Golang语言中的轻量级线程,能够在单一操作系统线程上运行多个Goroutine,从而提高并发编程的效率。本文将通过实际的代码示例,详细讲解Goroutine的创建、调度、性能优化以及在实际应用中的使用场景。


    Goroutine的基本概念

    Goroutine 是Golang语言中用于并发编程的基本单位。与传统的线程(Thread)相比,Goroutine的调度和切换成本更低,因为它们是基于Golang的协作式调度模型设计的。每个Goroutine的栈大小默认为2KB,但随着其生命周期的变化,栈大小会自动扩大以适应需求。

    特点:

    • 轻量级:创建和销毁Goroutine的开销非常低。
    • 高并发:Golang可以轻松支持数万甚至更多的Goroutine同时运行。
    • ** Channels**:Goroutine之间通过Channel进行通信,避免了共享内存带来的竞态条件问题。

    Goroutine的创建与启动

    Goroutine的创建非常简单,只需要在函数调用前加上go关键字即可启动一个新的Goroutine。

    示例代码
    package main
    
    import (
        "fmt"
        "runtime"
    )
    
    func print() {
        fmt.Println("这是一个新的Goroutine。")
    }
    
    func main() {
        go print()  // 启动一个新的Goroutine
        fmt.Println("主Goroutine结束。")  // 主Goroutine继续执行
    }
    

    解释

    • go print():启动一个新的Goroutine执行print函数。
    • 主Goroutine继续执行fmt.Println("主Goroutine结束。")
    • 需要注意的是,如果主Goroutine执行完毕,程序可能会直接退出,可能无法看到子Goroutine的输出。为了确保子Goroutine有足够的时间完成,可以通过time.Sleep()或者其他同步机制来控制。

    Goroutine的调度

    Goroutine的调度是由Golang的运行时环境负责的。与线程不同,Goroutine采用的是非抢占式调度,即只有在当前Goroutine主动让出CPU(例如调用runtime.Gosched())时,其他Goroutine才能获取执行机会。

    Gosched的作用

    runtime.Gosched()用于让出当前Goroutine的执行权限,允许其他Goroutine运行。

    示例代码
    package main
    
    import (
        "fmt"
        "runtime"
    )
    
    func main() {
        go func() {
            for i := 1; i <= 5; i++ {
                runtime.Gosched()
                fmt.Println(i)
            }
        }()
        fmt.Println("主Goroutine结束。")
    }
    
    输出
    主Goroutine结束。
    1
    2
    3
    4
    5
    

    解释

    • 子Goroutine在循环中调用runtime.Gosched(),主动让出CPU时间片。
    • 主Goroutine在子Goroutine开始后立即执行并输出“主Goroutine结束。”。

    Goroutine的性能优化

    Goroutine的性能优化主要体现在以下几个方面:

    1. 设置GOMAXPROCS:通过runtime.GOMAXPROCS(n)设置可以并行执行的最大CPU核数。合理设置这个值可以提高程序的并行度。

      • 示例:runtime.GOMAXPROCS(2),表示最多同时使用2个CPU核心。
    2. 避免竞态条件:多个Goroutine访问共享变量时,可能会出现竞态条件。可以通过Channel或sync包中的同步原语(如Mutex、RWMutex)来避免。

    3. 合理分配任务:在高并发场景下,合理分配任务可以提高程序的执行效率。

    示例代码
    package main
    
    import (
        "fmt"
        "math"
        "runtime"
        "sync"
        "time"
    )
    
    func findPrime(num int, inChan chan int, primeChan chan int, exitChan chan bool) {
        var flag bool
        for {
            v, ok := <-inChan
            if !ok {
                break
            }
            flag = true
            for i := 2; i < int(math.Sqrt(float64(v))); i++ {
                if v % i == 0 {
                    flag = false
                    break
                }
            }
            if flag {
                primeChan <- v
            }
        }
        exitChan <- true
    }
    
    func main() {
        num := 10000000  // 查找小于num的所有质数
        start := time.Now()
    
        inChan := make(chan int, num)
        primeChan := make(chan int, num)
        exitChan := make(chan bool, 4)  // 假设使用4个Goroutine
    
        go func() {
            for i := 2; i < num; i++ {
                inChan <- i
            }
            close(inChan)
        }()
    
        for i := 0; i < 4; i++ {
            go findPrime(num, inChan, primeChan, exitChan)
        }
    
        go func() {
            for i := 0; i < 4; i++ {
                <-exitChan
            }
            close(primeChan)
        }()
    
        count := 0
        for v := range primeChan {
            count++
        }
        fmt.Printf("找到%d个质数,用时:%v\n", count, time.Since(start))
    }
    

    解释

    • Channel通信:通过Channel传递数据,避免了共享变量带来的竞态条件。
    • 并行计算:通过启动多个Goroutine并行计算质数,提高了程序的执行效率。
    • 同步机制:通过exitChan等待所有Goroutine完成任务。

    实际应用中的使用场景

    1. 并发请求处理:在Web服务器中,通过Goroutine并行处理多个HTTP请求,提高吞吐量。
    2. 数据处理:在数据处理任务中,通过Goroutine并行处理不同的数据片段,提高处理速度。
    3. I/O密集型任务:在I/O密集型任务(如文件读写、网络通信)中,Goroutine可以通过非阻塞的方式提高资源利用率。

    总结

    Goroutine是Golang语言中的并行编程核心,具有轻量级、高效和灵活的特点。通过合理利用Goroutine,可以显著提高程序的性能和响应速度。在实际应用中,需要注意避免竞态条件,合理分配任务,并通过Channel等方式实现Goroutine之间的安全通信。


    Runtime与Routine的关系

    在Go语言中,runtimeroutine是两个密切相关但又有明确区别的概念。runtime主要指的是Go语言的运行时环境,它为Go程序的执行提供了必要的支持,如内存管理、Goroutine调度、错误处理等。而routine在Go语言中特指Goroutine,即轻量级的线程。

    具体来说:

    1. Runtime

      • 定义runtime是一个Go语言标准库中的包,提供了与程序运行时环境相关的功能。
      • 功能
        • Goroutine调度:管理Goroutine的创建、执行和调度。
        • 内存管理:负责内存的分配和回收,包括垃圾回收机制。
        • 错误处理:提供了一些与错误处理相关的函数,如runtime.Error
        • 与操作系统交互:处理程序运行时的环境,如线程数、CPU核数等。
        • 性能监控:提供了一些与性能监控相关的函数,如内存使用统计。
      • 常用函数
        • Gosched():让出当前Goroutine的执行权限,允许其他Goroutine运行。
        • GOMAXPROCS():设置或获取可以并行执行用户级处理的最大CPU核数。
        • NumCPU():返回当前可用的CPU核数。
        • Goexit():使当前Goroutine退出,然后调度器会打印当前的栈跟踪信息。
    2. Routine(Goroutine)

      • 定义:Goroutine是Go语言中的轻量级线性,它由Go的运行时环境管理。
      • 特点
        • 轻量级:创建和销毁Goroutine的开销比传统线程低。
        • 高并发:可以轻松支持数万甚至更多的Goroutine同时运行。
        • 非阻塞式调度:基于协作式调度,主动让出CPU时间片。
      • 创建方式:通过go关键字启动一个新的Goroutine。
      • 常见用途
        • 并发执行任务:将一个函数或方法转换为异步执行,不阻塞主Goroutine。
        • 处理I/O密集型任务:在I/O操作中,释放资源让其他Goroutine使用。
        • 并行计算:在多核CPU上并行执行任务以提高计算效率。
    3. 两者的关系

      • 调度管理runtime包负责管理Goroutine的调度,包括如何分配时间片、如何在多个CPU核间分配Goroutine等。
      • 资源管理:通过runtime包,可以控制Goroutine的数量和并行度,优化程序性能。
      • 协作式调度:Goroutine通过runtime.Gosched()主动让出CPU时间片,协作式调度依赖于Goroutine自身的配合,而不是操作系统强制切换。

    补充(代码)

    package main
    
    import (
    	"fmt"
    	"math"
    	"runtime"
    	"time"
    )
    
    func PrintCallerInfo() {
    	//返回调用栈的信息(文件名、行号等)。
    	//参数:skip表示从调用Caller开始往回数多少层的调用栈信息。
    	// 	skip = 0: 返回调用runtime.Caller的函数(即直接包含runtime.Caller(0)调用的那行代码)的文件名、行号等信息。
    	// skip = 1: 返回调用runtime.Caller所在函数的直接调用者的文件名和行号。换句话说,就是调用了包含runtime.Caller(1)的函数的地方的调用栈信息。
    	// skip = 2: 返回上一步调用者的调用者信息,依此类推。每增加1,就向上追溯一个调用栈帧。
    	// 以此类推,随着skip值的增加,可以逐级向上追溯到更早的调用栈信息。如果指定的层级超出了实际存在的调用栈层数,则ok将为false,其他返回值(pc, file, line)将没有意义或为零值。
    	pc, file, line, ok := runtime.Caller(1)
    	if !ok {
    		fmt.Println("runtime.Caller() failed")
    		return
    	}
    	fmt.Printf("PC: %v, File: %s, Line: %d\n", pc, file, line)
    }
    
    func main() {
    	// 设置可以并行执行的最大CPU数,并返回之前的设置值;控制了Go程序可以同时使用的操作系统线程数量。
    	fmt.Println(runtime.GOMAXPROCS(2))
    
    	// 启动一个协程
    	go func(x int) {
    		//time.Sleep(1 * time.Second)
    		fmt.Println("Goroutine", x)
    	}(1)
    
    	// 终止
    	go func(x int) {
    		defer fmt.Println("Goroutine", x)
    		//time.Sleep(1 * time.Second)
    		runtime.Goexit() // 当前goroutine 退出
    		fmt.Println("This won't print")
    	}(2)
    
    	// 当前程序中活跃的gotoutines数量
    	fmt.Println("NumGoroutine:", runtime.NumGoroutine())
    
    	// 让出当前goroutine的处理器,允许其他等待的goroutines运行;
    	// 这是一个提示,而不是强制性的上下文呢切换
    	go func() {
    		for i := 1; i <= 5; i++ {
    			runtime.Gosched()
    			fmt.Println(i)
    		}
    	}()
    
    	go func(x int) {
    		//time.Sleep(1 * time.Second)
    		fmt.Println("Goroutine", x)
    	}(3)
    
    	go func(x int) {
    		//time.Sleep(1 * time.Second)
    		fmt.Println("Goroutine", x)
    	}(4)
    
    	// 其它
    	//runtime.BlockProfile() //获取当前的阻塞概要信息(block profile),它用于分析goroutine之间的同步问题,如锁竞争、通道阻塞等。
    	//runtime.Caller() //返回调用栈的信息(文件名、行号等)。
    	//runtime.Callers() //类似Caller,但返回的是多个调用栈帧的信息。
    	//runtime.GC() //:手动触发垃圾回收。通常不需要手动调用,Go的垃圾收集器会自动管理内存。
    	fmt.Println(runtime.GOROOT()) // 返回Go的安装目录
    	//runtime.KeepAlive(x any) //确保对象在其作用域结束前不会被垃圾回收。
    	//runtime.Stack() //将当前goroutine或所有goroutines的堆栈跟踪写入提供的缓冲区。
    
    	// runtime.Caller
    	//PrintCallerInfo()
    	//time.Sleep(3 * time.Second) // 确保协程有足够的时间完成
    
    	//高并发例子
    	FindPrime()
    	//单线程
    	noGoroutine()
    }
    
    func FindPrime() {
    	var num int
    	var n int
    	n, _ = fmt.Scanf("%d", &num)
    	fmt.Println(n, num)
    	n = num
    	if num > 32 { // 当num很大时,增大n(即goroutine的数量)却没有带来性能提升甚至导致耗时
    		n = 32
    	}
    	// 对于num=1000000
    	// n = 1, 即noGoroutine(),大约200ms~270ms
    	// n = 2,大约150ms~250ms
    	// n = 4,大约170ms~270ms
    	// n = 8,大约>220ms
    	// 对于num=10000000
    	// n = 1, 即noGoroutine(),大约5s~6s
    	// n = 2,大约2.7s~2.9s
    	// n = 4,大约1.6s~2.0s
    	// n = 8,大约1.8s~2s
    	// n = 16(开始耗时增加), 大约2.1s~2.2s
    	// 对于num=100000000
    	// n = 1, 即noGoroutine(),>100s
    	// n = 4,大约40s~41s
    	// n = 8,大约25s~26s
    	// n = 16, 大约22s~24s
    	// n = 32,大约·32s~33s
    
    	var primeChan = make(chan int, num)
    	var inChan = make(chan int, num)
    	var exitChan = make(chan bool, n)
    
    	var start = time.Now()
    
    	go inPrime(num, inChan)
    	for i := 0; i < n; i++ {
    		go primeJudge(inChan, primeChan, exitChan)
    	}
    	go func() {
    		for i := 0; i < n; i++ {
    			<-exitChan
    		}
    		close(primeChan)
    	}()
    	for {
    		_, ok := <-primeChan
    		if !ok {
    			break
    		}
    		//fmt.Println(res)
    	}
    	close(exitChan)
    	fmt.Println(time.Since(start))
    }
    
    func inPrime(num int, inChan chan int) {
    	for i := 2; i < num; i++ {
    		inChan <- i
    	}
    	close(inChan)
    }
    
    func primeJudge(inChan chan int, primeChan chan int, exitChan chan bool) {
    	var flag bool
    	for {
    		v, ok := <-inChan
    		//fmt.Println("v = ", v, "; ok = ", ok)
    		if !ok {
    			break
    		}
    		flag = true
    		for i := 2; i < int(math.Sqrt(float64(v))); i++ {
    			if v%i == 0 {
    				flag = false
    				break
    			}
    		}
    		if flag {
    			primeChan <- v
    		}
    	}
    	exitChan <- true
    }
    
    func noGoroutine() {
    	var num int
    	n, _ := fmt.Scanf("%d", &num)
    	fmt.Println(n, num)
    	var count = 0
    	var start = time.Now()
    	for i := 2; i <= num; i++ {
    		var flag = true
    		for j := 2; j < (int)(math.Sqrt(float64(i))); j++ {
    			if i%j == 0 {
    				flag = false
    				break
    			}
    			if flag {
    				count++
    			}
    		}
    	}
    	fmt.Println(time.Since(start))
    }
    

    <think>好的,我现在需要回答用户关于Go语言及其协程的问题。用户之前询问过RPC和HTTP的问题,现在转向Go语言,可能是在构建分布式系统或微服务,因此需要了解Go的并发特性。 首先,我需要确认自己对Go语言的了解是否全面。Go的协程goroutine)是其并发模型的核心,还有通道(channel)用于协程间通信。用户可能想知道协程的优势、工作原理以及其他语言线程的区别。 接下来,用户可能关心协程的实际应用场景,比如如何处理高并发、协程调度机制,以及如何避免常见问题,比如竞态条件或死锁。需要解释GMP模型,即Goroutine、M(机器线程)、P(处理器)的关系,说明Go运行时如何高效管理协程。 还要考虑用户可能的疑问,比如协程的资源消耗,为什么比线程轻量,以及如何通过同步机制如WaitGroup或Mutex来管理协程。需要举例说明,比如并发处理HTTP请求,或者并行计算。 此外,用户可能对协程的错误处理感兴趣,比如如何捕获协程中的panic,或者如何优雅地关闭协程。需要提及recover函数和context包的使用。 最后,确保回答结构清晰,分点说明,包括基本概念、底层原理、使用示例和注意事项。使用代码示例时,用简单的例子展示协程的启动和通信,比如计算斐波那契数列或并发获取多个API数据。 需要避免过于技术化的术语,用通俗的语言解释,同时保持准确性。可能还需要对比其他语言的并发模型,比如Java的线程Go的协程,突出Go的优势,如内存占用低、创建成本小、调度效率高等。 总结回答的结构:先介绍Go语言及其特点,然后重点讲解协程的定义、优势、工作原理,接着是使用示例和最佳实践,最后常见问题及解决方案。确保覆盖用户可能关心的各个方面,帮助用户全面理解Go协程的应用和原理。</think>以下是关于Go语言及其协程机制的详细说明: --- ### 一、Go语言核心特点 1. **C系血统** - 语法类似C,但去除指针运算、类继承等复杂特性 - 编译型语言,直接生成机器码(编译速度极快) 2. **并发原生支持** - 协程goroutine)为语言级并发单元 - 通道(channel)实现CSP通信模型 3. **内存管理** - 自动垃圾回收(GC) - 无手动内存管理需求 4. **工具链完善** - 内置格式化工具(go fmt) - 依赖管理(go mod) - 测试框架(go test) --- ### 二、Go协程goroutine)深度解析 #### 1. 基本特性 ```go func main() { go func() { // 使用go关键字启动协程 fmt.Println("goroutine执行") }() time.Sleep(100 * time.Millisecond) // 等待协程完成 } ``` - **轻量级**:初始栈仅2KB(线程通常MB级) - **低成本**:创建百万级协程无压力 - **快速切换**:用户态调度,无内核切换开销 #### 2. 调度模型(GMP) ![GMP模型示意图](https://example.com/gmp-model.png)*(图示:Go调度器工作原理)* - **G**(Goroutine):协程实体 - **M**(Machine):操作系统线程 - **P**(Processor):逻辑处理器(默认等于CPU核心数) | 组件 | 职责 | 数量关系 | |------|-----|---------| | G | 执行单元 | 动态变化 | | M | 系统线程 | 默认<=10000 | | P | 资源调度 | 默认=CPU核心数 | #### 3. 调度策略 - **工作窃取**(Work Stealing):空闲P从其他P队列偷G - **抢占式调度**:10ms强制切换(防止协程独占) - **系统调用优化**:分离网络轮询器(netpoller) #### 4. 并发控制 ```go var wg sync.WaitGroup func worker() { defer wg.Done() // 业务逻辑 } func main() { wg.Add(10) for i := 0; i < 10; i++ { go worker() } wg.Wait() // 等待所有协程完成 } ``` --- ### 三、协程 vs 线程对比 | 维度 | Go协程 | 系统线程 | |---------------|-------------------|------------------| | 创建成本 | 2KB初始内存 | 1MB以上 | | 切换开销 | 用户态200ns | 内核态1-2μs | | 调度方式 | Go运行时协作调度 | OS内核抢占调度 | | 并发规模 | 轻松百万级 | 通常数千级 | | 内存占用 | 动态伸缩(max1GB)| 固定栈大小 | --- ### 四、最佳实践注意事项 1. **通道使用原则** - 优先使用`chan struct{}`作为信号通道 - 带缓冲通道不是队列,慎用`cap(chan) > 0` 2. **资源竞争防范** ```go var counter int var mu sync.Mutex func safeIncrement() { mu.Lock() defer mu.Unlock() counter++ } ``` 3. **协程泄漏检测** - 使用`runtime.NumGoroutine()`监控协程数量 - 确保所有协程都有退出路径 4. **性能调优技巧** - 设置`GOMAXPROCS`(通常无需调整) - 避免在热路径中使用`defer` - 使用`sync.Pool`减少内存分配 --- ### 五、典型应用场景 1. **高并发服务** ```go func handleConn(conn net.Conn) { defer conn.Close() // 处理连接 } func main() { ln, _ := net.Listen("tcp", ":8080") for { conn, _ := ln.Accept() go handleConn(conn) // 每个连接独立协程 } } ``` 2. **并行计算** ```go func parallelProcess(data []int) { ch := make(chan result) for _, d := range data { go func(v int) { ch <- compute(v) // 并行计算 }(d) } // 收集结果... } ``` 3. **定时任务调度** ```go func cronJob() { ticker := time.NewTicker(1 * time.Hour) defer ticker.Stop() for { select { case <-ticker.C: doTask() } } } ``` --- Go语言的协程机制彻底改变了并发编程范式,使得开发者可以用同步的方式编写异步代码,极大降低了并发编程的心智负担。其设计在云计算、微服务、分布式系统等领域展现出显著优势,成为现代后端开发的利器。
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包

    打赏作者

    草海桐

    你的鼓励将是我创作的最大动力

    ¥1 ¥2 ¥4 ¥6 ¥10 ¥20
    扫码支付:¥1
    获取中
    扫码支付

    您的余额不足,请更换扫码支付或充值

    打赏作者

    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值