Golang入门(4):并发

摘要

并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得越来越重要。Web服务器会一次处理成千上万的请求,这也是并发的必要性之一。Golang的并发控制比起Java来说,简单了不少。在Golang中,没有多线程这一说法,只有协程,而新建一个协程,仅仅只需要使用go关键字。而且,与Java不同的是,在Golang中不以共享内存的方式来通信,而是以通过通信的方式来共享内存。这方面的内容也比较简单。

1 线程与协程

在Golang中,并发是以协程的方式实现的。

在Java中,我们常常提到线程池,多线程这些概念。然而,在Golang中的协程,和这些是不一样的。所以在本文中,先对这几个概念进行区分。

简单来说,进程和线程是由操作系统进行调度的,协程是对内核透明,由程序自己调度的。不仅如此,Golang的协程所占用的内存空间极小,也就是说,协程更加的轻量。此外,协程的切换一般由程序员在代码中显式控制,而不是交给操作系统去调度。它避免了上下文切换时的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂。

至于别的,本文不进行深入的研究,本文的基调还是以入门为主,即怎么去用。

2 goroutine

简单来说,我们所编写的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如何编写并发的小程序:

package mainimport ("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连接。然后,当有连接过来的时候,每隔一秒将当前的时间打印在屏幕上。

在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关键字。

3 channel

注意,在Golang的并发中有一项很重要的特性,不要以共享内存的方式来通信,相反,要通过通信来共享内存。

这里说到的通信方式,指得就是channel,信道。

Channel是Go中的一个核心类型,我们可以把理解为是一种指定了大小和容量的管道。我们可以在这个管道的一边放入数据,在另一半拿出数据。举个简单的例子:

package mainimport "fmt"func main() {messages := make(chan string)go func() { messages <- "ping" }()msg := <-messagesfmt.Println(msg)
}

在这里需要说明几点:

  • 信道需要使用make的方式创建,除了能够指定类型,还能在第二个参数指定容量,否则默认为1,也就是说这是一个同步信道
  • 消息的传递和获取必须成对出现,传数据用channel <- data,取数据用<- channel。
  • 信道是会阻塞的,而且不管传还是取,必阻塞,直到另外的goroutine传或者取为止。
  • 对于阻塞,可以理解为是一个管道中已经有了东西,那么只有管道为空了,才能继续工作

4 range

对于上面提到的信道操作,存在这么几个问题:

  • 应该何时停止等待数据?
  • 还会有更多的数据么,还是所有内容都已经传输完成?
  • 我应该继续等待还是该做别的了?

当然,我们可以选择不断检查信道,直到他关闭为止。

但是我们有更加优雅的解决方案。使用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发现信道被关闭,于是结束。这也就实现了接收器不知道具体需要接收多少个蛋糕的情况下,能够自动结束的功能。

5 select

select关键字用在有多个信道的情况下。

他的目的是为了提高系统的效率,而不至于在某一个信道阻塞的情况下,不知道该干什么。

select中会有case代码块,用于发送或接收数据。语法如下:

select {
case i := <-c://...
case ...
default://...
}

注意,每一个case,必须是一个信道IO指令,default命令块不是必须。

规律如下:

  • 如果任意一个case代码块准备好发送或接收,执行对应内容
  • 如果多余一个case代码块准备好发送或接收,随机选取一个并执行对应内容
  • 如果任何一个case代码块都没有准备好,等待
  • 如果有default代码块,并且没有任何case代码块准备好,执行default代码块对应内容

我们还是以上面做蛋糕为例,但是这次可以同时做草莓味和巧克力味的蛋糕了:

package mainimport ("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, falsefor {//如果两个信道都关闭了,说明制作完成,结束程序if (strbry_closed && choco_closed) { return }fmt.Println("等待新蛋糕 ...")select {case cakeName, strbry_ok := <-strbry_cs:if (!strbry_ok) {strbry_closed = truefmt.Println(" ... 草莓信道关闭")} else {fmt.Println("在草莓信道中收到一个新蛋糕。名为:", cakeName)}case cakeName, choco_ok := <-choco_cs:if (!choco_ok) {choco_closed = truefmt.Println(" ... 巧克力信道关闭")} else {fmt.Println("在巧克力信道中收到一个新蛋糕。名为:", cakeName)}}}
}func main() {strbry_cs := make(chan string)choco_cs := make(chan string)//two cake makersgo makeCakeAndSend(choco_cs, "巧克力", 3)  //制作3个巧克力蛋糕,然后发送go makeCakeAndSend(strbry_cs, "草莓", 3)  //制作3个草莓蛋糕,然后发送//one cake receiver and packergo receiveCakeAndPack(strbry_cs, choco_cs)  //收获//查看结果time.Sleep(2 * 1e9)
}

在这里,因为我们是不知道哪种口味的蛋糕已经被制作完成的,所以我们使用了select。只要这个case被激活了,那么就会完成后面的代码。也就是说,当某种口味的蛋糕被制作完成之后,就会被收取。

注意,我们这里使用的多个返回值

case cakeName, strbry_ok := <- strbry_cs

第二个返回值是一个bool类型,当其为false时说明channel被关闭了。如果是true,说明有一个值被成功传递了。

我们使用可以这个值来判断是否应该停止等待。

写在最后

至此,《Golang入门》系列已经结束。谢谢你能够看到这里。

Golang入门(4):并发相关推荐

  1. golang中的并发竞争态

    golang程序中并发会引起并发竞争,一起没理解,为什么说两个goroutine访问共享资源会引发竞争态,我的理解如果只使用一个逻辑处理器本质上不就是同一时间只有一个goroutine在跑吗,为什么会 ...

  2. golang开发工程师-第一步:golang入门基础教学

    golang入门基础教学 前言 一.golang的优势何在? 二.goland破解教程 三.goland的使用教程 四.一个简单的go代码 五.变量的声明和赋值 六.数据类型的基本介绍 七.访问权限[ ...

  3. Golang的高并发

    Golang的高并发 Golang的调度器有三个核心的元素: 物理处理器 逻辑处理器 goroutine 物理处理器更接近cpu核的概念,主要包括 调度线程来运行. 分配逻辑处理器的基础, 每个物理处 ...

  4. golang入门实战(二)

    golang入门实战 github 接上篇 接口数据时间格式 token校验中间件 多平台打包 未完待续 github 项目完整代码–github 接上篇 golang入门实战(一) 接口数据时间格式 ...

  5. 【Golang入门】二、Go语言快速开发

    需求:开发一个hello.go程序,要求输出"hello world". 采用VScode进行Golang的开发,因此这一系列博客的代码均在VScode编译器上编译实现. 这里我们 ...

  6. 进一步认识golang中的并发

    如果你成天与编程为伍,那么并发这个名词对你而言一定特别耳熟.需要并发的场景太多了,例如一个聊天程序,如果你想让这个聊天程序能够同时接收信息和发送信息,就一定会用到并发,无论那是什么样的并发. 并发的意 ...

  7. Golang适合高并发场景的原因分析

    典型的两个现实案例: 我们先看两个用Go做消息推送的案例实际处理能力. 360消息推送的数据: 16台机器,标配:24个硬件线程,64GB内存  Linux Kernel 2.6.32 x86_64  ...

  8. Golang 入门笔记(一)

    初识 Go 语言 本章主要介绍了以下内容: Go 语言的特性: (2)使用 Go 语言的开源项目: (3)安装 Go 语言开发包和搭建其开发环境. 目录 文章目录 初识 Go 语言 目录 s1 Go语 ...

  9. Golang入门(3):一天学完GO的进阶语法

    摘要 在上一篇文章中,我们聊了聊Golang中的一些基础的语法,如变量的定义.条件语句.循环语句等等.他们和其他语言很相似,我们只需要看一看它们之间的区别,就差不多可以掌握了,所以作者称它们为&quo ...

最新文章

  1. szu 寒训个人复习第一天 线段树入门单点修改,区间修改,以及线段树的扩展运用[线段树+dp][区间最大公约数]
  2. js php 实现日历签到_Js 实现每日签到打卡轨迹功能。
  3. 如何成为SEO专家(10步指南)
  4. ImageMagick远程代码执行漏洞CVE-2016-8707 绿盟科技发布安全威胁通告
  5. 怎样成为php高手,怎么成为php高手?如何自学成为php高手?优秀的PHP开发者是怎样炼成的?-PHP教程-基础篇-php语法基础--创业的风,吹向了年轻之长藤个人博客网站...
  6. Linux之shell脚本遍历文件夹下所有文件
  7. 八十、React中的容器组件和无状态组件
  8. sha-1算法的实现 c语言,SHA-1算法的C语言实现
  9. 蔡高厅老师 - 高等数学阅读笔记 - 04 - 函数的连续性(18、19、20、21)
  10. Ubuntu 14.04 安装 MongoDB
  11. JZOJ5146:港湾
  12. 如何在 Mac 中对文档进行签名?
  13. mvc 调试 f12 浏览器闪退
  14. lisp取消选集选中状态_为什么对话框创建后是隐藏状态的
  15. windows 2003与windows 2008区别之AD DS篇
  16. kafka安装_kafka 安装部署教程
  17. 做一个iframe的弹出框
  18. 关于抓包软件Fiddler的简单汉化
  19. (CVPR 2020) RandLA-Net: Efficient Semantic Segmentation of Large-Scale Point Clouds
  20. cmake/gcc:strip缩减程序体积

热门文章

  1. [轉]VS2010 UML类图生成代码
  2. Oracle RAC CRS-0184 --Cannot communicate with the CRS daemon
  3. Another test
  4. 微软的报表工具 SQL Server 2000 Reporting Services 评估版
  5. 诗和远方:无题(四十九)
  6. clickhouse安装教程
  7. Java中常见的排序算法代码演示
  8. 栈的应用——表达式求值(双栈)
  9. mysql 单数据库设置编码,mysql数据库编码设置
  10. springmvc进不到controller_Spring、SpringMVC、MyBatis的整合