多线程
Go 协程和 Go 主线程
Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个 Go 线程上,可以起多个协程,你可以 这样理解,协程是轻量级的线程[编译器做优化]。
Go 协程的特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
快速入门
程序需求
- 在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出 "hello,world"
- 在主线程中也每隔一秒输出"hello,golang", 输出 10 次后,退出程序
- 要求主线程和 goroutine 同时执行.
程序实现
package main
import (
"fmt"
"strconv"
"time"
)
func goRoutineTest() {
for i := 1; i<=10; i++ {
fmt.Println("hello world " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go goRoutineTest() // 开启一个协程
for i :=1; i <= 10; i++ {
fmt.Println("hello, golang" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
快速入门小结
- 主线程是一个物理线程,直接作用在 cpu 上的。是重量级的,非常耗费 cpu 资源。
- 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
- Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一 般基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优势了
goroutine 的调度模型
待学
设置 Golang 运行的 cpu 数
介绍:为了充分了利用多 cpu 的优势,在 Golang 程序中,设置运行的 cpu 数目
num := runtime.NumCPU() //获取cpu的数量
runtime.GOMAXPROCS(num)
fmt.Println("cpu个数", num)
go1.8后,默认程序运行在多个核上,可以不用设置
go1.8前,需要设置一下,可以更高效的cpu利用
Channel(管道)
通过一个Demo理解:
计算1~200各个数的阶乘,使用goroutine实现
package main
import (
"fmt"
"time"
)
// 定义一个map
var myMap = make(map[int]int, 10)
// 定义一个函数计算n!
func factorial(num int) {
res :=1
for i := 1; i <= num; i++{
res *= i
}
// 将计算出的结果存入map中
myMap[num] = res
}
func main() {
// 开启 200 个协程完成这个任务
for i := 1; i <= 200; i++ {
go factorial(i)
}
// 休眠 10S
time.Sleep(time.Second * 10)
// 输出map中的结果
for index, value := range myMap{
fmt.Println(index,"的阶乘为",value)
}
}
运行结果:
fatal error: concurrent map write,原因在于map需要互斥访问
如何解决这个问题?本质上是线程直接通信的问题
-
使用全局变量的互斥锁
操作全局变量前,对其进行上锁,操作完成后,释放锁
-
使用管道channel来解决
使用全局变量枷锁同步改进程序
package main
import (
"fmt"
"sync"
"time"
)
// 定义一个map
var (
myMap = make(map[int]int,10)
// 声明一个全局变量互斥锁
// lock 是一个全局互斥锁
// sync 是包:sychorinized
// Mutex : 互斥
lock sync.Mutex
)
// 定义一个函数计算n!
func factorial(num int) {
res :=1
for i := 1; i <= num; i++{
res += i
}
// 将计算出的结果存入map中
// 访问 myMap 前,加锁
lock.Lock()
myMap[num] = res
// 访问完,释放所
lock.Unlock()
}
func main() {
// 开启 200 个协程完成这个任务
for i := 1; i <= 200; i++ {
go factorial(i)
}
// 休眠 10S
time.Sleep(time.Second * 10)
// 输出map中的结果
lock.Lock()
for index, value := range myMap{
fmt.Printf("map[%d]=[%d]\n", index, value)
//fmt.Println(index,"的阶乘为",value)
}
lock.Unlock()
}
引入channel的概念
为什么需要channel?
- 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
- 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
- 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作 状态,这时也会随主线程的退出而销毁
- 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
- 上面种种分析都在呼唤一个新的通讯机制-channel
什么是channel?
- channle 本质就是一个数据结构-队列
- 数据是先进先出【FIFO : first in first out】
- 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
- channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。
channel的使用
channel的定义
var 变量名 chan 数据类型
var intChan chan int // intChan用于存放int类型的数据
var mapChan chan map[int]string // mapChan用以存放map类型的数据
var perChan chan Person
var perChan2 chan *Person
说明
- channel 是引用类型
- channel 必须初始化才能写入数据, 即 make 后才能使用
- 管道是有类型的,intChan 只能写入 整数 int
管道使用Demo
package main
import "fmt"
func main() {
// 声明一个管道
var intChan chan int
// 为chan分配内存,可以存放三个int类型的管道
intChan = make(chan int, 3)
fmt.Printf("intChan的值是%v intChan本身的地址是 %p \n", intChan, &intChan)
// 向管道写入数据
intChan <- 10
num := 211
intChan <- num
intChan <- 50
// 查看管道长度和容量
fmt.Printf("管道的长度是%v,容量是%v \n",len(intChan) ,cap(intChan))
// 从管道读取数据
var num2 int
num2 =<-intChan
fmt.Println("num2的值是%d",num2)
// 发现管道的长度减 1
fmt.Printf("管道的长度是%v,容量是%v ",len(intChan) ,cap(intChan))
var num3 =<-intChan
var num4 =<-intChan
fmt.Println(num3, num4)
fmt.Printf("管道的长度是%v,容量是%v ",len(intChan) ,cap(intChan))
}
channel使用注意事项
- channel 中只能存放指定的数据类型
- channle 的数据放满后,就不能再放入了
- 如果从 channel 取出数据后,可以继续放入
- 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock
channel关闭
使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然 可以从该 channel 读取数据
package main
import "fmt"
func main() {
strChan := make(chan string, 3)
strChan <- "huang"
strChan <- "rong"
close(strChan)
// strChan <- "wei"
fmt.Println("okok")
str := <- strChan
fmt.Println(str)
}
channel的遍历
channel 支持 for--range 的方式进行遍历,请注意两个细节
- 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
- 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
package main
import (
"fmt"
)
func main() {
strChan := make(chan string, 3)
strChan <- "huang"
strChan <- "rong"
strChan <- "wei"
// 不可以使用普通的for 循环遍历channel
close(strChan)
for value := range strChan{
fmt.Println(value)
}
}
使用channel和goroutine实现多线程
- 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
- 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作 状态,这时也会随主线程的退出而销毁
使用两个channel可以解决如上问题
package main
import (
"fmt"
)
// Write Data
func write(intChan chan int) {
for i := 1; i <= 50; i++ {
// 放入数据
intChan <- i
fmt.Println("Write Data", i)
}
close(intChan)
}
// Read data
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok :=<- intChan
if !ok{
break
}
//time.Sleep(time.Second)
fmt.Println("读到数据",v)
}
// 读取完数据后,即任务完成
// exitChan 是一个标志,当读完数据后,向exit中存放一个true,主线程除非取出这个true,否则不停止
exitChan <- true
close(exitChan)
}
func main() {
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)
go write(intChan)
go readData(intChan,exitChan)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
channel 使用细节和注意事项
-
channel 可以声明为只读,或者只写性质
var chan1 chan int //默认情况下,管道是双向的,可读可写
var chan1 chan <- int //管道只可写
var chan3 chan -> int // 管道只可以读
-
使用 select 可以解决从管道取数据的阻塞问题
func main() { //使用 select 可以解决从管道取数据的阻塞问题 //1.定义一个管道 10 个数据 int intChan := make(chan int, 10) for i := 0; i < 10; i++ { intChan<- i } //2.定义一个管道 5 个数据 string stringChan := make(chan string, 5) for i := 0; i < 5; i++ { stringChan <- "hello" + fmt.Sprintf("%d", i) } //传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock //问题,在实际开发中,可能我们不好确定什么关闭该管道. //可以使用 select 方式可以解决 //label: for { select { //注意: 这里,如果 intChan 一直没有关闭,不会一直阻塞而 deadlock //,会自动到下一个 case 匹配 case v := <-intChan : fmt.Printf("从 intChan 读取的数据%d\n", v) time.Sleep(time.Second) case v := <-stringChan : fmt.Printf("从 stringChan 读取的数据%s\n", v) time.Sleep(time.Second) default : fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n") time.Sleep(time.Second) return //break label } }
-
goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题
如果我们起了一个协程,但是这个协程出现了panic,如果没有捕获这个panic,就会造成整个程序的崩溃,这时我们可以在goruotine中使用recover捕获panic,进行处理,这样即使协程出现问题也不会影响主线程的执行
//函数 func sayHello() { for i := 0; i < 10; i++ { time.Sleep(time.Second) fmt.Println("hello,world") } } //函数 func test() { //这里我们可以使用 defer + recover defer func() { //捕获 test 抛出的 panic if err := recover(); err != nil { fmt.Println("test() 发生错误", err) } }() //定义了一个 map var myMap map[int]string myMap[0] = "golang" //error } func main() { go sayHello() go test() for i := 0; i < 10; i++ { fmt.Println("main() ok=", i) time.Sleep(time.Second) } }