hello云胜

技术与生活

0%

协程goroutine

go中的多线程称为协程goroutine。和java的Thread是有区别的。

goroutine相比java的Thread更加轻量,(在一些地方被称为绿色线程)。goroutine创建的用户态线程。java的thread是内核态线程,是真正的系统级线程。

(linux操作系统分为用户态和内核态)。所以thread进行上下文切换时需要进行内核态和用户态的切换,此时的性能开销就比较大。goroutine就没有这些切换开销,只要内存充足,一个go程序可以轻松支持上万的并发goroutine。

Go不支持创建系统线程,所以协程是一个Go程序内部唯一的并发实现方式。

每个Go程序启动的时候只有一个对用户可见的协程,称之为主协程。 一个协程可以开启更多其它的协程。

在Go中,开启一个新的协程是非常简单的。 只需在一个函数调用之前使用一个go关键字,即可让此函数调用运行在一个新的协程之中。 当此函数调用退出后,这个新的协程也随之结束了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"log"
"math/rand"
"time"
)

func SayGreetings(greeting string, times int) {
for i := 0; i < times; i++ {
time.Sleep(2 * time.Second) // 睡眠2秒
log.Println(greeting)
}
}

func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(11)
go SayGreetings("hi!", 10)
go SayGreetings("hello!", 10)
time.Sleep(7 * time.Second)
}

只能分别执行3次,因为当main主协程结束后,子协程也就随之结束了。

这里使用log标准库的打印函数是因为log标准库的打印函数是经过了同步处理的,如果使用fmt的Println可能会有并发问题,不同协程的打印会交织在一起(这个例子来说,概率较低)。

如果改用java写

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
public class ThreadTest {

public void sayGreeting(String greeting, int times) {
for (int i = 0; i < times; i++) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(greeting + i);
}
}

public static void main(String[] args) {
ThreadTest demo = new ThreadTest();
new Thread(() -> {
demo.sayGreeting("hi", 10);
}).start();

new Thread(() -> {
demo.sayGreeting("hello", 10);
}).start();
}

}

猜猜这个代码会打印什么结果?

结果是十次全部执行完毕。

在java的main方法中,并没有使用类似go的main函数的sleep方法。java程序会等待所有线程执行完后才会彻底退出。

原因是java的程序的运行是由jvm控制的,jvm的退出由以下条件决定:

  1. jvm退出的时机是所有的非守护进程结束了。(守护进程是setDaemon(true)设置的)
  2. main线程是一个用户线程。是jvm启动的运行用户程序的一个用户线程。
  3. 用户在main线程里开启的其他线程还是用户线程。
  4. 即使main线程退出了,只要有其他用户线程还在运行,jvm就不会退出

协程的调度

我们可以很容易的启动多个线程。但并不是所有处于运行状态的协程都在执行。在同一时刻,只能最多有和主机逻辑cpu个数一样多的协程在同时执行。

go运行时来保证cpu不断在多个协程之间切换,从而使多个协程得以并行执行。这和操作系统并行调度执行系统级线程的原理使一样的,只是这里由go-runtime来做这个事。

goroutine-schedule

协程的详细状态图,注意睡眠和等待都属于运行状态。

go运行时采用一种被称为M-P-G模型的算法来实现协程调度。 其中,M表示系统线程,P表示逻辑处理器(并非上述的逻辑CPU),G表示协程。 大多数的调度工作是通过逻辑处理器(P)来完成的。 逻辑处理器P像一个监工一样通过将不同的处于运行状态协程(G)交给不同的系统线程(M)来执行。 一个协程在同一时刻只能在一个系统线程中执行。一个执行中的协程运行片刻后将自发地脱离让出一个系统线程,从而使得其它处于等待子状态的协程G得到执行机会。

并发同步措施

多线程代码我们最主要的是需要保障数据的并发安全问题。

WaitGroup

和java并发包中的CountDownLatch含义一致,用法也相似。用来等待其他线程/协程执行全部执行完毕后,才继续进行后续逻辑。

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 (
"log"
"math/rand"
"sync"
"time"
)

var wg sync.WaitGroup

func SayGreetings(greeting string, times int) {
for i := 0; i < times; i++ {
time.Sleep(2 * time.Second) // 睡眠2秒
log.Printf("%v,%d", greeting, i)
}
wg.Done()
}

func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(11)
wg.Add(2)
go SayGreetings("hi!", 10)
go SayGreetings("hello!", 10)
wg.Wait()
}

WaitGroup主要用三个方法

Add:声明等待几个任务完成

Done:完成其中1个任务

Wait:组合等待,知道全部任务完成。

和java的CountDownLatch用法几乎一样

chan