go语言中sync包和channel机制

文章转载至:https://www.bytelang.com/article/content/A4jMIFmobcA=golang


 

 

golang中实现并发很是简单,只需在须要并发的函数前面添加关键字"Go",可是如何处理go并发机制中不一样goroutine之间的同步与通讯,golang 中提供了sync包和channel机制来解决这一问题.缓存

sync 包提供了互斥锁这类的基本的同步原语.除 Once 和 WaitGroup 以外的类型大多用于底层库的例程。更高级的同步操做经过信道与通讯进行。并发

type Cond
    func NewCond(l Locker) *Cond
    func (c *Cond) Broadcast()
    func (c *Cond) Signal()
    func (c *Cond) Wait()
type Locker
type Mutex
    func (m *Mutex) Lock()
    func (m *Mutex) Unlock()
type Once
    func (o *Once) Do(f func())
type Pool
    func (p *Pool) Get() interface{}
    func (p *Pool) Put(x interface{})
type RWMutex
    func (rw *RWMutex) Lock()
    func (rw *RWMutex) RLock()
    func (rw *RWMutex) RLocker() Locker
    func (rw *RWMutex) RUnlock()
    func (rw *RWMutex) Unlock()
type WaitGroup
    func (wg *WaitGroup) Add(delta int)
    func (wg *WaitGroup) Done()
    func (wg *WaitGroup) Wait()

  

 

而golang中的同步是经过sync.WaitGroup来实现的.WaitGroup的功能:它实现了一个相似队列的结构,能够一直向队列中添加任务,当任务完成后便从队列中删除,若是队列中的任务没有彻底完成,能够经过Wait()函数来出发阻塞,防止程序继续进行,直到全部的队列任务都完成为止.函数

WaitGroup总共有三个方法:Add(delta int), Done(), Wait()。Add:添加或者减小等待goroutine的数量Done:至关于Add(-1)Wait:执行阻塞,直到全部的WaitGroup数量变成0spa

具体例子以下:code

 

package main  
  
import (  
    "fmt"  
    "sync"  
)  
  
var waitgroup sync.WaitGroup  
  
func Afunction(shownum int) {  
    fmt.Println(shownum)  
    waitgroup.Done() //任务完成,将任务队列中的任务数量-1,其实.Done就是.Add(-1)  
}  
  
func main() {  
    for i := 0; i < 10; i++ {  
        waitgroup.Add(1) //每建立一个goroutine,就把任务队列中任务的数量+1  
        go Afunction(i)  
    }  
    waitgroup.Wait() //.Wait()这里会发生阻塞,直到队列中全部的任务结束就会解除阻塞  
}  

  

在线示例:https://www.bytelang.com/o/s/c/6z7UkvezTJg=协程

 

使用场景:blog

  程序中须要并发,须要建立多个goroutine,而且必定要等这些并发所有完成后才继续接下来的程序执行.WaitGroup的特色是Wait()能够用来阻塞直到队列中的全部任务都完成时才解除阻塞,而不须要sleep一个固定的时间来等待.可是其缺点是没法指定固定的goroutine数目.队列

 

Channel机制:进程

相对sync.WaitGroup而言,golang中利用channel实习同步则简单的多.channel自身能够实现阻塞,其经过<-进行数据传递,channel是golang中一种内置基本类型,对于channel操做只有4种方式:

建立channel(经过make()函数实现,包括无缓存channel和有缓存channel);

向channel中添加数据(channel<-data);

从channel中读取数据(data<-channel);

关闭channel(经过close()函数实现,关闭以后没法再向channel中存数据,可是能够继续从channel中读取数据)

channel分为有缓冲channel和无缓冲channel,两种channel的建立方法以下:

var ch = make(chan int) //无缓冲channel,等同于make(chan int ,0)

var ch = make(chan int,10) //有缓冲channel,缓冲大小是5

其中无缓冲channel在读和写是都会阻塞,而有缓冲channel在向channel中存入数据没有达到channel缓存总数时,能够一直向里面存,直到缓存已满才阻塞.因为阻塞的存在,因此使用channel时特别注意使用方法,防止死锁的产生.例子以下:

无缓存channel:

 

package main  
  
import "fmt"  
  
func Afuntion(ch chan int) {  
    fmt.Println("finish")  
    <-ch  
}  
  
func main() {  
    ch := make(chan int) //无缓冲的channel  
    go Afuntion(ch)  
    ch <- 1  
      
    // 输出结果:  
    // finish  
} 

 

在线示例:https://www.bytelang.com/o/s/c/3cxH7Jko7YY=

 

代码分析:首先建立一个无缓冲channel ch, 而后执行 go Afuntion(ch),此时执行<-ch,则Afuntion这个函数便会阻塞,再也不继续往下执行,直到主进程中ch<-1向channel ch 中注入数据才解除Afuntion该协程的阻塞.

更正:

代码分析:对于该段程序(只有单核cpu运行的程序)首先建立一个无缓冲channel  ch,而后遇到go Afuntion(ch),查看此时无cpu能够用来运行该任务,则将该任务记下,等到有cpu时再运行该任务,而后执行ch<-1,此时主goroutine阻塞,查找是否有其余协程,查找到有Afuntion(ch)这一goroutine,则执行该goroutine内容,直到<-ch才从主goroutine获取数据1,解除主goroutine阻塞.(注:这种执行方式仅限于单核cpu)

若是指定多个cpu运行,则首先运行主goroutine建立无缓冲的channel,而后查看是否有空闲cpu能够运行另一个goroutine,若是有,则运行协程Afuntion(ch),对于多核cpu,主goroutine和另一个goroutine的运行顺序是不肯定的.

 

 

package main
import "fmt"  
import "runtime"  
import "time"  
  
func Afuntion(ch chan int) {  
    fmt.Println("finish")  
    <-ch  
}  
  
func main() {  
    runtime.GOMAXPROCS(runtime.NumCPU())  
    ch := make(chan int) //无缓冲的channel  
    go Afuntion(ch)  
    time.Sleep(time.Nanosecond * 1000)  
    fmt.Println("main goroutine")  
    ch <- 1  
}  

 

 

在线示例:https://www.bytelang.com/o/s/c/9z_uWI5ZumA=

 

运行结果: 

finishmain goroutine

或者  main goroutine

finish

主goroutine和另一个goroutine的执行顺序是不肯定的(对于多核cpu)

 

package main  
  
import "fmt"  
  
func Afuntion(ch chan int) {  
    fmt.Println("finish")  
    <-ch  
}  
  
func main() {  
    ch := make(chan int) //无缓冲的channel  
    //只是把这两行的代码顺序对调一下  
    ch <- 1  
    go Afuntion(ch)  
  
    // 输出结果:  
    // 死锁,无结果  
}  

 

在线示例:https://www.bytelang.com/o/s/c/sLL_Cto3k4E=

 

代码分析:首先建立一个无缓冲的channel, 而后在主协程里面向channel ch 中经过ch<-1命令写入数据,则此时主协程阻塞,就没法执行下面的go Afuntions(ch),天然也就没法解除主协程的阻塞状态,则系统死锁

总结:
对于无缓存的channel,放入channel和从channel中向外面取数据这两个操做不能放在同一个协程中,防止死锁的发生;同时应该先利用go 开一个协程对channel进行操做,此时阻塞该go 协程,而后再在主协程中进行channel的相反操做(与go 协程对channel进行相反的操做),实现go 协程解锁.即必须go协程在前,解锁协程在后.

带缓存channel:
对于带缓存channel,只要channel中缓存不满,则能够一直向 channel中存入数据,直到缓存已满;同理只要channel中缓存不为0,即可以一直从channel中向外取数据,直到channel缓存变为0才会阻塞.

因而可知,相对于不带缓存channel,带缓存channel不易形成死锁,能够同时在一个goroutine中放心使用,

 

close():

close主要用来关闭channel通道其用法为close(channel),而且实在生产者的地方关闭channel,而不是在消费者的地方关闭.而且关闭channel后,便不可再想channel中继续存入数据,可是能够继续从channel中读取数据.例子以下:

 

package main  
  
import "fmt"  
  
func main() {  
    var ch = make(chan int, 20)  
    for i := 0; i < 10; i++ {  
        ch <- i  
    }  
    close(ch)  
    //ch <- 11 //panic: runtime error: send on closed channel  
    for i := range ch {  
        fmt.Println(i) //输出0 1 2 3 4 5 6 7 8 9  
    }  
} 

 

在线示例:https://www.bytelang.com/o/s/c/XBiMiCoE7dc=

 

channel阻塞超时处理:
goroutine有时候会进入阻塞状况,那么如何避免因为channel阻塞致使整个程序阻塞的发生那?解决方案:经过select设置超时处理,具体程序以下:

 

package main  
  
 import (  
    "fmt"  
    "time"  
)  
  
func main() {  
    c := make(chan int)  
    o := make(chan bool)  
    go func() {  
        for {  
            select {  
            case i := <-c:  
                fmt.Println(i)  
            case <-time.After(time.Duration(3) * time.Second):    //设置超时时间为3s,若是channel 3s钟没有响应,一直阻塞,则报告超时,进行超时处理.  
                fmt.Println("timeout")  
                o <- true  
                break  
            }  
        }  
    }()  
    <-o  
}

 

在线示例:https://www.bytelang.com/o/s/c/6V74LnkRLN0=

 

golang 并发总结:

并发两种方式:sync.WaitGroup,该方法最大优势是Wait()能够阻塞到队列中的全部任务都执行完才解除阻塞,可是它的缺点是不可以指定并发协程数量.channel优势:可以利用带缓存的channel指定并发协程goroutine,比较灵活.可是它的缺点是若是使用不当容易形成死锁;而且他还须要本身断定并发goroutine是否执行完.可是相对而言,channel更加灵活,使用更加方便,同时经过超时处理机制能够很好的避免channel形成的程序死锁,所以利用channel实现程序并发,更加方便,更加易用.

相关文章
相关标签/搜索