goroutine和channel

一、goroutine

1、并发和并行:

多线程程序在单核上运行就是并发。

多线程程序在多核上运行就是并行。

2、Go协程和Go主线程

Go主线程(有人直接称为线程/也可以理解成进程):一个Go线程上,可以起多个协程,协程是轻量级的线程[编译器做优化]。

Go协程的特点:有独立的栈空间;共享程序堆空间;调度由用户控制;协程是轻量级的线程。

请编写一个程序,完成如下功能:
在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1秒输出 "hello,world"
在主线程中也每隔一秒输出"hello,golang", 输出10次后,退出程序
要求主线程和goroutine同时执行.
画出主线程和协程执行流程图

package main

import (
	"fmt"
	"strconv"
	"time"
)

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,golang " + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

 

主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源。
协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了。

3、goroutine的调度模型MPG

M指的是Machine,一个M直接关联了一个内核线程。由操作系统管理。 P指的是”processor”,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。它负责衔接M和G的调度上下文,将等待执行的G与M对接。 G指的是Goroutine,其实本质上也是一种轻量级的线程。包括了调用栈,重要的调度信息,例如channel等。
P的数量由环境变量中的GOMAXPROCS决定,通常来说它是和核心数对应,例如在4Core的服务器上回启动4个线程。G会有很多个,每个P会将Goroutine从一个就绪的队列中做Pop操作,为了减小锁的竞争,通常情况下每个P会负责一个队列。
三者关系如下图所示: 

以上这个图讲的是两个线程(内核线程)的情况。一个M会对应一个内核线程,一个M也会连接一个上下文P,一个上下文P相当于一个“处理器”,一个上下文连接一个或者多个Goroutine。为了运行goroutine,线程必须保存上下文。
上下文P(Processor)的数量在启动时设置为GOMAXPROCS环境变量的值或通过运行时函数GOMAXPROCS()。通常情况下,在程序执行期间不会更改。上下文数量固定意味着只有固定数量的线程在任何时候运行Go代码。可以使用它来调整Go进程到个人计算机的调用,例如4核PC在4个线程上运行Go代码。
图中P正在执行的Goroutine为蓝色的;处于待执行状态的Goroutine为灰色的,灰色的Goroutine形成了一个队列runqueues。
Go语言里,启动一个goroutine很容易:go function就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,一旦上下文运行goroutine直到调度点,它会从其runqueue中弹出goroutine,设置堆栈和指令指针并开始运行goroutine。

能否抛弃P(Processor),让Goroutine的runqueues挂到M上呢?答案是不行,需要上下文的目的是:当遇到内核线程阻塞的时候可以直接放开其他线程。

一个很简单的例子就是系统调用sysall,一个线程肯定不能同时执行代码和系统调用被阻塞,这个时候,此线程M需要放弃当前的上下文环境P,以便可以让其他的Goroutine被调度执行。

如上图左图所示,M0中的G0执行了syscall,然后就创建了一个M1(也有可能来自线程缓存),(转向右图)然后M0丢弃了P,等待syscall的返回值,M1接受了P,将继续执行Goroutine队列中的其他Goroutine。
当系统调用syscall结束后,M0会“偷”一个上下文,如果不成功,M0就把它的Gouroutine G0放到一个全局的runqueue中,将自己置于线程缓存中并进入休眠状态。全局runqueue是各个P在运行完自己的本地的Goroutine runqueue后用来拉取新goroutine的地方。P也会周期性的检查这个全局runqueue上的goroutine,否则,全局runqueue上的goroutines可能得不到执行而饿死。
均衡的分配工作:按照以上的说法,上下文P会定期的检查全局的goroutine队列中的goroutine,以便自己在消费掉自身Goroutine队列的时候有事可做。假如全局goroutine队列中的goroutine也没了呢?就从其他运行的中的P的runqueue里偷。
每个P中的Goroutine不同导致他们运行的效率和时间也不同,在一个有很多P和M的环境中,不能让一个P跑完自身的Goroutine就没事可做了,因为或许其他的P有很长的goroutine队列要跑,得需要均衡。 该如何解决呢?Go的做法倒也直接,从其他P中偷一半!

4、设置golang运行的cpu数

为了充分利用多cpu的优势,在golang程序中可以设置运行cpu数目。 go1.8后,默认让程序运行在多个核上,可以不用设置。go1.8之前,需要设置一下,可以更高效的利用cpu。

package main

import (
	"fmt"
	"runtime"
)

func main() {
	//获取当前系统cpu的数量
	num := runtime.NumCPU()

	//设置运行go程序的cpu数量
	runtime.GOMAXPROCS(num)
	fmt.Println("cpu number = ", num)
}

二、channel

计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用goroutine完成。 

package main

import (
	"fmt"
	"time"
)

var (
	myMap = make(map[int]int, 10)
)

func fac(n int) {
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}

	//将阶乘的计算结果放到map中
	myMap[n] = res
}

func main() {
	for i := 1; i <= 200; i++ {
		go fac(i)
	}

	time.Sleep(time.Second * 10)

	for i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}
}

上述代码因为没有对全局变量myMap加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map writes

不同goroutine之间如何通讯:(1)、全局变量加入互斥锁;(2)、使用管道channel来解决。

为了解决上述代码中存在的资源竞争问题,全局变量myMap加入互斥锁。

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	myMap = make(map[int]int, 10)

	//声明一个全局的互斥锁,
	lock sync.Mutex
)

func fac(n int) {
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}

	//将阶乘的计算结果放到map中
	//加锁
	lock.Lock()
	myMap[n] = res
	//释放锁
	lock.Unlock()
}

func main() {
	for i := 1; i <= 20; i++ {
		go fac(i)
	}

	time.Sleep(time.Second * 10)

	lock.Lock()
	for i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}
	lock.Unlock()
}

1、channel的基本介绍

channle本质就是一个数据结构-队列。
数据是先进先出【FIFO : first in first out】。
线程安全,多 goroutine 访问时,不需要加锁,就是说channel本身就是线程安全的。
channel有类型的,一个string的channel只能存放string类型数据。

2、声明channel

var 变量名 chan 数据类型

var intChan chan int (intChan 用于存放 int 数据)
var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型)
var perChan chan Person
var perChanPtr chan *Person

channel是引用类型
channel必须初始化才能写入数据, 即make后才能使用
管道是有类型的,intChan只能写入整数int

3、管道的初始化及读写数据

package main

import "fmt"

func main() {
	//创建一个可以存放3个int类型的管道
	var intChan chan int
	intChan = make(chan int, 3)

	fmt.Printf("intChan的值=%v intChan本身的地址=%p\n", intChan, &intChan)

	//向管道写入数据
	intChan <- 10
	num := 211
	intChan <- num
	intChan <- 50

	//向管道写入数据时不能超过其容量
	//intChan <- 80

	//查看管道的长度和容量
	fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))

	//从管道中读取数据
	var n int
	n = <-intChan
	fmt.Println("n=", n)
	fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))

	//在没有使用协程的情况下,如果管道的数据已经全部取出,再取就会报告deadlock
	num1 := <-intChan
	num2 := <-intChan
	//num3 := <-intChan
	fmt.Println("num1=", num1, "num2=", num2)
}

channel中只能存放指定的数据类型

channle的数据放满后,就不能再放入了
如果从channel取出数据后,可以继续放入
在没有使用协程的情况下,如果channel数据取完了,再取就会报dead lock

4、练习题

(1)、创建一个intChan,最多可以存放3个int,存3个数据到intChan中,然后再取出这三个int。

package main

import "fmt"

func main() {
	var intChan chan int
	intChan = make(chan int, 10)

	//intChan容量是3,再存放会报告deadlock
	intChan <- 10
	intChan <- 20
	intChan <- 30

	num1 := <-intChan
	num2 := <-intChan
	num3 := <-intChan
	//intChany已经没有数据了,再取数据会报告deadlock

	fmt.Printf("num1=%v num2=%v num3=%v", num1, num2, num3)
}

(2)、创建一个mapChan,最多可以存放10个map[string]string的key-value,对这个chan进行写入和读取。

package main

import "fmt"

func main() {
	var mapChan chan map[string]string
	mapChan = make(chan map[string]string, 10)

	m1 := make(map[string]string, 20)
	m1["city1"] = "北京"
	m1["city2"] = "天津"

	m2 := make(map[string]string, 20)
	m2["hero1"] = "宋江"
	m2["hero2"] = "武松"

	mapChan <- m1
	mapChan <- m2

	mo1 := <-mapChan
	mo2 := <-mapChan

	fmt.Printf("mo1=%v\nmo2=%v", mo1, mo2)
}

(3)、创建一个catChan,最多可以存放10个Cat结构体变量,对这个chan进行写入和读取。

package main

import "fmt"

type Cat struct {
	Name string
	Age  int
}

func main() {
	var catChan chan Cat
	catChan = make(chan Cat, 10)

	cat1 := Cat{Name: "tom", Age: 10,}
	cat2 := Cat{Name: "nancy", Age: 78,}

	catChan <- cat1
	catChan <- cat2

	c1 := <-catChan
	c2 := <-catChan

	fmt.Printf("c1=%v\nc2=%v", c1, c2)
}

(4)、创建一个catChanPtr,最多可以存放10个*Cat变量,对这个chan进行写入和读取。

package main

import "fmt"

type Cat struct {
	Name string
	Age  int
}

func main() {
	var catChan chan *Cat
	catChan = make(chan *Cat, 10)

	cat1 := Cat{Name: "tom", Age: 10,}
	cat2 := Cat{Name: "nancy", Age: 78,}

	catChan <- &cat1
	catChan <- &cat2

	c1 := <-catChan
	c2 := <-catChan

	fmt.Printf("c1=%p\nc2=%p", c1, c2)
}

(5)、创建一个allChan,最多可以存放10个任意数据类型变量,对这个chan写入和读取。

package main

import "fmt"

type Cat struct {
	Name string
	Age  int
}

func main() {
	var allChan chan interface{}
	allChan = make(chan interface{}, 10)

	cat1 := Cat{Name: "tom", Age: 10,}
	cat2 := Cat{Name: "nancy", Age: 78,}

	allChan <- &cat1
	allChan <- &cat2
	allChan <- 10
	allChan <- "jack"

	c1 := <-allChan
	c2 := <-allChan
	v1 := <-allChan
	v2 := <-allChan

	fmt.Println(v1, v2, c1, c2)
}

5、channel的遍历和关闭

使用内置函数close可以关闭channel, 当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据。

package main

import "fmt"

func main() {
	intChan := make(chan int, 3)
	intChan <- 100
	intChan <- 200

	//关闭后不能再写数据
	close(intChan)

	//管道关闭之后,读取数据时可以的
	n1 := <-intChan
	fmt.Println("n1=", n1)
}

channel的遍历

channel支持for--range的方式进行遍历,请注意两个细节
(1)、在遍历时,如果channel没有关闭,则回出现deadlock的错误
(2)、在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

package main

import "fmt"

func main() {
	intChan := make(chan int, 100)
	for i := 0; i < 100; i++ {
		intChan <- i * 2
	}

	close(intChan)
	for v := range intChan {
		fmt.Println("v=", v)
	}
}

 使用goroutine和channel协调完成如下需求:

开启一个writeData协程,向管道intChan中写入50个整数;开启一个readData协程,从管道intChan中读取writeData写入的数据。writeData和readData操作的是同一个管道,主线程需要等待writeData和readData协程都完成才能退出。

package main

import "fmt"

func writeData(intChan chan int) {
	for i := 1; 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.Printf("readData 读到数据=%v\n", 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
		}
	}
}

统计1-8000的数字中,哪些是素数?

package main

import (
	"fmt"
	"time"
)

func putNum(intChan chan int) {
	for i := 1; i <= 8000; i++ {
		intChan <- i
	}

	close(intChan)
}

func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
	var flag bool
	for {
		time.Sleep(time.Millisecond * 10)
		num, ok := <-intChan

		if !ok {
			break
		}

		flag = true

		for i := 2; i < num; i++ {
			if num%i == 0 {
				flag = false
				break
			}
		}

		if flag {
			primeChan <- num
		}
	}

	fmt.Println("有一个primeNum协程因为取不到数据退出")

	exitChan <- true
}

func main() {
	intChan := make(chan int, 1000)
	primeChan := make(chan int, 2000)
	exitChan := make(chan bool, 4)

	go putNum(intChan)

	for i := 0; i < 4; i++ {
		go primeNum(intChan, primeChan, exitChan)
	}

	go func() {
		for i := 0; i < 4; i++ {
			<-exitChan
		}

		close(primeChan)
	}()

	for {
		res, ok := <-primeChan
		if !ok {
			break
		}

		fmt.Printf("素数=%d\n", res)
	}

	fmt.Println("main线程退出")
}

6、channel使用细节

(1)、默认情况下,管道是双向的,可读写的。channel可以声明为只读,或者只写性质。

package main

import "fmt"

func main() {
	//声明为只写
	var intChan chan<- int
	intChan = make(chan int, 3)
	intChan <- 20

	fmt.Println("intChan=", intChan)

	//声明为只读
	var stringChan <-chan string
	str := <-stringChan

	fmt.Println("str=", str)
}

(2)、channel只读和只写的最佳实践

package main

import "fmt"

func send(ch chan<- int, exitChan chan struct{}) {
	for i := 0; i < 10; i++ {
		ch <- i
	}
	close(ch)

	var a struct{}
	exitChan <- a
}

func recv(ch <-chan int, exitChan chan struct{}) {
	for {
		v, ok := <-ch
		if !ok {
			break
		}
		fmt.Println(v)
	}
	var a struct{}
	exitChan <- a
}

func main() {
	var ch chan int
	ch = make(chan int, 10)
	exitChan := make(chan struct{}, 2)

	go send(ch, exitChan)
	go recv(ch, exitChan)

	var total = 0

	for _ = range exitChan {
		total++
		if total == 2 {
			break
		}
	}
	fmt.Println("结束")
}

(3)、使用select解决从管道中取数据的阻塞问题

package main

import (
	"fmt"
	"time"
)

func main() {
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan <- i
	}

	stringChan := make(chan string, 5)
	for i := 0; i < 5; i++ {
		stringChan <- "hello" + fmt.Sprintf("%d", i)
	}

	//传统的方法在遍历管道时,如果不关闭管道会阻塞而导致deadlock
	//在实际开发中,不好确定什么时候关闭管道。可以使用select方式解决
	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("都取不到数据")
				time.Sleep(time.Second)
				return
		}

	}
}

(4)、goroutine中使用recover,解决协程中出现panic导致程序崩溃问题

如果开启一个协程,但是这个协程出现了panic,如果没有捕获这个panic,就会造成整个程序崩溃,这时可以在goroutine中使用recover来捕获panic进行处理。这样即使这个协程发生问题,主线程仍然不受影响,可以继续执行。

package main

import (
	"fmt"
	"time"
)

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() 发生错误", err)
		}
	}()

	var myMap map[int]string
	//error,没有为map申请内存
	myMap[0] = "golang"
}
func main() {
	go sayHello()
	go test()

	for i := 0; i < 10; i++ {
		fmt.Println("main() ok=", i)
		time.Sleep(time.Second)
	}
}

 

posted on 2019-04-09 20:26  lina2014  阅读(558)  评论(0编辑  收藏  举报

导航