Go语言里的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为协程(goroutine)时,Go语言会将其视为一个独立的工作单元,这个单元会被调度到可用的逻辑处理器上执行。
并发编程基础
1、并发与并行(理论没看懂)
理解操作系统的线程(thread)和进程(process),有助于理解Go语言运行时调度器如何得用操作系统来并发运行goroutine。
并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。
使用较少的资源做更多的事情,是指导Go语言设计的哲学。
2、指定使用核心数
Go语言默认会调用CPU核心数,这些功能都是自动调整的,但是也提供了相应的标准库来指定核心数。使用flags包可以调整程序运行时调用的CPU核心数。
代码如下:
package main
import (
"fmt"
"time"
)
func longWait() {
fmt.Println("开始longWait()")
time.Sleep(5 * 1e9)
fmt.Println("结束longWait()")
}
func shortWait() {
fmt.Println("开始shortWait()")
time.Sleep(2 * 1e9)
fmt.Println("结束shortWait()")
}
func main() {
fmt.Println("这里是main()开始的地方")
go longWait()
go shortWait()
fmt.Println("挂起main()")
time.Sleep(10 * 1e9)
fmt.Println("这里是main()结束的地方")
}
main()、longWait()和shortWait()三个函数作为独立的处理单元按顺序启动,然后开始并行运行,每一个函数都在运行的开始和结束阶段输出了消息。
运行结果如下:
这里是main()开始的地方
挂起main()
开始longWait()
开始shortWait()
结束shortWait()
结束longWait()
这里是main()结束的地方
如果移除go语言关键字同,重新运行程序:
这里是main()开始的地方
开始longWait()
结束longWait()
开始shortWait()
结束shortWait()
挂起main()
这里是main()结束的地方
协程(goroutine)
操作系统自己掌管的进程(process)、进程内的线程(thread)以及进程内的协程(coroutine,也叫轻量级线程)。
协程的最大优势在于其“轻量级”,可以轻松创建上百万个协程而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万个。
Go语言在语言级别支持轻量级线程,叫goroutine。
1、协程基础
goroutine是Go语言并行设计的核心。
goroutine是通过Go程序的runtime管理的一个线程管理器。goroutine通过go关键字实现了,其实就是一个普通的函数。
代码如下:
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 5; i++ {
runtime.Gosched()
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
运行结果如下:与书本不一样。
hello
hello
hello
hello
hello
书本是:
hello
world
hello
world
hello
world
hello
world
hello
2、协程间通信
通常有两种最常见的并发通信模型:共享数据和消息。
代码如下:
package main
import (
"fmt"
"runtime"
"sync"
)
var counter int = 0
func Count(lock *sync.Mutex) {
lock.Lock()
counter++
fmt.Println(z)
lock.Unlock()
}
func main() {
lock := &sync.Mutex{}
for i := 0; i < 10; i++ {
go Count(lock)
}
for {
lock.Lock()
c := counter
lock.Unlock()
runtime.Gosched()
if c >= 10 {
break
}
}
}
运行结果如下:
# command-line-arguments
./main.go:14:14: undefined: z
不要通过共享内存来通信,而应该通过通信来共享内存。
通道(Channel)
代码如下:
package main
import (
"fmt"
)
func Count(ch chan int) {
ch <- 1
fmt.Println("Counting")
}
func main() {
chs := make([]chan int, 10)
for i := 0; i < 10; i++ {
chs[i] = make(chan int)
go Count(chs[i])
}
for _, ch := range chs {
<-ch
}
}
1、基本语法
一般channel的声明形式为:
在channel的用法中,最常见的包括写入和读出。
将一个数据写入(发送)到channel的语法很直观:
从channel中读取数据的语法是:
2、select
Go语言直接在语言级别支持select关键字,用于处理异步I/O问题。
3、缓冲机制
4、超时和计时器
5、channel的传递
6、单向channel
7、关闭channel
使用go语言内置的close()函数如下:
close(ch)
并发进阶
1、多核并行化
2、协程同步
只有在当需要告诉接受者不会再提供新的值的时候,才需要关闭通道。
只有发送者需要关闭通道,接收都永远不会需要。
代码如下:
package main
import (
"fmt"
"time"
)
func sendData(ch chan string) {
ch <- "纽约"
ch <- "华盛顿"
ch <- "伦敦"
ch <- "北京"
ch <- "东京"
}
func getData(ch chan string) {
var input string
for {
input = <-ch
fmt.Printf("%s ", input)
}
}
func main() {
ch := make(chan string)
go sendData(ch)
go getData(ch)
time.Sleep(1e9)
}
运行结果如下:
纽约 华盛顿 伦敦 北京 东京
代码如下:
package main
import (
"fmt"
)
func sendData(ch chan string) {
ch <- "纽约"
ch <- "华盛顿"
ch <- "伦敦"
ch <- "北京"
ch <- "东京"
close(ch)
}
func getData(ch chan string) {
for {
input, open := <-ch
if !open {
break
}
fmt.Printf("%s ", input)
}
}
func main() {
ch := make(chan string)
go sendData(ch)
getData(ch)
}
运行结果如下:
纽约 华盛顿 伦敦 北京 东京