golang之goroutine和channel
多线程程序在单核上运行,就是并发
多线程程序在多核上运行,就是并行
Go协程和Go主线程
Go主线程(线程):一个Go线程上,可以起多个协程 ,你可以这样理解,协程是轻量级的线程
Go协程的特点:
1)有独立的栈空间
2)共享程序堆空间
3) 调度由用户控制
4)协程是轻量级的线程3
goroutine快速入门
func test() { for i := 1; i <= 10; i++ { fmt.Println("test() hello, world " + strconv.Itoa(i)) time.Sleep(time.Second) } } func main() { go test() for i := 1; i <= 10; i++ { fmt.Println("main() hello, world " + strconv.Itoa(i)) time.Sleep(time.Second) } }
小结:
1)主线程是一个物理线程,直接作用在cpu上的,是重量级的,非常耗费cpu资源;
2)协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小;
3)Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了。
MPG模式基本介绍
1)M:操作系统的主线程(是物理线程)
2)P:协程执行需要的上下文
3)G:协程
设置运行的cpu数目:
func main() { cupNum := runtime.NumCPU() fmt.Println(cupNum) // 设置运行的cpu数目 runtime.GOMAXPROCS(cupNum) }
channel
1)channel本质就是一个数据结构-队列
2)数据是先进先出
3)线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
4)channel有类型的,一个string的channel只能存放string数据
5)示意图:
定义声明channel
var 变量名 chan 数据类型
举例:
var intChan chan int
var mapChan chan map[int]string
var perChan chan Person
var perChan2 chan *Person
1)channel是引用类型
2)channel必须初始化才能写入数据,即make后才能使用
3)管道是有类型的,intChan只能写入整数int
管道的初始化,写入与读取数据及基本的注意事项
func main() { var intChan chan int intChan = make(chan int, 4) fmt.Printf("intChan 的值=%v inChan本身的地址=%p\n", intChan, &intChan) // 存数据 intChan <- 10 num := 23 intChan <- num intChan <- 30 intChan<- 22 fmt.Printf("channel len=%v cap=%v \n", len(intChan), cap(intChan)) // 读取数据 var num2 int num2 = <-intChan fmt.Println("num2=",num2) fmt.Printf("channel len=%v cap=%v \n", len(intChan), cap(intChan)) }
channel的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据。
channel的遍历
channel支持for-range的方式进行遍历,请注意两个细节
1)在遍历时,如果channel没有关闭,则会出现deadlock的错误
2)在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
goroutine和channel结合使用案例
func writeData(intChan chan int) { for i := 0; i < 50; i++ { intChan<- i fmt.Println("writeData ", i) } close(intChan) } func readData(intChan chan int, exitChan chan bool) { for { v, ok := <-intChan if !ok { break } fmt.Println("readData ", v) } exitChan<- true close(exitChan) } func main() { intChan := make(chan int, 50) exitChan := make(chan bool, 1) go writeData(intChan) go readData(intChan, exitChan) for { _, ok := <-exitChan if !ok { break } } }
channel阻塞
如果只是向管道写入数据,而没有读取,就会出现阻塞而dead lock,原因是容量不足
如果,编译器运行,发现一个管道只有写,而没有读,则该管道会阻塞
channel注意事项
1)channel可以声明为只读(var chan2 chan<- int),或者只写(var chan2 <-chan int)性质
2)使用select可以解决从管道取数据的阻塞问题
3)goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题
如果我们起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic,就会造成整个程序崩溃,这时我们可以在goroutine中使用recover来捕获panic进行处理,这样即使这个协程发生的问题,但是主线程仍然不受影响,可以继续执行。
func sayHello() { for i := 0; i < 10; i++ { time.Sleep(time.Second) fmt.Println("hello world!") } } func test() { defer func() { if err := recover(); err != nil { fmt.Println("test() 发生了错误!") } }() var myMap map[int]string myMap[1] = "golang" } func main() { go sayHello() go test() for i := 0 ;i < 10; i++ { fmt.Println("main() ok=", i) time.Sleep(time.Second) } }