赞
踩
基本介绍
进程、线程与协程:
并发与并行:
在Go中,通过在函数或方法的调用前加上go关键字即可创建一个go协程,并让其运行对应的函数或方法。如下:
package main import ( "fmt" "time" ) func Print() bool { for i := 0; i < 10; i++ { fmt.Printf("Print: hello goroutine...%d\n", i+1) time.Sleep(time.Second) } return true } func main() { go Print() // 创建go协程 for i := 0; i < 5; i++ { fmt.Printf("main: hello goroutine...%d\n", i+1) time.Sleep(time.Second) } }
在上述代码中,主协程创建了一个新协程用于执行Print函数,主协程进行5次打印后退出,新协程进行10次打印后退出。运行结果如下:
说明一下:
常规的协程(Coroutine)
线程是在内核态视角下的最小执行单元,而协程是在线程的基础上,在用户态视角下进行二次开发得到的更小的执行单元。常规的协程(Coroutine)通常是与一个线程强绑定的,而一个线程可以绑定多个协程。如下:
说明一下:
Go中的协程(Goroutine)
Go语言中的协程(Goroutine)与常规的协程(Coroutine)的实现方式有所不同,Go中的协程不是与一个线程强绑定的,而是由Go调度器动态的将协程绑定到可用的线程上执行。如下:
说明一下:
GMP模型
GMP(Goroutine-Machine-Processor)模型是Go运行时系统中用于实现并发执行的模型,负责管理和调度协程的执行。G、M和P的含义分别如下:
GMP模型示意图如下:
上图说明:
调度器P获取可调度的G的流程如下:
说明一下:
GOMAXPROCS
在GMP模型中,G只有被P调度才得以执行,因此P的数量决定了G的最大并行数量。通过runtime包中的GOMAXPROCS函数可以获取和设置P的数量。如下:
package main
import (
"fmt"
"runtime"
)
func main() {
cpuNum := runtime.NumCPU() // 获取本地机器的逻辑CPU数
fmt.Printf("cpuNum = %d\n", cpuNum) // cpuNum = 6
runtime.GOMAXPROCS(4) // 设置可同时执行的最大CPU数
num := runtime.GOMAXPROCS(0) // 获取可同时执行的最大CPU数
fmt.Printf("num = %d\n", num) // num = 4
}
说明一下:
协程的生命周期
Go中协程的生命周期大致由如下几种状态组成:
状态转换如下:
说明一下:
协程的调度流程
GMP模型中存在三种类型的协程:
在创建M时,Go运行时系统会为每个M初始化一个g0,g0的调度流程如下:
示意图如下:
调度类型
GMP模型中的调度类型大致可分为如下四类:
触发前三种调度类型中的任意一种,都会导致当前G的调度终止,此时M的执行权将由普通的g交还给g0。示意图如下:
上图说明:
关于被动调度:
关于抢占调度:
协程间共享变量
例如,下面程序中启动了4个协程进行抢票,在抢票过程中需要并发访问全局变量tickets,代码中通过加锁的方式保证了tickets变量的并发安全。如下:
package main import ( "fmt" "sync" "time" ) var ( tickets = 1000 // 共享资源 mtx sync.Mutex // 互斥锁 ) func ByTicket(id int) { for { mtx.Lock() // 加锁 if tickets <= 0 { mtx.Unlock() // 解锁 break } time.Sleep(time.Microsecond) // 模拟抢票过程的耗时 tickets-- fmt.Printf("goroutine %d get a ticket, tickets = %d\n", id, tickets) mtx.Unlock() // 解锁 } } func main() { // 启动4个协程进行抢票 for i := 0; i < 4; i++ { go ByTicket(i) } for { if tickets <= 0 { break } } fmt.Printf("tickets sold out...tickets = %d\n", tickets) }
说明一下:
基本介绍
channel的示意图如下:
channel的定义方式
在定义channel时,通过make创建指定类型以及容量的channel。如下:
package main
import (
"fmt"
"unsafe"
)
func main() {
// make channel
var intChan = make(chan int, 10)
fmt.Printf("intChan type = %T\n", intChan) // intChan type = chan int
fmt.Printf("intChan len = %d\n", len(intChan)) // intChan len = 0
fmt.Printf("intChan cap = %d\n", cap(intChan)) // intChan cap = 10
fmt.Printf("intChan size = %d\n", unsafe.Sizeof(intChan)) // intChan size = 8
}
说明一下:
channel的读写
channel的读写:
channel <- data
的方式向channel中写入数据,在写入数据时,如果channel已满,则写操作会被阻塞,直到channel中有数据被读走,再执行写操作。data := <-channel
的方式从channel中读取数据,在读取数据时,如果channel为空,则读操作会被阻塞,直到有数据写入channel中,再执行读操作。例如,下面程序中定义了一个容量为5的channel,并启动了一个协程不断向该channel中写入数据,而在主协程中每隔1秒从该channel中读取一次数据。如下:
package main import ( "fmt" "time" ) func WriteNum(intChan chan int) { num := 0 for { intChan <- num // 向channel中写入数据 fmt.Printf("write a num: %d\n", num) num++ } } func ReadNum(intChan chan int) { for { time.Sleep(time.Second) num := <-intChan // 从channel中读取数据 fmt.Printf("read a num: %d\n", num) } } func main() { intChan := make(chan int, 5) go WriteNum(intChan) ReadNum(intChan) }
在上述代码中,由于向channel中写入数据的过程中没有进行任何休眠操作,因此程序运行后channel立马被写满了,此时对channel的写操作将会被阻塞,直到channel中的数据被主协程读走,才能再次执行写操作,因此后续对channel的写操作也被同步为每秒一次。程序运行结果如下:
说明一下:
channel的关闭
在Go中,通过内建函数close可以关闭指定的channel,channel关闭后不能再对其进行写操作,否则会触发panic异常,但仍可以从该channel中读取数据。如下:
package main import "fmt" func main() { charChan := make(chan int, 10) for i := 0; i < 10; i++ { charChan <- 'a' + i } close(charChan) // 关闭channel for { ch, ok := <-charChan if !ok { break } fmt.Printf("read a char: %c\n", ch) } }
运行程序后可以看到,channel虽然被关闭了,但仍然可以读取channel中的数据。如下:
说明一下:
<-channel
的方式读取channel中的数据将会得到两个值,第一个值是从channel中读取到的数据,第二个值表示本次对channel进行的读操作是否成功,如果channel已关闭并且channel中没有数据可读,那么第二个值将会返回false,否则为true。channel的遍历方式
在Go中,可以通过for range循环的方式对channel中的元素进行遍历,其特点如下:
使用案例如下:
package main
import "fmt"
func main() {
charChan := make(chan int, 10)
for i := 0; i < 10; i++ {
charChan <- 'a' + i
}
close(charChan) // 关闭channel
for value := range charChan {
fmt.Printf("read a char: %c\n", value)
}
}
说明一下:
只读/只写channel
在Go中,通过<-chan type
和chan<- type
的方式,可以将channel声明为只读或只写。如下:
package main import ( "fmt" "time" ) func WriteNum(intChan chan<- int) { // 只写channel num := 0 for { intChan <- num // 向channel中写入数据 fmt.Printf("write a num: %d\n", num) num++ } } func ReadNum(intChan <-chan int) { // 只读channel for { time.Sleep(time.Second) num := <-intChan // 从channel中读取数据 fmt.Printf("read a num: %d\n", num) } } func main() { intChan := make(chan int, 5) go WriteNum(intChan) ReadNum(intChan) }
说明一下:
题目要求:统计1-300000中有多少个素数
为了快速统计出素数的个数,使用多个Go协程并发进行素数判断,具体的解决思路如下:
为了让主协程能够判断primeChan中的素数是否已经读取完毕,需要借助一个exitChan:
示意图如下:
代码如下:
package main import "fmt" func Producer(numChan chan<- int) { for num := 1; num <= 300000; num++ { numChan <- num } close(numChan) } func IsPrime(num int) bool { for i := 2; i <= num-1; i++ { if num%i == 0 { return false } } return true } func Consumer(numChan <-chan int, primeChan chan<- int, exitChan chan<- bool) { for { num, ok := <-numChan if !ok { break } if IsPrime(num) { primeChan <- num } } exitChan <- true } func main() { numChan := make(chan int, 300000) primeChan := make(chan int, 300000) exitChan := make(chan bool, 6) // 生产者协程 go Producer(numChan) // 消费者协程 for i := 0; i < 6; i++ { go Consumer(numChan, primeChan, exitChan) } // 匿名协程 go func() { for i := 0; i < 6; i++ { <-exitChan } close(primeChan) close(exitChan) }() // 主协程 count := 0 for { _, ok := <-primeChan if !ok { break } count++ } fmt.Printf("prime count = %d\n", count) // prime count = 25998 }
select语句
在Go中,select语句用于实现非阻塞的通信。其特点如下:
使用案例如下:
package main import "fmt" func main() { intChan := make(chan int, 10) stringChan := make(chan string, 10) for i := 0; i < 10; i++ { intChan <- i stringChan <- fmt.Sprintf("hello select%d", i) } label: for { select { case num := <-intChan: fmt.Printf("read intChan: %d\n", num) case str := <-stringChan: fmt.Printf("read stringChan: %s\n", str) default: fmt.Printf("no data now...\n") break label } } }
运行代码后可以看到,当intChan和stringChan中都有数据时,select语句会随机对一个channel进行读操作,并在两个channel中的数据都被读取完后,通过执行default分支中的break语句跳出for循环。运行结果如下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。