1. 前言
1.1 进程和线程的介绍
-
进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单元
-
线程是进程的子任务
-
一个线程只能属于一个进程,而一个进程至少有一个或多个线程,线程依赖于进程而存在
-
进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存
1.2 并发和并行
- 多线程程序在单核cpu上运行,就是并发
- 多线程程序在多核cpu上运行,就是并行
2. Go 协程和 Go 主线程
- Go主线程,可以把它称为线程、也可以理解为进程。在一个 Go 线程上可以有多个协程,可以理解成协程是轻量级的线程,由 Go 编译器做了优化
- Go 协程有独立的栈空间、共享程序堆内存*
2.1 goroutine 案例
- 利用Go语言实现同时输出
hello world和hello golang两个字符串
package main
import (
"fmt"
"time"
"strconv"
)
func main() {
// 开启一个协程
go syncPrint()
for i := 1; i <= 10; i++ {
fmt.Println("main第" + strconv.Itoa(i) + "次打印:hello golang")
time.Sleep(time.Second)
}
}
func syncPrint() {
for i := 1; i <= 10; i++ {
fmt.Println("syncPrint第" + strconv.Itoa(i) + "次打印:hello world")
time.Sleep(time.Second)
}
}
- 主线程是一个物理线程,直接作用在 cpu 上的,是非常消耗 cpu 资源的;协程是从主线程中开启的,是轻量级线程,消耗资源相对较少
3. goroutine 之间通信
- 多个协程对全局变量
i进行累加,然后将累加的结果放到map中
package main
import (
"fmt"
"time"
)
var (
i = 1
resMap = make(map[int]int, 10)
)
func add() {
i++
resMap[i] = i
fmt.Println("add",resMap)
}
func main() {
for i:= 1; i < 20; i++ {
go add()
}
time.Sleep(time.Second * 5)
}
运行后报错:fatal error: concurrent map iteration and map write,意思是 并发的迭代且向 map 中写入数据
原因:多个协程来操作同一块内存数据(比如案例中 map),引发了资源竞争,导致了安全问题
3.1 如何保证不同协程之间数据安全
- 3.1.1 全局变量互斥锁
需要使用到
sync包下的相关函数来完成全局变量的互斥锁功能,如下所示:
type Mutex struct {
}
func (m *Mutex) Lock()
func (m *Mutex) Unlock()
- 利用全局变量互斥锁的方式来解决刚才并发遍历和写 map 问题
package main
import (
"fmt"
"time"
"sync"
)
var (
i = 1
resMap = make(map[int]int, 10)
// 声明一个全局互斥锁
lock sync.Mutex
)
func add() {
i++
lock.Lock() // 上锁
resMap[i] = i // fatal error: concurrent map iteration and map write
fmt.Println("add",resMap)
lock.Unlock() // 释放锁
}
func main() {
for i:= 1; i < 100; i++ {
go add()
}
time.Sleep(time.Second * 5)
}
使用互斥锁可以解决同步安全问题,但是 Go 官方建议高水平的同步方式应该是用 channel
- 使用管道 channel 来解决
4. 什么是 channel
channel 的本质是一个队列,遵循队列的先进先出原则,多个协程 goroutine 访问时,不需要加锁,它本身就是线程安全的。
4.1 声明 channel
channel 是引用类型,必须初始化分配内存空间才能使用,并且channel是有类型区分的,比如 string 类型的 channel 只能写入字符串
- 声明举例
var 变量名称 chan 数据类型
例子:
var intChannel chan int
intChannel = make(chan int, 3)
var mapChannel chan map[int]string
4.2 channel 读写示例
- 读写
channel中的元素使用<-标识符 channel中数据放满后,就不能再加入数据了- 在没有使用
goroutine的前提下,如果channel数据取完了,再取的话就会报错
4.2.1 读写 int 类型
package main
import (
"fmt"
)
func main() {
var intChannel chan int
intChannel = make(chan int, 3)
// 向管道中写入数据
intChannel <- 10
intChannel <- 20
intChannel <- 10
// 读取管道中的数据
res1 := <- intChannel
res2 := <- intChannel
res3 := <- intChannel
// res1=10, res1=20, res1=10
fmt.Printf("res1=%v, res1=%v, res1=%v \n", res1, res2, res3)
}
4.2.2 读写任意数据类型
package main
import (
"fmt"
)
type Dog struct {
name string
}
func main() {
var anyData chan interface{}
anyData = make(chan interface{}, 10)
dog := Dog{ name : "旺财", }
anyData <- dog
anyData <- 10
anyData <- "ABCD"
cityMap := map[string]string{
"a" : "北京",
"b" : "上海",
"c" : "西安",
}
anyData <- cityMap
resDog := <-anyData
intData := <-anyData
strData := <-anyData
mapData := <-anyData
// resDog={旺财}, intData=10, strData=ABCD, mapData=map[a:北京 b:上海 c:西安]
fmt.Printf("resDog=%v, intData=%v, strData=%v, mapData=%v \n", resDog, intData, strData, mapData)
}
注意: 读取管道中存储的任意类型数据,如果不确定数据类型时,最好做类型断言判断
dog := resDog.(Dog)
5. 管道的遍历和关闭
-
使用内置函数
close可以关闭channel,当channel关闭后,就不能再向channel中写入数据了,但是仍然可以读取channel中的数据 -
channel只支持for-range的方式遍历- 在遍历时,如果
channel没有关闭,则会出现 dead lock 的错误 - 在遍历时,如果
channel已经关闭,则会正常遍历数据,遍历完成后就会退出遍历
- 在遍历时,如果
-
内置函数
close只能作用在 双向 或 仅写 的管道上。
package main
import "fmt"
func main() {
intChan := make(chan int, 10)
for i:= 1; i < 10 ; i++ {
intChan <- i * 2
}
// 关闭管道后进行遍历
close(intChan)
// 这里与遍历数组有所不同
for v := range intChan {
fmt.Println("v=", v)
}
}
5.1 channel使用细节
- 补充说明
6. goroutine 和 channel 使用
6.1 开启一个协程向管道中写入数据,再开启另外一个协程读取管道中的数据,等两个协程完成后主线程退出
实现思路:
package main
import (
"fmt"
)
func main() {
// 数据管道
dataChan := make(chan int, 200)
// 标识管道
flagChan := make(chan bool, 1)
// 开启写入和读取数据协程
go writeData(dataChan)
go readData(dataChan, flagChan)
// 轮询标识管道
for {
v,ok := <-flagChan
fmt.Println("main poll ", v, ok)
if !ok {
break
}
}
}
func writeData(dataChan chan int) {
for i:= 1; i < 200; i++ {
dataChan <- i
fmt.Println("write data ", i)
}
close(dataChan)
}
func readData(dataChan chan int, flagChan chan bool) {
for {
v,ok := <-dataChan
if !ok {
break
}
fmt.Printf("read data , v=%v, ok=%v \n", v, ok)
}
// 读取完成,写入完成标识
flagChan <- true
close(flagChan)
}
- 如果注释掉
go readData(dataChan, flagChan)程序代码会怎样?
如果只向管道中写数据,没有读取数据,就会出现阻塞报错fatal error: all goroutines are asleep - deadlock!;因为管道初始化容量已经确定好了,当写入数据达到知道容量后就不能再写入了。
6.2 channel 使用细节
- 可以单独声明读、写 的
channel,默认情况下channel是双向的
var writeChan chan<- int // 仅支持写
var readChan <-chan int // 仅支持读
案例:利用协程分别开启一个只读、只写的管道,当这两个协程执行完成后再结束主线程
package main
import (
"fmt"
)
func main() {
dataChan := make(chan int, 10)
// 用于标识只读、只写管道执行完成
exitChan := make(chan bool, 2)
go onlyWrite(dataChan, exitChan)
go onlyRead(dataChan, exitChan)
var exitCount = 0
for _ = range exitChan {
exitCount ++
// 只读、只写管道已执行完成
if (exitCount == 2) {
break
}
}
fmt.Println("应用程序执行完成 ...")
}
func onlyWrite(dataChan chan<- int, exitChan chan bool) {
for i:=1; i<10; i++ {
dataChan <- i
}
close(dataChan)
// dataChan 为只写管道,如果读该管道会报错:receive from send-only channel
// for v := range dataChan {
// fmt.Println(v)
// }
exitChan <- true
}
// 只读管道不需要关闭
func onlyRead(dataChan <-chan int, exitChan chan bool) {
for {
v, ok := <-dataChan
if !ok {
break
}
fmt.Printf("only read data , v=%v, ok=%v \n", v, ok)
}
exitChan <- true
}
- 可使用
select解决从管道读取数据阻塞问题
比如对管道进行遍历,要对管道进行关闭操作,否则会阻塞而导致 deadlock;问题是实际开发中,我们可能并不能确定什么时候对管道进行关闭操作。此时可以用 select 解决该问题!
package main
import (
"fmt"
)
func main() {
intChan := make(chan int, 5)
for i:=0; i<5; i++ {
intChan <- i
}
for {
select {
case v := <-intChan:
fmt.Println("int chan, v= ",v )
default:
fmt.Println(" 取不到元素 ... ")
return
}
}
}
注意: select 必须和 case 一起使用
- 在
goroutine中使用recover,可以解决协程中出现异常,导致整个程序崩溃问题
在主程序中开启一个协程,如果协程出现了panic,而且我们没有捕获处理这个panic,就会造成整个程序崩溃。这时可以在协程中使用recover捕获异常,保证即使协程发生了问题,但主线程仍然可以继续执行
package main
import (
"fmt"
"time"
)
func main() {
go sayHello()
go testError()
time.Sleep(time.Second)
for i:=0; i<5; i++ {
fmt.Println("main i=",i)
}
}
func sayHello() {
for i:=0; i<5; i++ {
fmt.Println("sayHello , i=", i)
}
}
func testError(){
defer func() {
if err := recover(); err != nil {
fmt.Println("testError 发生异常", err)
}
}()
var dataMap map[int]string
dataMap[0] = "golang" // error
}