小白一枚,最近在研究golang,记录本身学习过程当中的一些笔记,以及本身的理解。git
- go中协程的实现
- go中协程的sync同步锁
- go中信道channel
- go中的range
- go中的select切换协程
- go中带缓存的channel
- go中协程调度
原文的地址为:https://github.com/forthealll...github
欢迎stargolang
介绍go中的协程以前,首先看如下go中的defer函数,defer函数不是普通的函数,defer函数会在普通函数返回以后执行。defer函数中能够释放函数内部变量、关闭数据库链接等等操做,举例来讲:数据库
func print(){ fmt.Println(2); } func main() { defer print(); fmt.Println(1); }
上述的例子中先输出1后输出2,说明defer确实是在普通函数调用结束以后执行的。缓存
go中使用协程的方式来处理并发,协程能够理解成更小的线程,占用空间小且线程上下文切换的成本少。并发
能够再为具体的描述如下协程的好处,协程比线程更加轻量,使用4K栈的内存就能够建立它们,能够用很小的内存占用就能够处理大量的任务。函数
在go中,携程是经过go关键字来调用,从关键字能够看出,golang的一个十分重要的特色就是协程,有句话叫“协程在手,说go就go”。学习
下面咱们来看一个例子:spa
func printOne(){ fmt.Println(1); } func printTwo(){ fmt.Println(2); } func printThree(){ fmt.Println(3); } func main() { go printOne(); go printTwo(); go printThree(); }
执行上述的main函数,咱们发现并无像咱们想的那样输出有123的输出,缘由在于虽然协程是并发的,可是若是在协程调用前退出了调用协程的函数后,协程会随着程序的消亡而消亡。操作系统
所以咱们能够在main函数中,将主函数挂起,增长等待协程调用的事件。
func main() { go printOne(); go printTwo(); go printThree(); time.Sleep(5 * 1e9); }
这样会有相应的go关键字修饰的协程函数的调用。咱们来看分别执行3次的结果。
咱们发现由于协程是并发执行的,咱们没法肯定其调用的顺序,所以 每次的调用主函数的返回结果都是不肯定的。
从协程的上述例子中,咱们能够看出使用协程的时候必须还要考虑两个问题:
问题1,能够经过sync的同步锁来实现,问题2,go中提供了channel来实现不一样协程间的通讯。
go中sync包提供了2个锁,互斥锁sync.Mutex和读写锁sync.RWMutex.咱们用互斥锁来解决上述的同步问题,改写上述的例子:
func printOne(m *sync.Mutex){ m.Lock(); fmt.Println(1); defer m.Unlock(); } func printTwo(m *sync.Mutex){ m.Lock(); fmt.Println(2); defer m.Unlock(); } func printThree(m *sync.Mutex){ m.Lock(); fmt.Println(3); defer m.Unlock(); } func main() { m:= new(sync.Mutex); go printOne(m); go printTwo(m); go printThree(m); time.Sleep(5 * 1e9); }
经过互斥锁,能够发现每次运行,确实都依次输出了1,2,3
go中有一种特殊的类型通道channel,能够经过channel来发送类型化的数据,实如今协程之间的通讯,经过通道的通讯方式也保证了同步性。
channel的声明方式很简单:
var ch1 chan string ch1 = make(chan string)
咱们用ch表示通道,通道的符号包括了流向通道(发送): ch <- int1 和从通道流出(接收) int2 = <- ch。
同时go中也支持声明单向通道:
var ch1 chan int //普通的channel var ch2 chan <- int //只用于写int数据 var ch3 <- chan int //只用于读int数据
上述定义的都是不带缓存区,或者说长度为1的channel,这种channel的特色就是:
一旦有数据被放入channel,那么该数据必须被取走才能让另外一条数据放入,这就是同步的channel,channel的发送者和接受者在同一时间只交流一条数据,而后必须等待另外一边完成相应的发送和接受动做。
咱们仍是用上述的输出123的例子,用同步channel来实现同步的输出。
func printOne(cs chan int){ fmt.Println(1); cs <- 1 } func printTwo(cs chan int){ <-cs fmt.Println(2); defer close(cs); } func main() { cs := make(chan int); go printOne(cs); go printTwo(cs); time.Sleep(5 * 1e9); }
上述的例子中会依次输出12,这样咱们经过同步channel的方式实现了同步的输出。
咱们前面讲到用为了等待go协程执行完成,咱们在main函数中用time.sleep来挂起主函数,其实main函数自己也能够当作一个协程,若是使用channel,就不用在main函数中用time.sleep来挂起。
咱们改写上述的例子:
func printOne(cs chan int){ fmt.Println(1); cs <- 1 } func main() { cs := make(chan int); go printOne(cs); <-cs; close(cs); }
上述的例子中,会输出 1 ,咱们并无在主函数中经过time.sleep的方式来挂起,转而用一个等待写入的channel来代替。
注意:通道能够被显式的关闭,当须要告诉接受者不会种子提供新的值的时候,就须要关闭通道。
上面咱们也讲到要及时的关闭channel,可是持续的访问数据源并检查channel是否已经关闭,并不高效。go中提供了range关键字。
range关键字在使用channel的时候,会自动等待channel的动做一直到channel关闭。通俗点将就是能够channel能够自动开关。
一样的来举例:
func input(cs chan int,count int){ for i:=1;i<=count;i++ { cs <- i } } func output(cs chan int){ for s:= range cs { fmt.Println(s); } } func main() { cs := make(chan int); go input(cs,5); go output(cs); time.Sleep(3*1e9) }
上述的例子会依次的输出1,2,3,4,5. 经过使用range关键字,当channel被关闭时,接受者的for循环也就自动中止了。
从不一样的并发执行过程当中获取值能够经过关键字select来完成,它和switch控制语句很是类似,也被称为通讯开关。
首先要明确select作了什么??
select中存在着一种轮询机制,select监听进入通道的数据,也能够是通道发送值的时候,监听到相应的行为后就执行case里面的操做。
select的声明:
select { case u:= <- ch1: ... case v:= <- ch2; ... }
一样的来看一下具体使用select的例子:
func channel1(cs chan int,count int){ for i:=1;i<=count;i++ { cs <- i } } func channel2(cs chan int,count int){ for i:=1;i<=count;i++ { cs <- i } } func selectTest(cs1 ,cs2 chan int){ for i:=1;i<10;i++ { select { case u:=<-cs1: fmt.Println(u); case v:=<-cs2: fmt.Println(v); } } } func main() { cs1 := make(chan int); cs2 := make(chan int); go channel1(cs1,5); go channel2(cs2,3); go selectTest(cs1,cs2); time.Sleep(3*1e9) } 输出结果为:1,2,1,2,3,3,4,5 总共8个数据。且由于没有作同步控制,所以运行几回后的输出结果是不相同的。
前面讲到的都是不带缓存的channel或者说长度为1的channel,实际上channel也是能够带缓存的,咱们能够在声明的时候执行channel的长度。
ch = make(chan string,3)
好比上述的例子中,指定了ch这个channel的长度为3,长度不为1的channel,就能够称之为带缓存的channel.
带缓存的channel能够连续写入,直到长度占满为止。
ch <- 1 ch <- 2 ch <- 3
讲到并发,就要提到go中的协程调度。go中的runtime包,提供了调度器的功能。runtime包提供了如下几个方法:
对于多核CPU的机器,go能够显示的指定编译器将go的协程调度到多个CPU上运行
import "runtime" ... cpuNum:=runtime.NumCPU; runtime.GOMAXPROCS(cpuNum)
来聊聊GO中的调度原理,首先定义如下模型的概念:
M:内核中的线程的数目
G:go中的协程,并发的最小单元,在go中经过go关键字来建立
P:处理器,即协程G的上下文,每一个P会维护一个本地的协程队列。
接着来看解释GO中协程调度的经典图:
咱们来解释上图: