什么是goroutine?golang
Goroutine是创建在线程之上的轻量级的抽象。它容许咱们以很是低的代价在同一个地址空间中并行地执行多个函数或者方法。相比于线程,它的建立和销毁的代价要小不少,而且它的调度是独立于线程的。在golang中建立一个goroutine很是简单,使用“go”关键字便可:算法
package main import ( "fmt" "time" ) func learning() { fmt.Println("My first goroutine") } func main() { go learning() /* we are using time sleep so that the main program does not terminate before the execution of goroutine.*/ time.Sleep(1 * time.Second) fmt.Println("main function") }
输出:缓存
My first goroutine main function
如果将time.sleep拿掉安全
package main import ( "fmt" //"time" ) func learning() { fmt.Println("My first goroutine") } func main() { go learning() /* we are using time sleep so that the main program does not terminate before the execution of goroutine.*/ //time.Sleep(1 * time.Second) fmt.Println("main function") }
只输出:网络
main function
这是由于,和线程同样,golang的主函数(其实也跑在一个goroutine中)并不会等待其它goroutine结束。若是主goroutine结束了,全部其它goroutine都将结束。并发
Goroutine与线程的区别函数
许多人认为goroutine比线程运行得更快,这是一个误解。Goroutine并不会更快,它只是增长了更多的并发性。当一个goroutine被阻塞(好比等待IO),golang的scheduler会调度其它能够执行的goroutine运行。与线程相比,它有如下几个优势:高并发
Goroutine所须要的内存一般只有2kb,而线程则须要1Mb(500倍)。工具
因为线程建立时须要向操做系统申请资源,而且在销毁时将资源归还,所以它的建立和销毁的开销比较大。相比之下,goroutine的建立和销毁是由go语言在运行时本身管理的,所以开销更低。ui
这是goroutine于线程的主要区别,也是golang可以实现高并发的主要缘由。线程的调度方式是抢占式的,若是一个线程的执行时间超过了分配给它的时间片,就会被其它可执行的线程抢占。在线程切换的过程当中须要保存/恢复全部的寄存器信息,好比16个通用寄存器,PC(Program Counter),SP(Stack Pointer),段寄存器等等。
而goroutine的调度是协同式的,它不会直接地与操做系统内核打交道。当goroutine进行切换的时候,以后不多量的寄存器须要保存和恢复(PC和SP)。所以gouroutine的切换效率更高。
Goroutine的调度
真如前面提到的,goroutine的调度方式是协同式的。在协同式调度中,没有时间片的概念。为了并行执行goroutine,调度器会在如下几个时间点对其进行切换:
下面让咱们来看一下调度器具体是如何工做的。Golang调度器中有三个概念
在一个Go程序中,可用的线程数是经过GOMAXPROCS来设置的,默认值是可用的CPU核数。咱们能够用runtime包动态改变这个值。OSThread调度在processor上,goroutines调度在OSThreads上,
每一个P会维护一个全局运行队列(称为runqueue),处于ready就绪状态的goroutine(灰色G)被放在这个队列中等待被调度。在编写程序时,每当go func启动一个goroutine时,runqueue便在尾部加入一个goroutine。在下一个调度点上,P就从runqueue中取出一个goroutine出来执行(蓝色G)。
当某个操做系统线程M阻塞的时候(好比goroutine执行了阻塞的系统调用),P能够绑定到另一个操做系统线程M上,让运行队列中的其余goroutine继续执行:
上图中G0执行了阻塞操做,M0被阻塞,P将在新的系统线程M1上继续调度G执行。M1有多是被新建立的,或者是从线程缓存中取出。Go调度器保证有足够的线程来运行全部的P,Go语言运行时默认限制每一个程序最多建立10000个线程,这个如今能够经过调用runtime/debug包的SetMaxThreads方法来更改。
Go能够在在一个逻辑处理器P上实现并发,若是须要并行,必须使用多于1个的逻辑处理器。Go调度器会把goroutine平等分配到每一个逻辑处理器上,此时goroutine将在不一样的线程上运行,不过前提是要求机器拥有多个物理处理器。
package main import ( "fmt" "runtime" "sync" ) var ( wg sync.WaitGroup ) func main() { //分配一个逻辑处理器P给调度器使用 runtime.GOMAXPROCS(1) //在这里,wg用于等待程序完成,计数器加2,表示要等待两个goroutine wg.Add(2) //声明1个匿名函数,并建立一个goroutine fmt.Printf("Begin Coroutinesn\n") go func() { //在函数退出时,wg计数器减1 defer wg.Done() //打印3次小写字母表 for count := 0; count < 3; count++ { for char := 'a'; char < 'a'+26; char++ { fmt.Printf("%c ", char) } fmt.Println("\n") } }() //声明1个匿名函数,并建立一个goroutine go func() { defer wg.Done() //打印大写字母表3次 for count := 0; count < 3; count++ { for char := 'A'; char < 'A'+26; char++ { fmt.Printf("%c ", char) } fmt.Println("\n") } }() fmt.Printf("Waiting To Finish....\n") //等待2个goroutine执行完毕 wg.Wait() }
这个程序使用runtime.GOMAXPROCS(1)来分配一个逻辑处理器给调度器使用,两个goroutine将被该逻辑处理器调度并发执行。程序输出:
Begin Coroutinesn Waiting To Finish.... A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z
从输出来看,是先执行完一个goroutine,再接着执行第二个goroutine的,大写字母所有打印完后,再打印所有的小写字母。那么,有没有办法让两个goroutine并行执行呢?为程序指定两个逻辑处理器便可:
package main import ( "fmt" "runtime" "sync" ) var ( wg sync.WaitGroup ) func main() { //分配一个逻辑处理器P给调度器使用 runtime.GOMAXPROCS(2) //在这里,wg用于等待程序完成,计数器加2,表示要等待两个goroutine wg.Add(2) //声明1个匿名函数,并建立一个goroutine fmt.Printf("Begin Coroutinesn\n") go func() { //在函数退出时,wg计数器减1 defer wg.Done() //打印3次小写字母表 for count := 0; count < 3; count++ { for char := 'a'; char < 'a'+26; char++ { fmt.Printf("%c ", char) } fmt.Println("\n") } }() //声明1个匿名函数,并建立一个goroutine go func() { defer wg.Done() //打印大写字母表3次 for count := 0; count < 3; count++ { for char := 'A'; char < 'A'+26; char++ { fmt.Printf("%c ", char) } fmt.Println("\n") } }() fmt.Printf("Waiting To Finish....\n") //等待2个goroutine执行完毕 wg.Wait() }
结果输出
Begin Coroutinesn Waiting To Finish.... A B C D E F a b c d e G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h T U V W X Y Z A B C D E F G H I J K L M N O P i j k l m n o p q r s t u v w x y z Q R S T U V W X Y Z
那若是只有1个逻辑处理器,如何让两个goroutine交替被调度?实际上,若是goroutine须要很长的时间才能运行完,调度器的内部算法会将当前运行的goroutine让出,防止某个goroutine长时间占用逻辑处理器。因为示例程序中两个goroutine的执行时间都很短,在为引发调度器调度以前已经执行完。不过,程序也可使用runtime.Gosched()来将当前在逻辑处理器上运行的goruntine让出,让另外一个goruntine获得执行:
package main import ( "fmt" "runtime" "sync" ) var ( wg sync.WaitGroup ) func main() { //分配一个逻辑处理器P给调度器使用 runtime.GOMAXPROCS(2) //在这里,wg用于等待程序完成,计数器加2,表示要等待两个goroutine wg.Add(2) //声明1个匿名函数,并建立一个goroutine fmt.Printf("Begin Coroutinesn\n") go func() { //在函数退出时,wg计数器减1 defer wg.Done() //打印3次小写字母表 for count := 0; count < 3; count++ { for char := 'a'; char < 'a'+26; char++ { if char == 'k' { runtime.Gosched() } fmt.Printf("%c ", char) } fmt.Println("\n") } }() //声明1个匿名函数,并建立一个goroutine go func() { defer wg.Done() //打印大写字母表3次 for count := 0; count < 3; count++ { for char := 'A'; char < 'A'+26; char++ { if char == 'K' { runtime.Gosched() } fmt.Printf("%c ", char) } fmt.Println("\n") } }() fmt.Printf("Waiting To Finish....\n") //等待2个goroutine执行完毕 wg.Wait() }
两个goroutine在循环的字符为k/K的时候会让出逻辑处理器,程序的输出结果为:
Begin Coroutinesn Waiting To Finish.... A B C D E F G H I J a K L b c d e f g h i j M N O P Q R S T U V W X Y Z k l A B C D E F G H I J m n o p q r s K L M N O P Q R S T U V W X Y Z A B C D E F G H I J t K L M N O P Q R S T U V W X Y Z u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z
这里大小写字母果真是交替着输出了。
处理竞争状态
并发程序避免不了的一个问题是对资源的同步访问。若是多个goroutine在没有互相同步的状况下去访问同一个资源,并进行读写操做,这时goroutine就处于竞争状态下:
package main import ( "fmt" "runtime" "sync" ) var ( //counter为访问的资源 counter int64 wg sync.WaitGroup ) func addCount() { defer wg.Done() for count := 0; count < 2; count++ { value := counter //当前goroutine从线程退出 runtime.Gosched() value++ counter = value } } func main() { wg.Add(2) go addCount() go addCount() wg.Wait() fmt.Printf("counter: %d\n", counter) }
//output:counter: 4 或者counter: 2
这段程序中,goroutine对counter的读写操做没有进行同步,goroutine 1对counter的写结果可能被goroutine 2所覆盖。Go可经过以下方式来解决这个问题:
检测竞争状态
有时候竞争状态并不能一眼就看出来。Go 提供了一个很是有用的工具,用于检测竞争状态。使用方式是:
go build -race example4.go//用竞争检测器标志来编译程序./example4 //运行程序
原子操做实例:
package main import ( "fmt" "runtime" "sync" "sync/atomic" ) var ( //counter为访问的资源 counter int64 wg sync.WaitGroup ) func addCount() { defer wg.Done() for count := 0; count < 2; count++ { //使用原子操做来进行 atomic.AddInt64(&counter, 1) //当前goroutine从线程退出 runtime.Gosched() } } func main() { wg.Add(2) go addCount() go addCount() wg.Wait() fmt.Printf("counter: %d\n", counter) }
这里使用atomic.AddInt64函数来对一个整形数据进行加操做,另一些有用的原子操做还有:atomic.StoreInt64() //写 , atomic.LoadInt64() //读 ,更多的原子操做函数请看atomic包中的声明。
使用互斥锁
对临界区的访问,可使用互斥锁来进行
package main import ( "fmt" "runtime" "sync" ) var ( //counter为访问的资源 counter int64 wg sync.WaitGroup mutex sync.Mutex ) func addCount() { defer wg.Done() for count := 0; count < 2; count++ { //加上锁,进入临界区域 mutex.Lock() value := counter //当前goroutine从线程退出 runtime.Gosched() value++ counter = value //离开临界区,释放互斥锁 mutex.Unlock() } } func main() { wg.Add(2) go addCount() go addCount() wg.Wait() fmt.Printf("counter: %d\n", counter) }
输出: counter: 4
使用Lock()与Unlock()函数调用来定义临界区,在同一个时刻内,只有一个goroutine可以进入临界区,直到调用Unlock()函数后,其余的goroutine才可以进入临界区。
在Go中解决共享资源安全访问,更经常使用的使用通道chan。
利用通道共享数据
CSP(Communicating Sequential Process)模型提供一种多个进程公用的“管道(channel)”, 这个channel中存放的是一个个”任务”.
目前正流行的go语言中的goroutine就是参考的CSP模型,原始的CSP中channel里的任务都是当即执行的,而go语言为其增长了一个缓存,即任务能够先暂存起来,等待执行进程准备好了再逐个按顺序执行.
Go语言采用CSP消息传递模型。经过在goroutine之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。这里就须要用到通道chan这种特殊的数据类型。当一个资源须要在goroutine中共享时,chan在goroutine中间架起了一个通道。通道使用make来建立:
unbuffered := make(chan int) //建立无缓存通道,用于int类型数据共享
buffered := make(chan string,10)//建立有缓存通道,用于string类型数据共享
buffered<- "hello world" //向通道中写入数据
value:= <-buffered //从通道buffered中接受数据
通道用于放置某一种类型的数据。建立通道时指定通道的大小,将建立有缓存的通道。无缓存通道是一种同步通讯机制,它要求发送goroutine和接收goroutine都应该准备好,不然会进入阻塞。
无缓存的通道
无缓存通道是同步的——一个goroutine向channel写入消息的操做会一直阻塞,直到另外一个goroutine从通道中读取消息。反过来也是,一个goroutine从channel读取消息的操做会一直阻塞,直到另外一个goroutine向通道中写入消息。《Go in action》中关于无缓存通道的解释有一个很是棒的例子:网球比赛。在网球比赛中,两位选手老是处在如下两种状态之一:要么在等待接球,要么在把球打向对方。球的传递可看为通道中数据传递。下面这段代码使用通道模拟了这个过程:
package main import ( "fmt" "math/rand" "sync" "time" ) var wg sync.WaitGroup func player(name string, court chan int) { defer wg.Done() for { //若是通道关闭,那么选手胜利 ball, ok := <-court if !ok { fmt.Printf(" %s Won\n", name) return } n := rand.Intn(100) //随机几率使某个选手Miss if n%13 == 0 { fmt.Printf(" %s Missed\n", name) //关闭通道 close(court) return } fmt.Printf(" %s Hit %d\n", name, ball) ball++ //不然选手进行击球 court <- ball } } func main() { rand.Seed(time.Now().Unix()) //建立无缓存channel court := make(chan int) //等待两个goroutine都执行完 wg.Add(2) //选手1等待接球 go player("candy", court) //选手2等待接球 go player("luffic", court) //球进入球场(能够开始比赛了) court <- 1 wg.Wait() }
有缓存的通道
有缓存的通道是一种在被接收前能存储一个或者多个值的通道,它与无缓存通道的区别在于:无缓存的通道保证进行发送和接收的goroutine会在同一时间进行数据交换,有缓存的通道没有这种保证。有缓存通道让goroutine阻塞的条件为:通道中没有数据可读的时候,接收动做会被阻塞;通道中没有区域容纳更多数据时,发送动做阻塞。向已经关闭的通道中发送数据,会引起panic,可是goroutine依旧能从通道中接收数据,可是不能再向通道里发送数据。因此,发送端应该负责把通道关闭,而不是由接收端来关闭通道。
小结