Go并发编程 Goroutine、Channel、Select、Mutex锁、sync、Atomic等

news2024/11/17 19:37:17

本文所有实例代码运行go版本:go version go1.18.10 windows/amd64

1 并发编程介绍

1.1 串行、并发、并行

  • 串行:所有任务一件一件做,按照事先的顺序依次执行,没有被执行到的任务只能等待。最终执行完的时间等于各个子任务之和。

在这里插入图片描述

  • 并发:是以交替的方式利用等待某个任务的时间来处理其他任务计算逻辑,在计算机中,例如一个单核CPU,会通过时间片算法,来高效合理的分配cpu计算资源。从用户角度来看似乎是多个任务在同时执行。

在这里插入图片描述

  • 并行:在同一时刻处理计算多个任务,以多核CPU为例,可以实现同时处理计算多个任务,一个CPU负责一个任务的计算逻辑,大家做到同时进行,就像三个任务有三个工人同时干活一样。

在这里插入图片描述

1.2 进程、线程、协程

  • 进程:是程序运行的基本单位,每个进程都有自己的独立内存空间,不同的进程可以通过进程之间的互相通信进行交流。比如:电脑上的 QQ、微信、WPS等都有各自的进程。在操作系统级别来看,进程是操作系统对一个正在运行的程序的一种抽象。一个系统里面可以同时运行多个进程,而每一个进程又好像是在独占的使用硬件资源,通过处理器在进程之间不停的切换来实现。
  • 线程:线程是处理器(CPU)资源分配和调度的基本单位,在一个进程中可以有多个线程,每个线程都运行在进程的环境上下文中,不同线程之间可以通过线程之间通信进行数据交换。比如:在360安全卫视进程中,你可以同时进行垃圾清理和病毒查杀,在微信中,你可以刷朋友圈同时接收消息。
  • 进程和线程区别
    • 创建和开销方面,进程的创建需要系统分配内存和CPU,文件句柄等资源,销毁时也要进行相应的回收,所以进程的管理开销很大;但是线程的管理开销则很小。
    • 进程之间不会相互影响;而一个线程崩溃可能会导致进程崩溃,从而影响同个进程里面的其他线程。
    • 线程是进程的子任务,是处理器(CPU)分配和调度的基本单位,进程是对运行时程序的封装,是系统进行资源分配和调度的基本单元。
  • 协程:在理解协程之前,需要明白线程的几个问题:
    • 1、在执行过程中分为用户态和内核态,两个状态的切换会造成资源开销;
    • 2、面线程创建的越多,CPU切换的就越频繁,因为操作系统的调度要保证相对公平
    • 3、线程的创建、销毁都需要调用系统调用,每次请求都创建,高并发下开销就显得很大,而且线程的数量不能太多,占用内存是 MB 级别。
    • 基于上面的问题,协程被提出,协程是用户态(用户空间)的一种抽象,对操作系统内核而言并没有这个概念,依然是以线程维度调度。协程的主要思想是在用户态实现调度算法,来达到用少量线程,处理大量任务的调度,因为是用户态调度切换,不涉及内核切换和不同线程之间的上下文切换,大大减少开销。

最后来一个图描述三者之间的关系:

在这里插入图片描述

2 并发核心-Goroutine

2.1 goroutine介绍

在Go中使用goroutine来实现并发,在Go中协程的概念最终落地到goroutine中,可以称为Go协程、协程Coroutine等。goroutine是由Go的运行时(runtime)调度和管理的,Go程序会将 goroutine 中的任务合理地分配给每个CPU。

在Go并发编程中无需关注:进程、线程、协程的概念,无需写创建和销毁的代码,只需要关注goroutine即可。而且goroutine的使用相当简单,Go在语言层面提供go关键字去开启一个goroutine。例如你想让函数 fun1 使用goroutine执行:

go func1()

2.2 使用goroutine

为一个函数创建一个goroutine,只需要在调用函数的时候在前面加上go关键字即可。

func func1() {
    fmt.Println("Hello F1!")
}
func main() {
    go func1()
    fmt.Println("main goroutine done!")
    // 这里睡一会,防止main结束后,func1 来不及运行
    time.Sleep(time.Second)
}

两次运行结果可能不一样

PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
Hello F1!
main done!
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
main done!
Hello F1!

go关键字可以用于匿名函数,并且goroutine实现多个并发非常简单,如下启动是个goroutine:

func main() {
	for i := 0; i < 10; i++ {
		go func(n int) {
			fmt.Println("执行了:", n)
		}(i)
	}
	fmt.Println("main done!")
	// 这里睡一会,防止main结束后,func1 来不及运行
	time.Sleep(time.Second)
}

运行结果:

PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
main done!
执行了: 4
执行了: 5
执行了: 1
执行了: 0
执行了: 7
执行了: 2
执行了: 3
执行了: 8
执行了: 9
执行了: 6

2.3 goroutine资源

在第一章中知道,线程是由操作系统内核进行调度的,涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,调度成本和开销比较大,而goroutine是由go runtime进行管理调度,大量的goroutine映射到少量的线程中去,其调度和切换更加轻量,基本都在用户态完成。一个goroutine的栈只有几K大小,非常轻量,轻松是实现10W级别并发支持。

3 数据交换-Channel

3.1 Channel是什么

在第二章介绍知道,go天然有高并发的特性,并且实现简单,但在实际开发中,难免会遇到不同并发线程(协程)之间进行数据交换和通信,在Java等编程语言中可以通过共享内存数据(即某个对象后者变量)实现不同线程之间数据传递和通信,同时为了保证数据的安全性需要合理的加锁。

在goroutine中,引入了一个新的概念 channel 通道,来实现数据传递,可以将channel看做是联通多个goroutine的数据桥梁,可以让一个goroutine发送特定的数据到另一个goroutine中去,并且保证先进先出的顺序。

在这里插入图片描述

3.2 Channel的语法和使用

3.2.1 channel定义

在go中channel是一种类型,可以通过和变量一样的方式进行定义,如下:

var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool  // 声明一个传递布尔型的通道
var ch3 chan []string // 声明一个传递string切片的通道
var ch3 chan MyStruct // 声明一个结构体类型的通道
fmt.Printf("ch1:%#v\n", ch1)
fmt.Printf("ch2:%#v\n", ch2)
fmt.Printf("ch3:%#v\n", ch3)
fmt.Printf("ch4:%#v\n", ch4)
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
ch1:(chan int)(nil)
ch2:(chan bool)(nil)
ch3:(chan []string)(nil)
ch4:(chan main.MyStruct)(nil)

从程序执行结果可以看出channel是引用类型(nil),通道声明后默认值nil值。

3.2.2 channel初始化

可以使用go内置make函数进行初始化:

fmt.Println("开始初始化")
ch1 = make(chan int, 10)     // 初始化一个int通道,通道缓冲大小10
ch2 = make(chan bool, 20)    // 初始化一个bool通道,通道缓冲大小20
ch3 = make(chan []string)    // 初始化一个[]string通道,无通道缓冲
ch4 = make(chan MyStruct, 5) // 初始化一个MyStruct通道,通道缓冲大小5
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
ch1:(chan int)(nil)
ch2:(chan bool)(nil)
ch3:(chan []string)(nil)
ch4:(chan main.MyStruct)(nil)
开始初始化
ch1:(chan int)(0xc0000180b0)
ch2:(chan bool)(0xc000112080)
ch3:(chan []string)(0xc00005c060)
ch4:(chan main.MyStruct)(0xc00005c0c0)

3.2.3 channel操作

channel发送接收数据使用 <-符号

  • 发送:向ch1通道中发送一个1-5五个数字
ch1 <- 1
ch1 <- 2
ch1 <- 3
ch1 <- 4
ch1 <- 5
  • 接收:从ch1中接收数字,这里为了方便就for循环接收五次,注意:通道在没有数据接收时,会进行阻塞
for i := 0; i < 5; i++ {
    n := <-ch1
    fmt.Println("从ch1中接收数据:", n)
}
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
从ch1中接收数据: 1
从ch1中接收数据: 2
从ch1中接收数据: 3
从ch1中接收数据: 4
从ch1中接收数据: 5
  • 关闭:一个通道可以通过内置函数close进行关闭,关闭后的通道不能再发送数据,但还可以接收数据,如果管道中还有数据则正常接收,如果没有则返回0
close(ch1)
ch1 <- 6 // 这里会报错
fmt.Println("从ch1中接收数据:", <-ch1)
panic: send on closed channel

goroutine 1 [running]:
main.main()
        D:/dev/go/workspace/go_demo_code/temp/t1.go:36 +0x32c
exit status 2
  • 通道关闭后,如何感知?当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型值,那么在循环操作通道的时候如何感知通道被关闭了,可以通过如下方式。
// 通道关闭后手动break
for {
    n, ok := <-ch1
    if !ok {
        break
    }
    fmt.Println("从ch1中接收数据:", n)
}
// 通道关闭后会自动退出for range循环
for i := range ch1 {
    fmt.Println(i)
}

完整代码:

package main

import "fmt"

type MyStruct struct {
}

func main() {
	var ch1 chan int      // 声明一个传递整型的通道
	var ch2 chan bool     // 声明一个传递布尔型的通道
	var ch3 chan []string // 声明一个传递string切片的通道
	var ch4 chan MyStruct // 声明一个结构体类型的通道
	fmt.Printf("ch1:%#v\n", ch1)
	fmt.Printf("ch2:%#v\n", ch2)
	fmt.Printf("ch3:%#v\n", ch3)
	fmt.Printf("ch4:%#v\n", ch4)

	fmt.Println("开始初始化")

	ch1 = make(chan int, 10)     // 初始化一个int通道,通道缓冲大小10
	ch2 = make(chan bool, 20)    // 初始化一个bool通道,通道缓冲大小20
	ch3 = make(chan []string)    // 初始化一个[]string通道,无通道缓冲
	ch4 = make(chan MyStruct, 5) // 初始化一个MyStruct通道,通道缓冲大小5

	ch1 <- 1
	ch1 <- 2
	ch1 <- 3
	ch1 <- 4
	ch1 <- 5

	for i := 0; i < 5; i++ {
		n := <-ch1
		fmt.Println("从ch1中接收数据:", n)
	}
	close(ch1)
	ch1 <- 6
	fmt.Println("从ch1中接收数据:", <-ch1)
	fmt.Println("从ch1中接收数据:", <-ch1)
}

3.3 channel实战

需求:定义两个int类型的 channel,开启三个goroutine,go1 发送数据到到通道 ch1,go2接收ch1通道中的数值进行平方操作,再将结果写入到ch2,go3接收ch2的数据进行打印输出。

package main

import (
	"fmt"
	"time"
)

func main() {

	var ch1 = make(chan int, 5)
	var ch2 = make(chan int, 5)

	go func1(ch1)

	go func2(ch1, ch2)

	go func3(ch2)

    // 主函数睡眠
	time.Sleep(time.Second)

}

func func1(ch chan int) {
	for i := 0; i < 10; i++ {
		fmt.Println("发送一个数据:", i)
		ch <- i
	}
}

func func2(ch1, ch2 chan int) {
    // 这里死循环无限接收
	for {
		n := <-ch1
		fmt.Println("对数据进行平方处理:", n)
		ch2 <- n * n
	}
}

func func3(ch chan int) {
    // 这里死循环无限接收
	for {
		n := <-ch
		fmt.Println("接收到数据了直接打印:", n)
	}
}

运行结果:

PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
发送一个数据: 0
发送一个数据: 1
发送一个数据: 2
发送一个数据: 3
发送一个数据: 4
发送一个数据: 5
发送一个数据: 6
对数据进行平方处理: 0
对数据进行平方处理: 1
对数据进行平方处理: 2
对数据进行平方处理: 3
对数据进行平方处理: 4
对数据进行平方处理: 5
对数据进行平方处理: 6
发送一个数据: 7
发送一个数据: 8
发送一个数据: 9
接收到数据了直接打印: 0
接收到数据了直接打印: 1
接收到数据了直接打印: 4
接收到数据了直接打印: 9
接收到数据了直接打印: 16
接收到数据了直接打印: 25
接收到数据了直接打印: 36
对数据进行平方处理: 7
对数据进行平方处理: 8
对数据进行平方处理: 9
接收到数据了直接打印: 49
接收到数据了直接打印: 64
接收到数据了直接打印: 81

4 多路复用-Select

第三章我们知道了可以通过channel进行多个goroutine的数据交换,在使用通道时,如果没有数据接收会阻塞,处理监听多个通道,就无法通过一个goroutine很好的接收数据。这种情况go提供了select,多路复用,可以同时监听多个channel,使用如下:

select {
   case c1 := <- ch1:
      fmt.Println("c1=", c1)
   case c2 := <- ch1:
      fmt.Println("c2=", c2)
}
  • select语句是专为通道而设计的,所以每个case表达式中都只能包含操作通道的表达式
  • select 默认阻塞,只有监听的channel中有发送或者接受数据时才运行
  • 设置default则不阻塞,通道内没有待接受的数据则执行default
  • 多个channel准备好时,会随机选一个执行

5 并发安全-Mutex锁

在使用多个goroutine操作临界资源(共享资源),就会发生竞争情况,出现数据安全问题,最总结果和期望的不一致,如下:


var num int

func main() {
	go add()
	go add()
	time.Sleep(time.Second * 2)
	fmt.Println(num)
}

func add() {
	for i := 0; i < 10000; i++ {
		num++
	}
}

运行三次,两次结果都错误:

PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
15737
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
20000
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
15825

5.1 互斥锁

互斥锁顾名思义相互排斥,同一时间保证只有一个goroutine可以持有锁,进行共享资源的访问,在go中互斥锁使用sync.Mutex实现。

  1. 声明锁对象:var lock sync.Mutex
  2. 调用锁方法加锁或者解锁
    • lock.Lock():会一直等待直到获取锁
    • lock.TryLock():尝试获得锁,获取失败立即返回
    • lock.Unlock():释放锁
  3. 多个goroutine同时等待一个锁时,唤醒的策略是随机的。
var num int

// 声明一把锁
var lock sync.Mutex

func main() {
	go add()
	go add()
	time.Sleep(time.Second * 2)
	fmt.Println(num)
}

func add() {
	for i := 0; i < 10000; i++ {
        // 共享资源访问前加锁
		lock.Lock()
		num++
        // 操作完释放锁
		lock.Unlock()
	}
}

运行结果:

PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
20000
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
20000
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
20000

5.2 读写锁

和其他编程语言类似,go也提供了读写锁,提高读多写少场景的锁性能,分为读锁写锁两部分具有一下特性:

  1. 并发读操作不加锁, R R 不阻塞
  2. 一个goroutine获取读锁后,其他goroutine,获取写锁就会等待,R W 阻塞
  3. 一个goroutine获取写锁后,其他goroutine,获取读写锁都会等待,W R、W W 阻塞

一句话:读读共享,读写互斥,写写互斥


var num int

//var lock sync.Mutex

var rwlock sync.RWMutex

func main() {
	go add()
	go add()
	go read()
	time.Sleep(time.Second * 2)
	fmt.Println(num)
}

func read() {
	// 测试效果读取5次
	for i := 0; i < 5; i++ {
		// 加读锁
		rwlock.RLock()
		fmt.Println("读取:", num)
		// 释放读锁
		rwlock.RUnlock()
		// 让出CPU执行时间,后面会介绍
		runtime.Gosched()
	}

}

func add() {
	for i := 0; i < 10000; i++ {
		// 加写锁
		rwlock.Lock()
		num++
		// 释放写锁
		rwlock.Unlock()
	}
}
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
读取: 0
读取: 999
读取: 1212
读取: 1334
读取: 1335
20000

6 并发控制-Sync

多个goroutine并发运行时,难免需要对并发程序进行控制,如第五章的锁控制并发安全访问,灾还有一些常见的情况:

  1. 一个goroutine,需要等待多个goroutine完成任务再执行业务代码。
  2. 某些特定代码在并发场景下只希望被执行一次。
  3. 多个等待中的goroutine,接收到一个goroutine通知后,开始处理一些业务(如果单纯使用 chan 或互斥锁,那么只能有一个协程可以等待,并读取到数据)
  4. go默认的map是并发不安全的,实际开发中我们需要一些并发类的容器,例如map等

sync包提供了一些开箱即用的api和对象,帮助我们控制并发goroutine,解决实际需求。

6.1 sync.WaitGroup

WaitGroup类似Java的CountDownLatch,可以实现等待并发任务执行完, sync.WaitGroup有以下几个方法:

  • Add(delta int) 计数器+delta

  • Done() 计数器-1

  • Wait() 阻塞直到计数器变为0


var num int

//var lock sync.Mutex

var rwlock sync.RWMutex
//定义一个WaitGroup
var wg sync.WaitGroup

func main() {
    // 三个 goroutine,这里直接加三
	wg.Add(3)
	go add()
	go add()
	go read()
    // 等待所有goroutine任务结束
	wg.Wait()
	fmt.Println(num)
}

func read() {
	// 测试效果读取5次
	for i := 0; i < 5; i++ {
		// 加读锁
		rwlock.RLock()
		fmt.Println("读取:", num)
		// 释放读锁
		rwlock.RUnlock()
		// 让出CPU执行时间
		runtime.Gosched()
	}
    // 结束一个减一
	wg.Done()
}

func add() {
	for i := 0; i < 10000; i++ {
		// 加写锁
		rwlock.Lock()
		num++
		// 释放写锁
		rwlock.Unlock()
	}
	wg.Done()
}

6.2 sync.Once

sync.Once,用来保证某种行为只会被执行一次,高并发场景,可以用来处理一次性操作

核心函数:

func (o *Once) Do(f func()) 

例子:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {
	wg.Add(3)
	var once sync.Once
	go load(&once)
	go load(&once)
	go load(&once)

	wg.Wait()
	fmt.Println("主方法结束")
}

// 注意这里需要传入指针类型
func load(once *sync.Once) {
	fmt.Println("load方法被调用了")
	once.Do(func() {
		fmt.Println("无论多少次,once.Do只会调用一次")
	})
	wg.Done()
}

运行结果

PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
load方法被调用了
无论多少次,once.Do只会调用一次
load方法被调用了
load方法被调用了
主方法结束

6.3 sync.Cond

sync.Cond 基于互斥锁/读写锁,经常用在多个 goroutine 等待,一个 goroutine 通知的场景,当共享资源的状态发生变化的时候,它可以用来通知被互斥锁阻塞的 goroutine。

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	cond := sync.NewCond(&sync.Mutex{})
	go fun1(cond)
	go fun1(cond)
	go fun1(cond)

	go func() {
		time.Sleep(time.Second)
		fmt.Println("发出信号了")
		cond.Broadcast()
		//cond.Signal()
	}()

	time.Sleep(time.Second * 2)
	fmt.Println("主方法结束")
}

func fun1(cond *sync.Cond) {
	cond.L.Lock()
	fmt.Println("func1被调用了...")
	cond.Wait()
	fmt.Println("func1结束了")
	cond.L.Unlock()
}
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
func1被调用了...
func1被调用了...
func1被调用了...
发出信号了
func1结束了
func1结束了
func1结束了
主方法结束

6.4 sync.Map

看一个并发情况下操作map的代码,执行后会报错:fatal error: concurrent map writes


func main() {

	m := make(map[int]int)
	go put(m, 0)
	go put(m, 10)
	go put(m, 20)
	time.Sleep(time.Second * 2)
	fmt.Println("主方法结束")
}

func put(m map[int]int, start int) {
	for i := start; i < start+10; i++ {
		m[i] = i * i
	}
}

当然解决这个问题可以通过对map操作加锁,Go语言的sync包中还提供了一个开箱即用的并发安全版map sync.Map


func main() {

    // 声明一个并发map
	syncMap := &sync.Map{}

	go syncPut(syncMap, 0)
	go syncPut(syncMap, 10)
	go syncPut(syncMap, 20)
	time.Sleep(time.Second * 1)
	fmt.Println("主方法结束")
    // 遍历打印
	syncMap.Range(func(key, value any) bool {
		fmt.Printf("key=%v, value=%v\n", key, value)
		return true
	})
}

func syncPut(m *sync.Map, start int) {
	for i := start; i < start+10; i++ {
        // 安全的存储数据
		m.Store(i, i*i)
	}
}

7 原子操作 Atomic

在go中我们使用加锁来保证并发场景下数据安全访问,但加锁的代价比较大,涉及到内核态的上下文切换会比较耗时,go中针对基本数据类型的操作提供了atomic包,可以实现原子的操作基本数据类型。

// 原子性的获取*addr的值。
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

//原子性的将val的值保存到*addr。
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

//原子性的将val的值添加到*addr并返回新值。
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

//原子性的将新值保存到*addr并返回旧值。
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

//原子性的比较*addr和old,如果相同则将new赋值给*addr并返回真。
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

举一个简答的例子:

package main

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

func main() {
	var wg sync.WaitGroup
	var num int32
	wg.Add(3)

	go func() {
		for i := 0; i < 1000; i++ {
			atomic.AddInt32(&num, 1)
		}
		wg.Done()
	}()

	go func() {
		for i := 0; i < 1000; i++ {
			atomic.AddInt32(&num, 1)
		}
		wg.Done()
	}()

	go func() {
		for i := 0; i < 1000; i++ {
			atomic.AddInt32(&num, 1)
		}
		wg.Done()
	}()

	wg.Wait()

	fmt.Println(num)

}
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
3000

8 底层控制-Runtime

Go Runtime 后续专门写一篇文章深入介绍,这里只介绍几个简单的方法,初步了解Go Runtime。

  • runtime.GOMAXPROCS 设置多少个OS线程来同时执行Go代码,默认值是机器上的CPU核心数
  • runtime.Gosched() 让出CPU时间片,重新等待安排任务
  • runtime.Goexit() 退出当前协程
  • runtime.NumGoroutine() 查看当前Goroutine数量
  • runtime.NumCPU() 返回cpu数量
  • runtime.GC() 让运行时系统进行一次强制性的垃圾收集

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

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

相关文章

效率神器!神级ChatGPT浏览器插件分享

大家好&#xff0c;我是卷了又没卷&#xff0c;薛定谔的卷的AI算法工程师「陈城南」~ 担任某大厂的算法工程师&#xff0c;带来最新的前沿AI知识和工具&#xff0c;欢迎大家交流~&#xff0c;后续我还会分享更多 AI 有趣工具和实用玩法&#xff0c;包括AI相关技术、ChatGPT、AI…

初识SpringBoot -- SpringBoot入门保姆级教程(一)

文章目录 前言一、初识SpringBoot1.SpringBoot简介2.用编译器IDEA创建SpringBoot项目3.在官网创建SpringBoot项目4.SpringBoot项目快速启动&#xff08;前后端分离基本能力&#xff09;5.了解SpringBoot起步依赖和启动类 总结 前言 为了巩固所学的知识&#xff0c;作者尝试着开…

RocketMQ消息消费

RocketMQ消息消费示例代码&#xff1a; public static void main(String[] args) throws InterruptedException, MQClientException {DefaultMQPushConsumer consumer new DefaultMQPushConsumer("please_rename_unique_group_name_4");consumer.setNamesrvAddr(&qu…

C++ 学习 ::【基础篇:15】:C++ 类的基本成员函数:析构顺序问题(全局/静态/局部量) 及 类类型(自定义类型)与析构函数

本系列 C 相关文章 仅为笔者学习笔记记录&#xff0c;用自己的理解记录学习&#xff01;C 学习系列将分为三个阶段&#xff1a;基础篇、STL 篇、高阶数据结构与算法篇&#xff0c;相关重点内容如下&#xff1a; 基础篇&#xff1a;类与对象&#xff08;涉及C的三大特性等&#…

Nginx配置块location及rewrite详解(遗憾)

文章目录 一、location配置块详解1.location 大致分为三类2.location 常用的匹配规则3.location 匹配的优先级4.location 匹配流程5.location 的实际使用&#xff08;1&#xff09;直接匹配网站根目录首页&#xff08;2&#xff09;处理静态文件请求&#xff08;3&#xff09;通…

函数式接口相关知识点

这里写目录标题 函数式接口简介以及注意点函数式接口作为方法的参数函数式接口作为方法参数常用的函数式接口Supplier简介具体代码操作 Consumer简介具体代码演示演示1演示2 Predicate接口简介以及接口中的方法text和negate方法and和or方法Function方法简介具体操作1具体操作2 …

SQL语句之DDL语言

说明&#xff1a;DDL&#xff08;Data Definition Language&#xff0c;数据定义语言&#xff09;&#xff0c;用来定义数据库对象(数据库、表)&#xff0c;包括了数据库和表的创建、查询、使用和删除操作。 一、数据库操作 新安装的数据库&#xff0c;默认有以下四个数据库&…

计算机网络-数据链路层

概念 结点&#xff1a;主机、路由器 链路&#xff1a;结点间物理通道 数据链路&#xff1a;结点间逻辑通道&#xff0c;控制数据传输协议的硬件和软件加到链路上构成数据链路 帧&#xff1a;链路层的协议数据单元&#xff0c;封装网络层数据报 数据链路层负责通过一条链路从一…

银行转账问题(死锁)

本文主要讲述死锁的一个经典案例—银行转账问题&#xff0c;并对该问题进行定位、修复。 1. 问题说明 当账户A对账户B进行转账时&#xff0c; 首先需要获取到两把锁&#xff1a;账户A和账户B的锁。获取两把锁成功&#xff0c;且余额大于0&#xff0c;则扣除转出人的余额&…

我记不住的那些C语言的struct知识

背景&#xff1a; 最近在重学C语言&#xff0c;目的是为了能看懂操作系统的底层代码&#xff0c;也为后续使用C语言开发一个类似redis数据库的中间件做准备&#xff0c;于是又重新踏上了学习C语言的道路&#xff0c;早在上学期间就学习过C语言&#xff0c;但是很久都不用了&…

ssm学习-spring01

Spring_day01 今日目标 掌握Spring相关概念完成IOC/DI的入门案例编写掌握IOC的相关配置与使用掌握DI的相关配置与使用1,课程介绍 对于一门新技术,我们需要从为什么要学、学什么以及怎么学这三个方向入手来学习。那对于Spring来说: 1.1 为什么要学? 从使用和占有率看 Spri…

使用 ChatGPT API 构建系统(一):分类

今天我学习了DeepLearning.AI的 Building Systems with the ChatGPT API 的在线课程&#xff0c;我想和大家一起分享一下该门课程的一些主要内容。 下面是我们通过Openai API来访问ChatGPT模型的主要代码&#xff1a; import openai#您的openai的api key openai.api_key YOUR…

chatgpt赋能python:Python删除节点:从入门到实践

Python删除节点&#xff1a;从入门到实践 在任何编程语言中&#xff0c;删除节点都是一个极为常见的操作。在Python中&#xff0c;它同样非常重要&#xff0c;因为我们通常会使用Python处理各种数据结构&#xff0c;诸如树、链表等等。但是&#xff0c;删除节点并不总是一件容…

C++类和对象 -- 知识点补充

补充 const成员函数static成员友元内部类匿名对象拷贝对象时的一些编译器优化 const成员函数 将const修饰的成员函数称为const成员函数&#xff0c;const修饰类成员函数&#xff0c;实际是修饰该成员函数隐含的this指针&#xff0c;表明在该成员函数中不能对类的成员进行修改。…

javaWeb ssh自习室管理系统myeclipse开发mysql数据库MVC模式java编程计算机网页设计

一、源码特点 java ssh自习室管理系统是一套完善的web设计系统&#xff08;系统采用ssh框架进行设计开发&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S 模式开发。开发环境为TOMCAT7.0,…

预报名通道已开启,2023第11届国际生物发酵展,8月4-6日上海见!

新机遇、新挑战、新发展 同期展会&#xff1a;酵素产品与益生产品展 制药机械与包装技术展 生化仪器及实验室设备展 合成生物技术与生物制造展 展会时间&#xff1a; 2023年8月4日 9:00-17:00 2023年8月5日 9:00-17:00 2023年8月6日 9:00-15:00 展会地点&#xff1a…

【数据结构】栈和队列选择题和面试编程题

目录 一、选择题 二、栈和队列的面试题 1、括号匹配问题 1.1 题目说明 1.2 题目解析 2、用队列实现栈 2.1 题目说明 2.2 题目解析 3、用栈实现队列 3.1 题目说明 3.2 题目解析 一、选择题 1、若进栈序列为 1,2,3,4 &#xff0c;进栈过程中可以出栈&#xff0c;则下列不可能的…

软考A计划-电子商务设计师-信息安全知识

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

安全防御——IDS(入侵检测系统)

安全防御——IDS&#xff08;入侵检测系统&#xff09; IDS介绍为什么需要IDSIDS的工作原理IDS的工作过程第一步&#xff1a;信息收集第二步&#xff1a;数据分析 IDS的主要检测方法1、模式匹配&#xff08;误用检测&#xff09;2、统计分析&#xff08;异常检测&#xff09;3、…

chatgpt赋能python:Python创建venv的完全指南

Python创建venv的完全指南 在Python开发中&#xff0c;虚拟环境是一个非常有用的工具。它可以让我们在同一台计算机上拥有多个Python环境&#xff0c;而不会互相干扰。在本文中&#xff0c;我们将介绍如何使用Python创建venv&#xff08;虚拟环境&#xff09;。 什么是venv&a…