进程:程序启动时(好比qq),操做系统位程序开启一个进程。能够把它看作是操做系统进行资源分配和调度的一个容器,里面包含了该应用程序用到的全部资源。算法
线程:是一个独立的执行空间,用来被系统调度来运行程序代码。好比我下载文件,操做系统调度会安排到合适的cpu上进行执行,而且不必定是该程序进程所在的cpu。这个调度咱们不用关心。编程
也就是一个进程能够有好多个线程,线程用来执行具体的任务。每一个进程的初始线程叫作主线程,因此进程至少有一个线程。安全
上图,系统的调度器来调度线程在合适的cpu上运行。网络
并发:多线程或多协程在一核cpu上运行就是所谓的并发,都是cpu经过切换时间片给人一种并发的感受。多线程
并行:是真正意义上的并发,就是多核cpu同时去处理多个线程,互不干扰,并行处理。并发
并发不是并行:并行是让不一样的代码片断同时在不一样的cpu上执行,利用多核的优点。并发是经过很是快切换时间片来实现“同时”运行。并行的关键是同时作不少的事情,而并发是指同时管理不少事情,这些事情可能只作了一半就暂停作别的任务。函数
总结:并发优于并行,能够有效利用资源。go语言能够利用多核,经过goroutine 高效并发。atom
go语言经过一个叫作goroutine的东西进行并发执行,每个goroutine就是一个独立的工做单元,能够执行咱们的程序代码。goroutine是相似协程(coroutine)的东西,能够理解为一个更轻量的线程。goroutine的具体使用后面再讲,目前咱们来看一下go语言是如何经过goroutine实现并发的。spa
目前能够理解为:goroutine是个执行代码的独立工做单元,须要将它放到合适的线程和cpu上进行执行。这就须要go语言的逻辑处理器和调度器。操作系统
go语言中支撑整个scheduler实现的主要有4个重要结构,分别是M、G、P、Sched。
这里借用一张图来看下:
注意:go1.5以后为每一个cpu建立一个逻辑处理器。
咱们从一个goroutine被建立到goroutine被执行的过程来看一下go是如何实现调度的。
(1)建立一个goroutine,它会放在全局运行队列中,等待调度器调度
(2)调度器将这个goroutine 分配给一个逻辑处理器A,将它放到了这个逻辑处理器的本地队列中,这个goroutine就会等待逻辑处理器A执行它
(3)每一个逻辑处理器默认绑定了一个线程,它是在线程中去执行本身本地队列中的goroutine。
(4)逻辑处理器和原来的线程分离,调度器从新建立一个线程和这个逻辑处理器绑定。这时候逻辑处理器在新的线程上继续执行本地运行队列的其余goroutine。 同时,阻塞的goroutine随着线程分离,从本地队列移除。
(5)那个阻塞的goroutine和分离的线程会继续阻塞,等待系统调用的返回。一旦执行完成并返回,这个goroutine就会从新放回到原来逻辑处理器的本地队列。
(6)以前的线程目前没有goroutine了,可是它会被保存,以备以后使用。
调度器对能够建立的逻辑处理器的数量没有限制,但语言运行时默认限制每一个程序最多建立 10 000 个线程。这个
限制值能够经过调用 runtime/debug 包的 SetMaxThreads 方法来更改。
小结:
概念 | 说明 |
---|---|
进程 | 一个程序对应的资源容器 |
线程 | 一个独立的执行空间,一个进程能够有多个线程 |
goroutine | 一样是独立的执行空间,可是一个线程能够有多个goroutine |
逻辑处理器 | 绑定一个线程,运行goroutine |
调度器 | 将goroutine分配到合适的逻辑处理器 |
全局运行队列 | 全部刚建立的goroutine都在这 |
本地运行队列 | 逻辑处理器的goroutine队列 |
因此,咱们能够利用多核cpu,调度器建立多个逻辑处理器,而后每一个逻辑处理器能够绑定一个线程去运行多个goroutine。这样咱们就充分利用了多核资源实现并发处理,比单纯的多线程更加优秀,高效,省资源。
注意:上面第(4)(5)步,若是goroutine 是在执行网络io的操做,这个goroutine就不必定就回到这个逻辑处理器了。它实际上会先从逻辑处理器分离,移到集成了网络轮询器的运行时 ,一旦该轮询器指示某个网络读或者写操做已经就绪,对应的 goroutine 就会从新分配到逻辑处理器上来完成操做 。
看到这咱们重温下并发和并行:
go语言实现并发,建立多个goroutine,调度器会将goroutine分配到逻辑处理器的本地运行队列,逻辑处理器去运行goroutine。若是只有一个逻辑处理器,只会实现并发,不会实现并行。
要实现并行,就须要多个逻辑处理器,在不一样的cpu上,而后调度器会平等的将goroutine分配到每一个逻辑处理器,这样多个线程多个goroutine就实现了并行和并发。 至于这些算法怎么调度,咱们根本不须要关心,咱们只要记住goroutine是咱们进行并发编程的一个独立单元就能够了。
goroutine实际上是官方实现的超级“轻量线程池”。每一个实例4~5kb的占内存占用,更加轻量。只需在函数调⽤语句前添加 go 关键字,就可建立并发执⾏单元。开发⼈员⽆需了解任何执⾏细节,调度器会⾃动将其安排到合适的系统线程上执⾏。
package main import ( "fmt" "time" ) func main() { //经过go 关键字 +匿名函数就能够开启一个goroutine go func() { fmt.Println("Hello, World!") }() //因为main函数也是一个goroutine,若是不让线程等待,那么main方法执行完,就退出了,还来不及打印helloworld time.Sleep(1 * time.Second) }
WaitGroup可以一直等到全部的goroutine执行完成,而且阻塞主线程的执行,直到全部的goroutine执行完成。
WaitGroup总共有三个方法:Add(delta int),Done(),Wait()。简单的说一下这三个方法的做用。
方法名 | 说明 |
---|---|
Add | 添加或者减小等待goroutine的数量; |
Done | 至关于Add(-1),减小一个须要等待的goroutine数量 |
Wait | 进行等待,需等待的goroutine数量为0 |
WaitGroup用于线程同步,WaitGroup等待一组线程集合完成,才会继续向下执行。 主线程(goroutine)调用Add来设置等待的线程(goroutine)数量。 而后每一个线程(goroutine)运行,并在完成后调用Done。 同时,Wait用来阻塞,直到全部线程(goroutine)完成才会向下执行。
package main import ( "fmt" "runtime" "sync" ) func main() { runtime.GOMAXPROCS(1)//只使用1个物理处理器 var wg sync.WaitGroup wg.Add(2) //添加须要等待goroutine数量 fmt.Println("开启两个goroutine") go func() { defer wg.Done()//函数结束时通知main函数执行完毕 for i:=0;i<1000;i++{ fmt.Println("A:",i) } }() go func() { defer wg.Done()//函数结束时通知main函数执行完毕 for i := 0; i < 1000; i++ { fmt.Println("B:",i) } }() fmt.Println("等待goroutine运行") wg.Wait() fmt.Println("程序结束") }
从上面的代码咱们能够看到,咱们用sync包下的waitgroup 来进行线程等待,避免main函数执行完来不及执行goroutine就退出的状况。waitgroup详情下面会讲。咱们用go关键字+匿名函数 开启了两个goroutine,来并发的去打印,可是由于这个1000个打印速度太快了,还没来得及切换goroutine就第一个就已经打印完了,因此你可能会输出,A打印完了才打印B这种顺序输出。咱们能够增长一下打印时间,就能看到他们是并发打印的了。好比加个time.Sleep(time.Millisecond) 。 这段代码中有一个runtime.GOMAXPROCS(1),这是go语言能够指定程序运行使用的cpu核数,咱们能够设置多个,来实现并行。
通常来讲,经过runtime.GOMAXPROCS(runtime.NumCPU())
能够设置本机逻辑CPU的数量,不是物理CPU,好比一个双核CPU,带有超线程技术,则会被认为是4个逻辑CPU。 runtime.Gosched () 可让出底层线程,让其余goroutine 使用,runtime.Goexit 将当即终止当前goroutine 执行
runtime 小结:
runtime.GOMAXPROCS() //设置使用的逻辑处理器数量 runtime.NumCPU() //本地逻辑cpu的数量 runtime.Gosched() // 将当前goroutine的线程让给别的goroutine,本身进入运行队列等待 runtime.Goexit() //当即终止当前goroutine 运行 runtime.GOROOT() //获取go的根目录 runtime.GOOS // 获取操做系统信息
sync.WaitGroup 类型的变量是一个值类型,若是在函数间进行传递,是值传递,这样执行Done()和 wait()方法就不是同一个 WaitGroup了,就会出现死锁,因此传递时必须传递指针。 代码以下:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func(wg *sync.WaitGroup, i int) { fmt.Printf("i=>%d\n", i) wg.Done() }(&wg, i) //这里要传指针就对了 } wg.Wait() }
若是两个或者多个 goroutine 在没有互相同步的状况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种状况被称做竞争状态(race condition)。 咱们要作的是:同一时刻只能有一个 goroutine 对共享资源进行读和写操做 。
package main import ( "fmt" "runtime" "sync" ) var ( count int wg sync.WaitGroup ) func main(){ wg.Add(2) //开启两个goroutine go incCount(1) go incCount(2) wg.Wait() fmt.Println("最终结果:",count) } //执行两次 count++ func incCount(id int) { defer wg.Done() for i:=0;i<2;i++{ value:=count runtime.Gosched() value++ count=value } } //输出 最终结果: 2
从上面能够看出,咱们开启两个goroutine,每一个goroutine,都执行了两个value++并赋值给count,也就是说最终的结果应该是4,可是如今确是2。 毫无疑问,在对count 进行读写的时候,两个goroutine进行了资源竞争,而且没有同步。
程序运行就像图中所示,两个goroutine在进行切换的时候,并无同步count的数量,而且他们相互覆盖了对方,致使各自有通常的工做白作了。
go run -race goDemo.go// -race go自带的竞争监测命令,能够查看哪一行哪些方法有资源竞争。
================== WARNING: DATA RACE Read at 0x0000005fa2d0 by goroutine 7: main.incCount() D:/gopath/src/awesomeProject/goroutine/godemo.go:23 +0x76 Previous write at 0x0000005fa2d0 by goroutine 6: main.incCount() D:/gopath/src/awesomeProject/goroutine/godemo.go:26 +0x97 Goroutine 7 (running) created at: main.main() D:/gopath/src/awesomeProject/goroutine/godemo.go:16 +0x90 Goroutine 6 (finished) created at: main.main() D:/gopath/src/awesomeProject/goroutine/godemo.go:15 +0x6f ================== 最终结果: 4 Found 1 data race(s) exit status 66
如何解决资源竞争和线程同步,这就有两类,一类是传统的方式——加锁,另外一类是go语言有的经过chanel,采用csp模型,即经过通讯去共享内存,而不是经过共享内存而通讯。
咱们要实现同一时间只能有一个goroutine对共享资源进行读写操做,go语言提供了传统的解决方案,atomic和sync 包。 另外一种方式是使用channel,下一篇单独讲。
atomic 包提供了一些函数来保证对资源的读写安全。好比LoadInt32 和 StoreInt32两个函数,一个读取int32类型的值,一个写入int32类型的值。还有AddInt32()同步整型加法等,以下:
package main import ( "fmt" "runtime" "sync" "sync/atomic" ) var ( count int32 wg sync.WaitGroup ) func main(){ wg.Add(2) go incCount(1) go incCount(2) wg.Wait() fmt.Println("最终结果:",count) } func incCount(id int) { defer wg.Done() for i:=0;i<2;i++{ // 安全地对 count 加 1 atomic.AddInt32(&count, 1) runtime.Gosched() } }
这时候执行结果是4,读写安全了。atomic虽然能够解决资源竞争问题,可是比较都是比较简单的,支持的数据类型也有限。因此,sync 提供了互斥锁来解决。
sync包里提供了一种互斥型的锁,可让咱们本身灵活的控制哪些代码,同时只能有一个goroutine访问,被sync互斥锁控制的这段代码范围,被称之为临界区,临界区的代码,同一时间,只能有一个goroutine访问。代码以下:
package main import ( "fmt" "runtime" "sync" ) var ( count int32 wg sync.WaitGroup mutex sync.Mutex //声明 mutex 互斥锁变量 ) func main() { wg.Add(2) //2个等待的goroutine go incCount() go incCount() wg.Wait() fmt.Println(count) } func incCount() { defer wg.Done() for i := 0; i < 2; i++ { mutex.Lock() //临界区开始位置 value := count runtime.Gosched() value++ count = value mutex.Unlock()//临界区结束位置 } }
咱们仍是使用 sync.WaitGroup 来进行等待两个goroutine都执行完再推出main函数。 重点看咱们还声明了一个
mutext sync.Mutex
这个互斥锁,经过mutex.Lock()
加锁, mutex.Unlock()
解锁。它将中间的代码块造成一个临界区,因此,这段代码块同时只能有一个goroutine进行操做,因此,goroutine1 将count赋值给value,让出线程,此时goroutine2也没法进入临界区的代码,等待goroutine1 执行完临界区的代码,goroutine2再进行执行。这样就保证了资源的读写安全。
固然goroutine 同步还有更好,更简单的方式,使用channel。即所谓的:经过通讯来共享内存,而不是经过共享内存来通讯。