并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得愈来愈重要。Web服务器会一次处理成千上万的请求,这也是并发的必要性之一。Golang的并发控制比起Java来讲,简单了很多。在Golang中,没有多线程这一说法,只有协程,而新建一个协程,仅仅只须要使用go
关键字。并且,与Java不一样的是,在Golang中不以共享内存的方式来通讯,而是以经过通讯的方式来共享内存。这方面的内容也比较简单。html
在Golang中,并发是以协程的方式实现的。程序员
在Java中,咱们经常提到线程池,多线程这些概念。然而,在Golang中的协程,和这些是不同的。因此在本文中,先对这几个概念进行区分。编程
简单来讲,进程和线程是由操做系统进行调度的,协程是对内核透明,由程序本身调度的。不只如此,Golang的协程所占用的内存空间极小,也就是说,协程更加的轻量。此外,协程的切换通常由程序员在代码中显式控制,而不是交给操做系统去调度。它避免了上下文切换时的额外耗费,兼顾了多线程的优势,简化了高并发程序的复杂。小程序
至于别的,本文不进行深刻的研究,本文的基调仍是以入门为主,即怎么去用。服务器
简单来讲,咱们所编写的Golang源代码所有都跑在goroutine中。网络
咱们只须要使用go
关键字,就能够启动一个goroutine。多线程
package main import "fmt" func f(msg string) { fmt.Println(msg) } func main(){ go f("hello goroutine") }
至于其他的事情,就交给Golang的runtime了,Go的runtime负责对goroutine进行调度。简单的来说,调度就是决定哪一个goroutine将得到资源开始执行、哪一个goroutine应该中止执行让出资源、哪一个goroutine应该被唤醒恢复执行等。并发
咱们下面写个小例子,来看看Golang如何编写并发的小程序:curl
package main import ( "io" "log" "net" "time" ) func main() { listener, err := net.Listen("tcp", "localhost:8000") if err != nil { log.Fatal(err) } for { conn, err := listener.Accept() if err != nil { log.Print(err) // 假设出现了错误 continue } handleConn(conn) // 处理链接 } } func handleConn(c net.Conn) { defer c.Close() for { _, err := io.WriteString(c, time.Now().Format("15:04:05\n")) if err != nil { return // 链接关闭,则中止执行 } time.Sleep(1 * time.Second) } }
简单解释一下,这个来自于这里的小例子中,咱们监听了本地8000端口的TCP链接。而后,当有链接过来的时候,每隔一秒将当前的时间打印在屏幕上。tcp
在Windows中,咱们可使用curl
命令来测试:
curl 127.0.0.1:8000
效果以下:
可是问题来了,若是咱们再打开一个CMD窗口,去创建一个TCP链接,是失败的。除非将原来的那个链接中断,Golang才能接受新的链接。否则,新的链接将一直被阻塞。
能够看到,若是同时启动两个链接,只有一个链接能够提供打印时间的服务,另外一个链接将被阻塞:
这时,将第一个链接中断,则第二个链接才能够进行打印:
在这个时候,咱们只须要在调用handleConn(conn)
这个函数以前,加上go
的关键字,就能够实现并发了。
部分代码以下:
for { conn, err := listener.Accept() if err != nil { log.Print(err) // 假设出现了错误 continue } go handleConn(conn) // 处理链接 }
随后,咱们就能够处理多个链接了:
因此,在Golang中实现并发,就是这么的简单。咱们须要作的,就是在调用须要建立协程的函数前面,加上go
关键字。
注意,在Golang的并发中有一项很重要的特性,不要以共享内存的方式来通讯,相反,要经过通讯来共享内存。
这里说到的通讯方式,指得就是channel,信道。
Channel是Go中的一个核心类型,咱们能够把理解为是一种指定了大小和容量的管道。咱们能够在这个管道的一边放入数据,在另外一半拿出数据。举个简单的例子:
package main import "fmt" func main() { messages := make(chan string) go func() { messages <- "ping" }() msg := <-messages fmt.Println(msg) }
在这里须要说明几点:
channel <- data
,取数据用<- channel
。对于上面提到的信道操做,存在这么几个问题:
固然,咱们能够选择不断检查信道,直到他关闭为止。
可是咱们有更加优雅的解决方案。使用range
关键字,使用在channel上时,会自动等待channel的动做一直到channel被关闭。下面来看一个小例子,这个例子来源于简书:
package main import ( "fmt" "time" "strconv" ) func makeCakeAndSend(cs chan string, count int) { for i := 1; i <= count; i++ { cakeName := "Strawberry Cake " + strconv.Itoa(i) cs <- cakeName //将蛋糕送入cs } close(cs) } func receiveCakeAndPack(cs chan string) { for s := range cs { fmt.Println("Packing received cake: ", s) } } func main() { cs := make(chan string) go makeCakeAndSend(cs, 5) go receiveCakeAndPack(cs) //让程序不会立刻结束,以达到查看输出结果的目的 time.Sleep(3 * 1e9) }
在这里,咱们定义了一个同步信道。
在制做蛋糕的过程当中,咱们使用了一个for循环,不断的将蛋糕送入cs
中。
注意,这里由于是同步信道,因此并非将五个蛋糕所有制做完,再所有一块儿接收的,而是制做一个,接受一个。
最后,咱们关闭这个信道,随后range
发现信道被关闭,因而结束。这也就实现了接收器不知道具体须要接收多少个蛋糕的状况下,可以自动结束的功能。
select
关键字用在有多个信道的状况下。
他的目的是为了提升系统的效率,而不至于在某一个信道阻塞的状况下,不知道该干什么。
select中会有case代码块,用于发送或接收数据。语法以下:
select { case i := <-c: //... case ... default: //... }
注意,每个case,必须是一个信道IO指令,default命令块不是必须。
规律以下:
咱们仍是以上面作蛋糕为例,可是此次能够同时作草莓味和巧克力味的蛋糕了:
package main import ( "fmt" "strconv" "time" ) func makeCakeAndSend(cs chan string, flavor string, count int) { for i := 1; i <= count; i++ { cakeName := flavor + "蛋糕 " + strconv.Itoa(i) cs <- cakeName //send a strawberry cake } close(cs) } func receiveCakeAndPack(strbry_cs chan string, choco_cs chan string) { strbry_closed, choco_closed := false, false for { //若是两个信道都关闭了,说明制做完成,结束程序 if (strbry_closed && choco_closed) { return } fmt.Println("等待新蛋糕 ...") select { case cakeName, strbry_ok := <-strbry_cs: if (!strbry_ok) { strbry_closed = true fmt.Println(" ... 草莓信道关闭") } else { fmt.Println("在草莓信道中收到一个新蛋糕。名为:", cakeName) } case cakeName, choco_ok := <-choco_cs: if (!choco_ok) { choco_closed = true fmt.Println(" ... 巧克力信道关闭") } else { fmt.Println("在巧克力信道中收到一个新蛋糕。名为:", cakeName) } } } } func main() { strbry_cs := make(chan string) choco_cs := make(chan string) //two cake makers go makeCakeAndSend(choco_cs, "巧克力", 3) //制做3个巧克力蛋糕,而后发送 go makeCakeAndSend(strbry_cs, "草莓", 3) //制做3个草莓蛋糕,而后发送 //one cake receiver and packer go receiveCakeAndPack(strbry_cs, choco_cs) //收获 //查看结果 time.Sleep(2 * 1e9) }
在这里,由于咱们是不知道哪一种口味的蛋糕已经被制做完成的,因此咱们使用了select
。只要这个case被激活了,那么就会完成后面的代码。也就是说,当某种口味的蛋糕被制做完成以后,就会被收取。
注意,咱们这里使用的多个返回值
case cakeName, strbry_ok := <- strbry_cs
第二个返回值是一个bool类型,当其为false时说明channel被关闭了。若是是true,说明有一个值被成功传递了。
咱们使用能够这个值来判断是否应该中止等待。
至此,《Golang入门》系列已经结束。
谢谢你可以看到这里。
做者大概花了一周的时间,学习Golang,而且将本身学习的内容以博客的形式分享出来,但愿可以给你们一些帮助。
固然了,在这期间必定会有不少疏漏,但愿你们能够指正。其次,也不少地方没有深究,这是由于做者这个系列的文章只是想先对Golang有一个总体的认识,至于其余的,在用到的时候,再深刻进行挖掘。
日后的内容,做者可能会考虑Golang网络编程方面,也可能考虑Golang源码方面,或者说Golang的各类包系列,这个等做者研究研究,再与你们进行分享。再远一点,像Redis相关,MySQL相关,系统底层如操做系统,计网等,也都会进行介绍。
扯远了,flag立了不少(笑)
那么接下来,也请各位多多指教。
谢谢啦~
PS:若是有其余的问题,也能够在公众号找到做者。而且,全部文章第一时间会在公众号更新,欢迎来找做者玩~