goroutine和channel

一、goroutine

1、并发和并行:

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

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

2、Go协程和Go主线程

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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完成。 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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加入互斥锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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、管道的初始化及读写数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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进行写入和读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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进行写入和读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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进行写入和读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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写入和读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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读取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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协程都完成才能退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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的数字中,哪些是素数?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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可以声明为只读,或者只写性质。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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只读和只写的最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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解决从管道中取数据的阻塞问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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进行处理。这样即使这个协程发生问题,主线程仍然不受影响,可以继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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   lina2014  阅读(559)  评论(0编辑  收藏  举报

导航

< 2025年1月 >
29 30 31 1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31 1
2 3 4 5 6 7 8
点击右上角即可分享
微信分享提示