进程(Process),线程(Thread),协程(Coroutine,也叫轻量级线程)
进程
进程是一个程序在一个数据集中的一次动态执行过程,进程一般由程序,数据集,进程控制块三部分组成
线程
线程也叫轻量级进程,他是一个基本的CPU执行单元,也就是程序执行过程中的最小单元,由线程ID,程序计数器,寄存器集合和堆栈共同组成的,一个进程可以包含多个线程
协程
协程是一种用户态的轻量级线程,又称微线程,协程的调度完全由用户控制
一、主Goroutine
封装main函数的Goroutine被称为主Goroutine
主Goroutine所做的事情并不是执行main函数那么简单,它首先要做的是设定每一个goroutine所能申请的栈空间的最大尺寸,在32位计算机系统中此最大尺寸为250MB,而在64位计算机系统中此尺寸为1GB,如果有某个Goroutine的栈空间尺寸大于这个限制,那么运行时系统就会引发一个栈溢出(stack overflow)的运行时恐慌,随后这个go程序的运行也会终止
此后主Goroutine会进行一系列的初始化工作:
1、创建一个特殊的defer语句,用于在主Goroutine退出时做必要的善后处理,因为主Goroutine也可能非正常结束
2、启动专用于在后台清扫内存垃圾的Goroutine,并设置GC可用的表示
3、执行main包中所引用包的init函数
4、执行main函数
二、Goroutine
GO中使用Goroutine来实现并发
Goroutine是与其他函数或方法同时运行的函数或方法,与线程相比创建goroutine的成本很小,他就是一段代码,一个函数入口,以及在堆上为其分配一个堆栈(初始大小为4k,会随着程序的执行自动增长删除)。
在GO语言中使用goroutine,在调用函数或者方法前面加上go关键字即可
Go
package main
import "fmt"
func main() {
// 使用go关键字使用goroutine调用hello函数
go hello()
for i := 0; i < 150000; i++ {
//fmt.Println("main-", i)
}
}
func hello() {
for i := 0; i < 10; i++ {
fmt.Println("hello-----------", i)
}
}
/*
此处代码可设置main协程for循环次数的大小观测go协程调用hello情况
*/
-
当新的Goroutine开始时,Goroutine调用立即返回,与函数不同,go不等待Goroutine执行结束
-
当Goroutine调用,并且Goroutine的任何返回值被忽略之后,go立即执行到下一行代码
-
mian的Goroutine应该为其他的Goroutine执行,如果main的Goroutine终止了,程序将被终止,而其他的Goroutine将不会运行
三、runtime
获取系统信息
schedule调度让出时间片,让别的goroutine先执行
Goexit //终止当前的goroutine
Go 语言的 runtime
包提供了与 Go 运行时环境交互的各种功能。这个包允许你控制和检查程序的运行时行为,包括但不限于:
-
垃圾回收(Garbage Collection):可以手动触发垃圾回收,或者调整垃圾回收的策略。
-
并发控制 :提供了包括
Gosched()
在内的方法来控制 goroutine 的调度。 -
程序退出:可以正常或非正常地退出程序。
-
堆栈管理:可以获取当前 goroutine 的堆栈信息。
-
环境变量:读取和设置环境变量。
-
系统信号:处理操作系统信号。
-
CPU 信息:获取 CPU 的数量和相关信息。
-
内存分配:可以手动分配和释放内存。
-
性能监控:可以监控程序的 CPU 使用情况。
以下是一些 runtime
包中常用函数的简要说明:
runtime.GOMAXPROCS
:设置最大可运行的操作系统线程数。runtime.NumCPU
:返回机器的 CPU 核心数。runtime.NumGoroutine
:返回当前运行的 goroutine 数量。runtime.Gosched
:让出 CPU 时间片,使得其他 goroutine 可以运行。runtime.Goexit
:退出当前的 goroutine。runtime.KeepAlive
:确保某个 goroutine 不会被垃圾回收。runtime.SetFinalizer
:为对象设置终结器,当垃圾回收器准备回收该对象时,会调用该终结器。runtime.GC
:强制运行垃圾回收器。
Go
package main
import (
"fmt"
"runtime"
)
func main() {
//获取系统信息
fmt.Println("获取GOROOT目录", runtime.GOROOT())
fmt.Println("获取操作系统", runtime.GOOS)
fmt.Println("获取CPU", runtime.NumCPU())
//Goroutine 调度
go func() {
for i := 0; i < 100; i++ {
fmt.Println("Goroutine---", i)
}
}()
for i := 0; i < 100; i++ {
//让出时间片,让别的Goroutine先执行,不一定可以让成功
runtime.Gosched()
fmt.Println("main---", i)
}
}
runtime.Gosched()
是 Go 语言运行时库中的一个函数,它用于让出 CPU 时间片,让其他 goroutine(轻量级线程)有机会执行。这通常用于避免阻塞或减少阻塞的持续时间,尤其是在长时间运行的 goroutine 中,你可能会在适当的地方调用 Gosched
来让出 CPU,以避免长时间占用 CPU 导致其他 goroutine 饥饿。
以下是 runtime.Gosched()
函数的一些使用场景:
-
避免饥饿 :在长时间运行的循环中,如果确定当前 goroutine 可能不会被阻塞,可以调用
Gosched
来让出 CPU。 -
控制执行顺序 :在某些情况下,你可能希望控制 goroutine 的执行顺序,通过
Gosched
可以给其他 goroutine 运行的机会。 -
减少 CPU 使用 :在某些 I/O 密集型操作中,如果当前 goroutine 主要是等待 I/O 操作完成,调用
Gosched
可以让出 CPU,减少不必要的 CPU 使用。 -
避免死锁 :在某些复杂的 goroutine 调度中,如果担心死锁问题,可以在适当的地方调用
Gosched
来减少死锁的风险。
Go
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
/*
因为goroutine2延时了一定时间,如果goroutine1不让出CPU时间片那么必先执行完成
*/
go func() {
for i := 0; i < 5; i++ {
runtime.Gosched() // 让出 CPU 时间片
fmt.Println("Goroutine 1:", i)
}
}()
go func() {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println("Goroutine 2:", i)
}
}()
time.Sleep(3 * time.Second) // 等待两个 goroutine 执行完毕
}
请注意,过度使用 Gosched
可能会导致性能下降,因为频繁的调度会消耗额外的 CPU 资源。因此,应该在仔细考虑后,根据实际需要来使用 Gosched
。
查看协程数&CUP数
Go
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
var wg sync.WaitGroup //创建并发组
fmt.Printf("当前运行的goroutine数量: %d\n", runtime.NumGoroutine())
//创建10个协程
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("第 %d 个协程在 running\n", id)
}(i)
}
fmt.Printf("当前运行的goroutine数量: %d\n", runtime.NumGoroutine())
wg.Wait()
fmt.Printf("CPU核心数: %d\n", runtime.NumCPU())
}
四、互斥锁
在并发编程中会遇到的临界资源安全问题,可以采用互斥锁的方式来解决,后面也可通过通过通道channel来解决
临界资源:指并发环境中多个进程、线程、协程共享的资源
使用sync包下的锁解决临界资源安全问题(Mutex)
Go
package main
import (
"fmt"
"sync"
"time"
)
// 定义全局变量 票库存为10张
var tickets int = 10
// 创建锁
var mutexs sync.Mutex
func main() {
//三个窗口同时售票
go saleTicket("售票口1")
go saleTicket("售票口2")
go saleTicket("售票口3")
time.Sleep(time.Second * 15) //等待售票完
}
// 售票函数
func saleTicket(name string) {
for {
// 在检查之前上锁
mutexs.Lock()
if tickets > 0 {
time.Sleep(time.Second)
fmt.Printf("%s剩余的票数为:%d\n", name, tickets)
tickets--
} else {
fmt.Println("票已售完")
break
}
//操作结束后解锁
mutexs.Unlock()
}
}
sync包下的同步等待组(WaitGroup)
Go
package main
import (
"fmt"
"sync"
"time"
)
var w sync.WaitGroup
func main() {
// 公司最后关门的人 0
// wg.Add(2) 判断还有几个线程、计数 num=2
// wg.Done() 我告知我已经结束了 -1
w.Add(2)
go test11()
go test22()
fmt.Println("main等待ing")
w.Wait() // 等待 wg 归零,才会继续向下执行
fmt.Println("end")
// 理想状态:所有协程执行完毕之后,自动停止。
//time.Sleep(3 * time.Second)
}
func test11() {
for i := 0; i < 5; i++ {
time.Sleep(1 * time.Second)
fmt.Println("test1--", i)
}
w.Done()
}
func test22() {
defer w.Done()
for i := 0; i < 5; i++ {
fmt.Println("test2--", i)
}
}
五、Channel通道
不要以共享内存的方式通信,而要以通信的方式共享内存
通道可以被认为是Goroutines通信的管道,类似于管道中的水从一端到另一端的流动,数据可以从一端发送到另一端,通过通道接收,GO语言中建议使用Channel通道来实现Goroutines之间的通信
GO从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信
每个通道都有与其相关的类型,类型是通道允许传输的数据类型(通道的零值为nil,nil通道没有任何用处,因此通道必须使用类似于map和切片的方法定义)
一个通道发送和接收数据默认是阻塞的,当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从通道中读取数据
关闭通道
发送者可以通过关闭通道来通知接收方不会有更多的数据被发送到通道
Go
close(ch)
接收者可以在接收来自通道的数据时使用额外的变量来检查通道是否已关闭
Go
v,ok := <- ch
当ok的值为true,表示成功的从通道中读取了一个数据value,通道关闭时仍然可以读(存)数据
当ok的值为false,表示从一个封闭的通道读取数据,从闭通道读取的数据将是通道类型的零值
缓冲通道
缓冲通道是指一个通道,带有一个缓冲区,发送到一个缓冲通道只有在缓冲区满时才被阻塞,类似的,从缓冲通道接收的信息只有在为空时才会被阻塞,可以通过将额外的容量参数传递给make函数来创建缓冲通道,该函数指定缓冲区的大小
Go
package main
import (
"fmt"
"strconv"
"time"
)
func main() {
//定义通道可以写10个数据
ch := make(chan string, 10)
go test3(ch)
for v := range ch {
fmt.Println(v)
}
}
func test3(ch chan string) {
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
fmt.Println("通道内写入数据", "tset--"+strconv.Itoa(i))
ch <- "tset--" + strconv.Itoa(i)
}
close(ch)//如果不关闭协程,主协程的for循环一值阻塞,知道报错"fatal error: all goroutines are asleep - deadlock!"
}
定向通道
单向通道也就是定向通道,这些通道只能发送数据或者接收数据
Go
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go writerOnly(ch)
go readOnly(ch)
time.Sleep(time.Second * 2)
}
// 只读,指只允许管道读入/写出数据
func writerOnly(ch chan<- int) {
ch <- 10
}
// 只写,指指只允许管道读出/写入数据
func readOnly(ch <-chan int) {
temp := <-ch
fmt.Println(temp)
}
Select
-
每个case都必须是一个通道的操作
-
如果任意某个通信可以进行,他就执行,其他被忽略
-
如果有多个case都可以运行,Select会随机公平的选出一个执行
否则
-
如果有default子句,则执行该语句
-
如果没有default字句,select将阻塞,直到某个通信可以运行,GO不会重新对channel或值进行求值
Go
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
time.Sleep(time.Second * 2)
ch1 <- 100
}()
go func() {
time.Sleep(time.Second * 2)
ch2 <- 200
}()
select {
case num1 := <-ch1:
fmt.Println("ch1--", num1)
case num2 := <-ch2:
fmt.Println("ch2--", num2)
}
//没有default,则等待通道(阻塞),因为select{}本身是阻塞的
}
利用通道解决临界资源安全问题
Go
package main
import (
"fmt"
"sync"
"time"
)
// 定义全局chan,存储票总数
var totalTickets chan int
var wg sync.WaitGroup
func main() {
// 初始化票数量:总票数10张
totalTickets = make(chan int, 2)
totalTickets <- 10
wg.Add(3)
go sell("售票口1")
go sell("售票口2")
go sell("售票口3")
wg.Wait()
fmt.Println("买完了,下班")
}
func sell(name string) {
defer wg.Done()
for { //for循环表示一直在卖,一直在营业
residue, ok := <-totalTickets
if !ok {
fmt.Printf("%s: 关闭\n", name)
break
}
if residue > 0 {
time.Sleep(time.Second * 1)
totalTickets <- residue - 1 //
fmt.Println(name, "售出1张票,余票:", residue)
} else {
//进入此处时票已经买完了,因为for循环一进来售票窗口就检查是否还有票,假如最后卖完的是售票口3,那么销售窗口1跟2就会判断还有没有
fmt.Printf("%s: 关闭\n", name)
close(totalTickets)
break
}
}
}