关于如何使用groutine运行程序 、检测并修正竞争状态 、利用通道共享数据 。参考至《GO语言实战》第六章内容
使用groutine运行程序**
检测并修正竞争状态
利用通道共享数据
当一个函数创建为 goroutine时,Go 会将其视为一个独立的工作单元。
当运行一个应用程序(如一个 IDE 或者编辑器)的时候,操作系统会为这个应用程序启动一个进程。可以将这个进程看作一个包含了应用程序在运行中需要用到和维护的各种资源的容器。
一个线程是一个执行空间,这个空间会被操作系统调度来运行函数中所写的代码。
并发(concurrency)不是并行(parallelism)。并行是让不同的代码片段同时在不同的物理处理器上执行。
并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。
一个正运行的 goroutine 在工作结束前,可以被停止并重新调度。
调度器这样做的目的是防止某个 goroutine 长时间占用逻辑处理器。当 goroutine 占用时间过长时,调度器会停止当前正运行的 goroutine,并给其他可运行的 goroutine 运行的机会。
和其他函数调用一样,创建为 goroutine 的函数调用时可以传入参数。不过 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 33 34 35 36 37 38 39 40 41 package mainimport ( "fmt" "runtime" "sync" ) var wg sync.WaitGroupfunc main () { runtime.GOMAXPROCS(1 ) wg.Add(2 ) fmt.Println("Create Groutines" ) go printPrime("A" ) go printPrime("B" ) fmt.Println("Waiting To Finish" ) wg.Wait() fmt.Println("Terminating Program" ) } func printPrime (prefix string ) { defer wg.Done() next: for outer:=2 ;outer<5000 ;outer++ { for inner:=2 ;inner < outer;inner++{ if outer%inner==0 { continue next } } fmt.Printf("%s:%d\n" ,prefix,outer) } fmt.Println("Completed" ,prefix) }
记住,只有在有多个逻辑处理器且可以同时让每个goroutine 运行在一个可用的物理处理器上的时候,goroutine 才会并行运行。
1 2 runtime.GOMAXPROCS(2 ) runtime.GOMAXPROCS(runtime.NumCPU())
写并发程序时的潜在危险,以及需要注意的事情:
如果两个或者多个groutine在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于互相竞争的状态,这种情况被称为竞争状态 。
同一时刻只能有一个goroutine对共享资源进行读和写操作。
Go 语言有一个特别的工具,可以在代码里检测竞争状态。
1 $ go build -race //用竞争检测器标志来编译程序
种修正代码、消除竞争状态的办法是,使用 Go 语言提供的锁机制,来锁住共享资源,从而保证 goroutine 的同步状态。
Go 语言提供了传统的同步 goroutine 的机制,就是对共享资源加锁。如果需要顺序访问一个整型变量或者一段代码,atomic 和 sync 包里的函数提供了很好的解决方案。
1 2 3 import "sync/atomic" atomic.Addint64(&counter,1 )
程序使用了 atmoic 包的 AddInt64 函数。这个函数会同步整型值的加法,方法是强制同一时刻只能有一个 goroutine 运行并完成这个加法操作。
另外LoadInt64和StoreInt64。这两个函数提供了一种安全地读和写一个整型值的方式。
互斥锁用于在代码上创建一个临界区,保证同一时间只有一个goroutine可以执行这个临界区代码。
1 2 3 4 5 import syncvar mutex sync.Mutex mutex.Lock() {} mutex.Unlock()
使用大括号只是为了让临界区看起来更清晰,并不是必需的。
当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。
声明通道时,需要指定将要被共享的数据的类型,可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。
在Go语言中需要内置函数make来创建一个通道。
1 2 3 4 unbuffered := make (chan int ) bufferd := make (chan string ,10 )
想通道发送值或者指针需要用到<-操作符
1 2 3 4 buffered := make (chan string ,10 ) buffered <- "Gopher"
为了让另一个 goroutine 可以从该通道里接收到这个字符串,我们依旧使用<-操作符,但这次是一元运算符
无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。
这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收的操作。
如果没有同时准备好,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 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 package mainimport ( "fmt" "math/rand" "sync" "time" ) var wg sync.WaitGroupfunc init () { rand.Seed(time.Now().UnixNano()) } func main () { court := make (chan int ) wg.Add(2 ) go player("Nadal" ,court) go player("Djokovic" ,court) court <- 1 wg.Wait() } func player (name string ,court chan int ) { defer wg.Done() for { ball,ok := <- court if !ok{ fmt.Printf("Player %s Won\n" ,name) return } n := rand.Intn(100 ) if n%13 == 0 { fmt.Printf("Player %s Missed\n" ,name) close (court) return } fmt.Printf("Player %s Hit %d\n" ,name,ball) ball++ court <- ball } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Output: Player Djokovic Hit 1 Player Nadal Hit 2 Player Djokovic Hit 3 Player Nadal Hit 4 Player Djokovic Hit 5 Player Nadal Hit 6 Player Djokovic Hit 7 Player Nadal Hit 8 Player Djokovic Hit 9 Player Nadal Hit 10 Player Djokovic Hit 11 Player Nadal Hit 12 Player Djokovic Hit 13 Player Nadal Hit 14 Player Djokovic Hit 15 Player Nadal Hit 16 Player Djokovic Hit 17 Player Nadal Hit 18 Player Djokovic Missed Player Nadal Won
有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。
通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞
无缓冲的通道保证进行发送和接收的 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 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 package mainimport ( "fmt" "math/rand" "sync" "time" ) const ( numberGoroutines = 4 taskLoad = 10 ) var wg sync.WaitGroupfunc init () { rand.Seed(time.Now().Unix()) } func main () { tasks := make (chan string ,taskLoad) wg.Add(numberGoroutines) for gr := 1 ; gr <= numberGoroutines; gr++ { go worker(tasks,gr) } for post := 1 ; post <= taskLoad; post++{ tasks <- fmt.Sprintf("Task:%d" ,post) } close (tasks) wg.Wait() } func worker (tasks chan string ,worker int ) { defer wg.Done() for { task,ok := <- tasks if !ok { fmt.Printf("Worker:%d:Shutting Down\n" ,worker) return } fmt.Printf("Worker:%d:Started %s\n" ,worker,task) sleep := rand.Int63n(100 ) time.Sleep(time.Duration(sleep) * time.Millisecond) fmt.Printf("Worker: %d : Completed %s \n" ,worker,task) } }
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 Output: Worker:4 :Started Task:4 Worker:3 :Started Task:3 Worker:2 :Started Task:2 Worker:1 :Started Task:1 Worker: 1 : Completed Task:1 Worker:1 :Started Task:5 Worker: 2 : Completed Task:2 Worker:2 :Started Task:6 Worker: 4 : Completed Task:4 Worker:4 :Started Task:7 Worker: 1 : Completed Task:5 Worker:1 :Started Task:8 Worker: 3 : Completed Task:3 Worker:3 :Started Task:9 Worker: 4 : Completed Task:7 Worker:4 :Started Task:10 Worker: 3 : Completed Task:9 Worker:3 :Shutting Down Worker: 2 : Completed Task:6 Worker:2 :Shutting Down Worker: 4 : Completed Task:10 Worker:4 :Shutting Down Worker: 1 : Completed Task:8 Worker:1 :Shutting Down
当通道关闭后,goroutine 依旧可以从通道接收数据,但是不能再向通道里发送数据。
有缓冲的通道和无缓冲的通道的例子很好地展示了如何编写使用通道的代码。在下一章,我们会介绍真实世界里的一些可能会在工程里用到的并发模式。
并发是指goroutine运行的时候是相互独立的。
使用关键词go创建goroutine来运行函数。
goroutine在逻辑处理器上执行,而逻辑处理器具有独立的系统线程和运行队列。
竞争状态是指两个或者多个goroutine试图访问同一个资源。
原子函数和互斥锁提供了一种防止出现竞争状态的办法。
通道提供了一种在两个goroutine之间共享数据的简单方法。
无缓冲的通道保证同时交换数据,而有缓冲的通道不做这种保证。