GO语言并发编程入门:Goroutine、Channel、Context、并发安全、GMP调度模型

news2025/1/9 16:34:30

GO语言并发编程入门:Goroutine、Channel、Context、并发安全、GMP调度模型

1.GO并发介绍

并发:多线程程序在一个核的cpu上运行。
并行:多线程程序在多个核的cpu上运行。
由上可知并发不是并行,并行是直接利用多核实现多线程的运行,并发则主要由切换时间片来实现”同时”运行,go可以设置使用核数,以发挥多核计算机的能力。

Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。Go语言的并发编程特点主要体现在Goroutine协程和Channel通道的使用上。

  • Goroutine协程:Goroutine是Go语言中的并发执行单位。它是一种轻量级的协程,由负责整个Go程序的执行的底层系统组件Go运行时(Go runtime)调度和管理。在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。与传统的线程、协程相比,Goroutine的创建和销毁代价非常小,可以高效地创建大量的Goroutine,Goroutine 奉行通过通信来共享内存,而不是共享内存来通信。Goroutine4~5KB的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是go高并发的根本原因。
  • Channel通道:通道是Goroutine之间进行安全通信和数据共享的机制。它提供了同步和互斥的功能,确保数据的有序传输和访问。通过通道,不同的Goroutine可以安全地进行数据交换和共享状态。

在Go语言中,Goroutine和操作系统线程是多对多的关系。具体来说:

  • 一个操作系统线程(OS Thread)可以对应多个Goroutine。
  • Go程序可以同时使用多个操作系统线程,这使得Go语言能够充分利用多核处理器的计算能力。
  • Go运行时调度器(Go Scheduler)负责将多个Goroutine调度到少量的操作系统线程上执行,并处理它们之间的通信。

2.GO并发编程

2.1 父子协程

在Go语言中,可以通过创建协程(Goroutine)来实现并发执行的任务。当父协程创建一个子协程时,父协程和子协程是相互独立的并发执行单元。父协程可以继续执行其他操作,而不需要等待子协程完成。子协程会在创建后立即开始执行,与父协程并发执行。父协程和子协程之间不存在直接的调用关系,它们是相互独立的执行流程。父协程的结束不会影响子协程的执行。即使父协程结束,子协程仍然可以继续执行,直到完成或被终止。父协程和子协程之间是独立的执行上下文,彼此之间的运行状态不会相互影响。
然而,需要注意的是,如果主协程(即main函数所在的协程)结束了,整个程序会终止,所有的协程也会被强制结束。这意味着如果主协程提前结束,尚未完成的子协程也会被中止。因此,在使用协程进行并发编程时,我们需要确保主协程不会过早地结束,以确保子协程能够完成任务,可以考虑采用以下方法:

  1. 使用time.Sleep使协程睡眠确保并发子协程完成

  2. 使用sync.WaitGroup等待组确保并发子协程完成

2.1.1 使用time.Sleep使协程睡眠确保并发子协程完成

time包提供了时间相关的功能,其中最常用的是time.Sleep函数,它可以让当前的Goroutine休眠一段时间。通过结合Goroutine和time.Sleep,我们可以实现协程的并发执行。

package main
import (
	"fmt"
	"time"
)
func main() {
	go task("Task 1")  // 启动协程1
	go task("Task 2")  // 启动协程2
	// 主协程休眠一段时间,确保协程有足够的时间执行
	time.Sleep(3 * time.Second)
}
func task(name string) {
	for i := 0; i < 5; i++ {
		fmt.Println(name+":", i) // 打印任务名称和当前迭代值
		time.Sleep(500 * time.Millisecond)
	}
}

在上面的示例中,我们通过启动两个协程(task1和task2)来实现并发执行。主协程(main函数)休眠3秒钟,确保协程有足够的时间执行。这样,我们就实现了协程的并发执行。

2.1.2 使用sync.WaitGroup等待组确保并发子协程完成

sync.WaitGroup文档介绍:https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#waitgroup
sync包提供了一些同步原语,如WaitGroup等待组,它可以用来等待一组协程的完成。通过WaitGroup,类似于操作系统中的PV信号量,可以实现协程的并发执行和同步等待。

package main
import (
	"fmt"
	"sync"
)
func main() {
	var wg sync.WaitGroup
	wg.Add(2) // 设置等待组的计数器为2,表示有两个协程需要等待
	go func() {//开启协程1
		defer wg.Done() // 协程完成后调用Done方法,减少等待组的计数器
		task("Task 1") 
	}()
	go func() {//开启协程1
		defer wg.Done() // 协程完成后调用Done方法,减少等待组的计数器
		task("Task 2") 
	}()
	wg.Wait() // 等待所有协程完成,完成后才结束main协程
}
func task(name string) {
	for i := 0; i < 5; i++ {
		fmt.Println(name+":", i) // 打印任务名称和当前迭代值
	}
}

在上述示例中,我们使用sync包中的WaitGroup来实现协程的并发执行和同步等待。通过调用wg.Add方法设置等待组的计数器为2,然后在每个协程中使用defer wg.Done()来减少计数器。最后,通过wg.Wait()等待所有协程完成。

2.2 Channel实现并发与协程通信

Channel文档介绍:https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-channel/
在并发编程中,单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义,对共享资源的正确访问需要精确的控制。
在目前的绝大多数语言中,都是通过加锁等线程同步方案来解决这一困难问题,而Go语言却另辟蹊径,它将共享的值通过Channel传递(实际上多个独立执行的线程很少主动共享资源)。channel像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序,可以让一个goroutine发送特定值到另一个goroutine的通信机制。在任意给定的时刻,最好只有一个Goroutine能够拥有该资源,数据竞争从设计层面上就被杜绝了。
这也是Go语言作者提出的并发编程哲学:不要通过共享内存来通信,而应通过通信来共享内存。

虽然我们在 Go 语言中也能使用共享内存加互斥锁进行通信,但是 Go 语言提供了一种不同的并发模型,即通信顺序进程(Communicating sequential processes,CSP)。Goroutine 和 Channel 分别对应 CSP 中的实体和传递信息的媒介,Goroutine 之间会通过 Channel 传递数据。

CSP(Communicating sequential processes)是一种并发编程模型,它强调通过通信来实现并发。在CSP中,程序被分解成一组独立的进程,这些进程通过通道进行通信。通道是一种同步的通机制,它允许进程之间传递数据。CSP模型的一个重要特点是,进程之间的通信是通过发送和收消息来实现的,而不是通过共享内存。
CSP模型的一个优点是,它可以避免一些常见的并发编程问题,例如死锁和竞态条件。这是因为CSP模型中的进程是独立的,们不会相互干扰或阻塞彼此。此外,CSP模型还可以使并发程序更易于理解和调试,因为它们的行为是通过进程之间的通信来定义的。

在这里插入图片描述
上图中的两个 Goroutine,一个会向 Channel 中发送数据,另一个会从 Channel 中接收数据,它们两者能够独立运行并不存在直接关联,但是能通过 Channel 间接完成通信。

2.2.1 使用Channel实现协程并发并进行协程通信

package main
import "fmt"
func main() {
	ch := make(chan int) // 创建一个int类型的通道

	go func() {
		ch <- 10 // 发送数据到通道
	}()

	data := <-ch // 从通道接收数据
	fmt.Println(data)
}

在上述示例中,我们通过make函数创建了一个无缓冲的int类型的通道ch,无缓冲的通道只有在有人接收值的时候才能发送值。就。因此,在匿名函数中使用ch <- 10将数据10发送到通道,就必须还要通过data := <-ch从通道中接收数据并打印。

2.2.2 使用通道实现生产者消费者模式

生产者消费者模式是并发编程中的常见模式,其中生产者生成数据并将其放入通道,而消费者从通道中取出数据并进行处理。以下是多对多的生产者消费者模式

package main

import (
"fmt"
"sync"
)

// producer 向通道发送数据
func producer(ch chan<- int, id int) {
	for i := 0; i < 5; i++ {
	ch <- i * id
	}
}
// consumer 从通道接收数据
func consumer(ch <-chan int, id int) {
	for i := range ch {
	fmt.Printf("消费者 %d 接收到数据: %d\n", id, i)
	}
}

func main() {
	ch := make(chan int, 10)
	// wg 用于等待所有协程完成
	var wg sync.WaitGroup
	// producerWg 用于等待所有生产者协程完成,根据该等待组判断何时关闭通道
	var producerWg sync.WaitGroup
	
	// 启动多个生产者协程
	for i := 0; i < 3; i++ {
		producerWg.Add(1) // 增加生产者等待组计数器
		wg.Add(1) // 增加总等待组计数器
		go func() {
		producer(ch, i+1)
		wg.Done() // 减少总等待组计数器
		producerWg.Done() // 减少生产者等待组计数器
	}()
}

	// 启动多个消费者协程
	for i := 0; i < 3; i++ {
		wg.Add(1) // 增加总等待组计数器
		go func() {
		consumer(ch, i+1)
		wg.Done() // 减少总等待组计数器
	}()
	}
	// 等待所有生产者协程完成后关闭通道
	producerWg.Wait()
	close(ch)
	// 等待所有协程完成
	wg.Wait()
}

在上面的示例中,我们创建了一个int类型的通道ch,并将其作为参数传递给生产者和消费者协程。生产者协程通过ch <- i将数据发送到通道,并打印相关信息。消费者协程使用num := range ch循环接收通道中的数据,并进行处理。通过关闭通道close(ch)来通知消费者协程数据已经全部发送完毕。

2.2.3 select多路复用

在某些场景下我们需要同时从多个通道接收数据,Go内置了select关键字,可以同时响应多个通道的操作。

  • select可以同时监听一个或多个channel,直到其中一个channel ready
  • 如果多个channel同时ready,则随机选择一个执行
  • 可以用于判断管道是否存满
package main
import (
    "fmt"
    "time"
)
func main() {
    // 创建两个缓冲区大小为 1 的通道
    ch1 := make(chan int, 1)
    ch2 make(chan int, 1)
    // 向 ch1 发送数据的 goroutine
    go func() {
        time.Sleep(2 * time.Second)
        ch1 <- 1
    }()
    // 向 ch2 发送数据的 goroutine
    go func() {
        time.Sleep(1 * time.Second)
        ch2 <- 2
    }()
    // 无限循环,等待从通道接收数据
    for {
        select {
        case x := <-ch1:
            fmt.Println("Received from ch1:", x)
            return
        case x := <-ch2:
            fmt.Println("Received from ch2:", x)
            return
        default:
            // 如果两个通道都已满,则打印一条消息并等待 500 毫秒
            fmt.Println("All channels are full")
            time.Sleep(500 * time.Millisecond)
            break
        }
    }
}

2.3 并发安全性与实现

在并发编程中,要注意并发安全性。并发安全性指的是在并发环境下,多个协程访问共享资源时,能够正确地进行同步和互斥,避免数据竞争和不一致的结果。

在Go语言中,可以通过以下方式实现并发安全:

  1. 使用互斥锁(Mutex):互斥锁是一种最基本的同步原语,它可以保证同一时刻只有一个 goroutine 可以访问共享资源在访问共享资源之前,需要先获取互斥锁,访问完成后再释放互斥锁。这样可以避免多个 goroutine 同访问共享资源导致的数据竞争问题。
  2. 使用读写锁(RWMutex):读写锁是一种特殊的互斥锁它可以同时支持多个 goroutine 对共享资源进行读操作,但只能有一个 goroutine 进行写操作。在读写锁中,读操作和写操作是互斥的,但多个读操作间是不互斥的。这样可以提高并发性能,减少锁的竞争。
  3. 使用原子操作(Atomic):原子操作是一种特殊的操作它可以保证在多个 goroutine 同时访问同一个变量时,对该变量的读写操作是原子的。原子操作可以避免数据竞争问题,但只适用于简单的数据类型,如整数、指针等。
  4. 使用通道(Channel):通道是一种特殊数据结构,它可以在多个 goroutine 之间传递数据,并且保证传递的数据是并发安全的。在使用通道时需要注意通道的缓冲区大小和通道的方向,以避免死锁和数据竞争问题。
  5. 使用 sync 包中的其他同步原语:Go语言的标准库中提供了许多同步原语,如条件变(Cond)、信号量(Semaphore)等,可以根据具体的需求选择合适的同步原语来实现并发安全。

2.3.1 使用互斥锁(Mutex)

Mutex文档介绍:https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#mutex

package main

import (
	"fmt"
	"sync"
)
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
    for i := 0; i < 5000; i++ {
        lock.Lock() // 加锁
        x = x + 1
        lock.Unlock() // 解锁
    }
    wg.Done()
}
func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
} 

2.3.2 使用读写锁(RWMutex)

RWMutex文档介绍:https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#rwmutex
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

package main

import (
	"fmt"
	"sync"
)
var (
    x      int64
    wg     sync.WaitGroup
    lock   sync.Mutex
    rwlock sync.RWMutex
)
func write() {
    rwlock.Lock() // 加写锁
    x = x + 1
    time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
    rwlock.Unlock()                   // 解写锁
    wg.Done()
}
func read() {
    rwlock.RLock()               // 加读锁
    time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
    rwlock.RUnlock()             // 解读锁
    wg.Done()
}
func main() {
    start := time.Now()
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go write()
    }
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go read()
    }
    wg.Wait()
    end := time.Now()
    fmt.Println(end.Sub(start))
}

2.3.3 使用原子操作(Atomic)

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

var counter int64 // 定义一个int64类型的计数器变量

func main() {
	var wg sync.WaitGroup
	// 启动10个goroutine,每个goroutine调用atomic.AddInt64方法自增计数器
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			atomic.AddInt64(&counter, 1)
		}()
	}
	wg.Wait()
	fmt.Println("Counter value:", counter)
}

2.3.4 使用通道(Channel)

package main
import (
	"fmt"
)
func main() {
 := make(chan int, 10) // 创建一个缓冲区大小为10的通道
	// 启动一个goroutine,向通道中写入数据
	go func() {
		for i := 0; i < 10;++ {
			ch <- i
		}
		close(ch) // 关闭通道
	}()
	// 从通道中读取数据
	for value := range ch {
		fmt.Println("Received value:", value)
	}
}

2.3.5 使用 sync 包中的其他同步原语(以信号量semaphore为例)

semaphore文档介绍:https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#semaphore

package main
import (
	"fmt"
	"olang.org/x/sync/semaphore"
	"sync"
)
var sem *semaphore.Weighted = semaphore.NewWeighted(1) // 创建一个权重为1的信号量
func main() {
	var wg sync.WaitGroup

	// 启动10个goroutine,每个goroutine获取信号量并输出信息
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			sem.Acquire(nil, 1) // 获取信号量
			defer sem.Release(1) // 释放信号量
			fmt.Println("Goroutine", id, "is running")
		}(i)
	}
	wg.Wait()
}

2.4 recovery恢复程序运行

在 Go 语言中,recovery 是一种机制,用于在程序发生 panic 时恢复程序的执行。当程序发生 panic 时,recovery 可以获 panic,并在程序崩溃前进行一些处理,例如输出日志、释放资源等。
在Go语言中,可以使用panic()函数来抛出一个异常,从而在协程中添加错误。例如,我们可以在子协程中添加一个除数为0的错误,如下所示:

package main

import (
    "fmt"
    "time"
)
func childRoutine() {
    defer func() {
        if r := recover(); r != nil {
            // 处理子协程中的错误
            fmt.Println("子协程出现错误:", r)
        }
    }()
    // 子协程中的代码
    a, b := 10, 0
    c := a / b // 除数为0,会抛出异常
    fmt.Println("子协程执行完毕", c)
}
func main() {
    // 启动子协程
    go childRoutine()
    // 主协程中的代码
    time.Sleep(time.Millisecond * )
    fmt.Println("主协程执行完毕")
}

在这个示例中,我们在子协程中定义了两个变量ab,并将b的值设置为0然后,我们尝试将a除以b,这会抛出一个异常。在defer语句中,我们使用recover()函数捕获这个异常,并在控制台输出错误信息。

当我们运行这个程序时,子协程会抛出一个异常,但是由于我们使用了recover()函数来捕获异常,程序不会崩溃,而是会在控制台输出错误信息,并继续执行其他协程。这样,我们就可以在协程中添加错误,避免程序崩溃,更好地管理协程并处理异常,确保程序的稳定性。

3.Context上下文

Context文档介绍:https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/
在这里插入图片描述

在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。Go 服务的每一个请求都是通过单独的 Goroutine 处理的,HTTP/RPC 请求的处理器会启动新的 Goroutine 访问数据库和其他服务。

如下图所示,我们可能会创建多个 Goroutine 来处理一次请求,而 context.Context 的作用是在不同 Goroutine 之间同步请求特定数据、取消信号以及处理请求的截止日期。
在这里插入图片描述

  • context 主要用于控制协程的生命周期和传递上下文信息。通过 context,我们可以在协程之间传递一些额外的信息,例如请求 ID、超时时间、取消信号等。同时,context 还提供了一种机制来取消或超时协程,以避免因为某个协程长时间阻塞而导致整个程序变得不可用。
  • channel 主要用于协程的通信和同步。通过 channel,我们可以在协程之间传递数据和信号,例如任务、消息、事件等。同时, 还提供了一种机制来同步协程,以避免因为协程之间的竞争而导致数据不一致或死锁等问题。

以下是 context 在 Go 并发编程中的一些应用:

  1. 传递元数据:context 可以用于在多个 Goroutine 之间传递请求范围的元数据,例如请求 ID、认证令牌等。
  2. 取消操作:context 可以用于在多个 Goroutine 之间传递取消信号。当一个操作需要被取消时,可以使用 context 通知所有相关的 Goroutine 停止工作。
  3. 超时控制:context 可以用于设置操作的超时时间。当操作超过指定的时间限制时,context 会自动发送取消信号,使得相关的 Goroutine 可以及时停止工作。

以下是实现了这三种功能的简单代码示例

package main
import (
	"context"
	"fmt"
	"time"
)
type keyType string
const key keyType = "value"
// worker 是一个 Goroutine,它会不断检查 context 中的元数据值
// 当元数据值达到 5 或超过 2 秒时,Goroutine 将被取消。
func worker(ctx context.Context) {
	select {
	case <-ctx.Done():
		fmt.Println("已超时,结束协程")
		return
	default:
		id := ctx.Value(key).(int) // 从 context 中获取数据值
		if id >= 5 {
			fmt.Println("id超过5,结束协程")
			return
		}
		fmt.Printf("接收到查询id为%d\n", id)
		time.Sleep(500 * time.Millisecond)
	}
}
func main() {
	// 创建一个带有超时功能的 context,超时时间为 2 秒
	//context.Background() 作为根 context,返回回一个空的 context
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	// 使用 context.WithValue 为 context 添加一个名为 value 的元数据,初始值为 0
	ctx = context.WithValue(ctx, key, 0)
	// 逐渐增加 context 中的元数据值,并在循环结束后等待 3 秒,以确保 Goroutine 有足够的执行。
	for i := 1; i <= 10; i++ {
		time.Sleep(300 * time.Millisecond)
		ctx = context.WithValue(ctx, key, i) // 更新 context 中的元数据值
		// 启动 worker Goroutine
		go worker(ctx)
	}
	time.Sleep(3 * time.Second)
}

4. GMP调度模型

GMP模型是一个高效的并行计算模型,它是Go语言运行时系统的一部分,负责将Go程序中的goroutine分配给多个处理器,以实现并行计算,从而提高程序的性能。
GMP调度模型支持任务窃取(task stealing)机制。当一个处理器空闲时,它可以从其他处理器的工作队列中窃取一个gor进行处理。这种机制可以使得goroutine的负载更加均衡,从而提高程序的性能。
GMP调度模型支持动态调整处理器数量的功能。当程序的负载发生变化时,调度器可以动态地增加或减少处理器的数量,以适应程序的需求。这种机制可以使得计算资源的利用更加高效。

GMP介绍文档:https://www.topgoer.cn/docs/golang/chapter09-11

4.1GMP 模型介绍

M(machine)

  • M代表着真正的执行计算资源,可以认为它就是os thread(系统线程)。
  • M是真正调度系统的执行者,每个M就像一个勤劳的工作者,总是从各种队列中找到可运行的G,而且这样M的可以同时存在多个。
  • M在绑定有效的P后,进入调度循环,而且M并不保留G状态,这是G可以跨M调度的基础。

P(processor)

  • P表示逻辑processor,是线程M的执行的上下文。
  • P的最大作用是其拥有的各种G对象队列、链表、cache和状态。

G(goroutine)

  • 调度系统的最基本单位goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等。
  • 在G的眼中只有P,P就是运行G的“CPU”。
  • 相当于两级线程
    在这里插入图片描述
  • 全局队列(Global Queue):存放等待运行的 G。
  • P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
  • P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
  • M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。

4.2调度器的设计策略

  1. 复用线程:避免频繁的创建、销毁线程,而是对线程的复用。

    • work stealing 机制:​当本线程无可用的G时,先从全局队列中取,如果全局队列中也没有的话,就取其他线程绑定的P偷取P,而不是销毁线程
    • hand off 机制:​ 当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。
  2. 利用并行:GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度,比如 GOMAXPROCS = 核数/2,则最多利用了一半的 CPU 核进行并行。

  3. 抢占:在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程,在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死,这就是 goroutine 不同于 coroutine 的一个地方。

  4. 全局 G 队列:在新的调度器中依然有全局 G 队列,当绑定的P中没有G可以执行的时候,就去全局G队列中找

4.3 go func () 调度流程

在这里插入图片描述

  1. 我们通过 go func () 来创建一个 goroutine;
  2. 有两个存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列。新创建的 G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中;
  3. G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系。M 会从 P 的本地队列弹出一个可执行状态的 G 来执行,如果 P 的本地队列为空,则首先从全局获取,若全局也为空就会向其他的 MP 组合偷取一个可执行的 G 来执行;
  4. 一个 M 调度 G 执行的过程是一个循环机制;
  5. 当 M 执行某一个 G 时候如果发生了 syscall 或则其余阻塞操作,M 会阻塞,如果当前有一些 G 在执行,runtime 会把这个线程 M 从 P 中摘除 (detach),然后再创建一个新的操作系统的线程 (如果有空闲的线程可用就复用空闲线程) 来服务于这个 P;
  6. 当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列。如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中。

4.4调度器的生命周期

在这里插入图片描述

  • M0:m0就是进程启动后的初始线程

  • G0:代表着初始线程的stack

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

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

相关文章

C语言_数据类型[详细分析]

接上一篇&#xff1a;C语言_关键字_标识符简介 本次来分享C语言的数据类型&#xff0c;是博主的一些学习笔记的和心得的总结&#xff0c;话不多说&#xff0c;开始上菜&#xff1a; 此博主在CSDN发布的文章目录&#xff1a;我的CSDN目录&#xff0c;作为博主在CSDN上发布的文章…

如何零基础快速搭建一个后台管理系统

真在的大师&#xff0c;都永远怀着一颗学徒的心&#xff01;&#xff01;&#xff01; 大家好&#xff0c;我是为你们操碎了心的小编&#xff0c;今天我又带来了一款轻量级的saas后台管理框架&#xff0c;让你零基础也可快速搭建一个功能强大的后台管理系统。 niucloud-admin采…

什么是AOP,如何实现?(有落地代码)

AOP 的核心思想是将横切关注点抽象为一个独立的模块&#xff08;称之为“切面”&#xff09;&#xff0c;然后在需要应用它的地方进行调用。比如&#xff0c;在需要记录日志的方法中&#xff0c;我们可以定义一个切面来负责日志记录&#xff0c;这样所有调用该方法的地方都会被…

hugging face开源的transformers模型可快速搭建图片分类任务

2017年,谷歌团队在论文「Attention Is All You Need」提出了创新模型,其应用于NLP领域架构Transformer模型。从模型发布至今,transformer模型风靡微软、谷歌、Meta等大型科技公司。且目前有模型大一统的趋势,现在transformer 模型不仅风靡整个NLP领域,且随着VIT SWIN等变体…

关于I/O

I/O 1. 概念1.1 页缓存的简单工作流程1.2 页缓存的写机制或者写触发的时机1.3 为什么需要套字节缓冲区1.4 套接字缓冲区的简单流程 2. 传统I/O方式2.1 传统I/O读写流程2.2 传统 I/O的性能问题 3. DMA技术3.1 将数据写入磁盘的流程3.2 从磁盘读取数据的流程 4. 网络数据传输流程…

【python】制作一个点单小程序!

周末总是在吃的方面&#xff0c;及其纠结&#xff0c;今天来制作一个点单小程序&#xff0c;加入自己喜欢吃的东西&#xff0c;来慢慢挑选&#xff0c;让每个周末快乐无限&#xff01; 一.安装环境 python 3.7.8 QT xlrd、xlwt库使用pip接口进行安装 pip install xlrd pip …

DMBOK知识梳理for CDGA/CDGP——第一章数据管理

关 注gzh“大数据食铁兽“&#xff0c;回复“知识点”获取《DMBOK知识梳理for CDGA/CDGP》常考知识点&#xff08;第一章数据管理&#xff09; 第一章 数据管理 第一章在 CDGA|CDGP考试中分值占比均不是很高&#xff0c;主要侧重点是考概念性的知识&#xff0c;理解数据管理的…

设计模式 -第1部分 避免浪费- 第1章 Flyweight 模式 - 共享对象避免浪费

第1部分 避免浪费 注&#xff1a;其内容主要来自于【日】-结城浩 著《图解设计模式》20章节 极力推荐大家阅读原著 第1章 Flyweight 模式 - 共享对象避免浪费 1.1 Flyweight 模式 Flyweight 的意思"轻量级"&#xff0c;其在英文中的原意指比赛中选手体重最轻等级的一…

迪赛智慧数——饼图柱状图(基本饼图和基本柱状图):“怒路症”数据解读

效果图 35%的司机承认自己属于“路怒族”&#xff0c;还有65%的人表示自己不是“路怒族”。 近日&#xff0c;上海两车高架上斗气碰撞差点掉落高架&#xff0c;上海高架出现“史诗级”斗气车。小编在此呼吁大家&#xff0c;开车路上减压&#xff0c;避免坏情绪伴随&#xff0c…

DDoS攻击与防御(一)

前言 这章主要讲述DDoS攻击与防御方式 理论知识来源于 https://www.microsoft.com/zh-cn/security/business/security-101/what-is-a-ddos-attack 1&#xff1a;攻击 一般来说&#xff0c;DDoS 攻击分为三大类&#xff1a;容量耗尽攻击、协议攻击和资源层攻击。 1>容量耗尽…

shiro基于redis实现分布式权限管理,在加入shiro的缓存管理后,项目报错

shiro基于redis实现分布式权限管理&#xff0c;在加入shiro的缓存管理后&#xff0c;项目报错 报错信息概括解决其他详细报错信息 报错信息概括 2023-05-24 16:27:56.374 ERROR 28740 --- [nio-8092-exec-6] o.a.s.web.servlet.AbstractShiroFilter : session.touch() method …

水处理计算常用表格大全

第二章 设计方案城市污水处理厂的设计规模与进入处理厂的污水水质和水量有关&#xff0c;污水的水质和水量可以通过设计任务书的原始资料计算。2.1 厂址选择在污水处理厂设计中&#xff0c;选定厂址是一个重要的环节&#xff0c;处理广的位置对周围环境卫生、基建投资及运行管理…

加强密码安全,保护您的账户——ADSelfService Plus

在当今数字化时代&#xff0c;密码安全成为了每个人都需要关注的重要问题。随着越来越多的个人和组织依赖于互联网和电子系统进行业务和通信&#xff0c;确保账户的安全性变得尤为关键。在这方面&#xff0c;ADSelfService Plus是一个功能强大的解决方案&#xff0c;为用户提供…

版图设计工具解析-virtuoso的display.drf文件解析

1. display.drf文件解析 virtuoso的版图颜色定义分析 下图为virtuoso的版图颜色&#xff0c;包括填充&#xff0c;轮廓&#xff0c;彩点&#xff0c;线形 本文以smic18mmrf的display.drf文件进行解析 smic18的PDK包下存在display.drf文件 打开文件display.drf文件后看到如下…

ApiKit 简介安装以及如何使用

一、介绍 ApiKit 是接口管理、开发、测试全流程集成工具&#xff0c;定位 API 管理 Mock 自动化测试 异常监控 团队协作。 1、开发测试过程中的现状 yapi -- 管理接口文档 rap -- 前端开发mock数据 postman -- 开发调试接口、测试调用接口 jmeter -- 基本的压力测试 2…

1个普通Java程序员需要具备什么样的素质和能力才可以称得上高级工程师?

1个Java程序员具备什么样的素质和能力才可以称得上高级工程师&#xff1f; 这个问题也引发了我的一些思考&#xff0c;可能很多人会说&#xff0c;“作为高级工程师&#xff0c;基础得过硬、得熟练掌握一门编程语言、至少看过一个优秀开源项目的源代码、有过高并发/性能优化的…

【RocketMQ】RocketMQ入门

【RocketMQ】RocketMQ入门 文章目录 【RocketMQ】RocketMQ入门1. 消费模式2. 发送/消费 消息2.1 同步消息2.2 异步消息2.3 单向消息2.4 延迟消息2.5 批量消息2.6 顺序消息 1. 消费模式 MQ的消费模式大致分为两种&#xff0c;一种是推Push&#xff0c;一种是拉pull。 Push模式…

在变压器厂中使用 ISA-95 应用程序进行调度集成

介绍 在工业批量和连续生产/运营环境中&#xff0c;调度涉及将诸如罐、反应器和其他加工设备之类的资源分配给生产/运营任务。第 4 层生产/运营计划确定要制造什么产品、要制造多少产品以及何时制造。根据设备、物料、人员和班次的可用性&#xff0c;随着时间的推移分配资源。…

CSDN中如何获得铁粉(用心篇)

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

快速实现pytest自定义配置项,让Web自动化测试更便捷!

目录 前言&#xff1a; 一、什么是pytest.ini 二、在pytest.ini中添加自定义配置项 三、使用自定义配置项 四、结论 前言&#xff1a; WEB自动化测试是一个重要的环节&#xff0c;需要结合框架和工具进行开发。在WEB自动化测试中&#xff0c;常用的是pytest框架&#xff…