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 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连接。然后,当有连接过来的时候,每隔一秒将当前的时间打印在屏幕上。
在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 main
import "fmt"
func main() {
messages := make(chan string)
go func() { messages <- "ping" }()
msg := <-messages
fmt.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 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:如果有其他的问题,也可以在公众号找到作者。并且,所有文章第一时间会在公众号更新,欢迎来找作者玩~

Golang入门(4):并发的更多相关文章
- Golang 入门 : 理解并发与并行
Golang 的语法和运行时直接内置了对并发的支持.Golang 里的并发指的是能让某个函数独立于其他函数运行的能力.当一个函数创建为 goroutine 时,Golang 会将其视为一个独立的工作单 ...
- Golang 入门 : 竞争条件
笔者在前文<Golang 入门 : 理解并发与并行>和<Golang 入门 : goroutine(协程)>中介绍了 Golang 对并发的原生支持以及 goroutine 的 ...
- Golang 入门 : goroutine(协程)
在操作系统中,执行体是个抽象的概念.与之对应的实体有进程.线程以及协程(coroutine).协程也叫轻量级的线程,与传统的进程和线程相比,协程的最大特点是 "轻"!可以轻松创建上 ...
- Golang 入门系列(十七)几个常见的并发模型——生产者消费者模型
前面已经讲过很多Golang系列知识,包括并发,锁等内容,感兴趣的可以看看以前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863.ht ...
- Java程序员的Golang入门指南(下)
Java程序员的Golang入门指南(下) 4.高级特性 上面介绍的只是Golang的基本语法和特性,尽管像控制语句的条件不用圆括号.函数多返回值.switch-case默认break.函数闭包.集合 ...
- Golang 入门 : channel(通道)
笔者在<Golang 入门 : 竞争条件>一文中介绍了 Golang 并发编程中需要面对的竞争条件.本文我们就介绍如何使用 Golang 提供的 channel(通道) 消除竞争条件. C ...
- Golang入门(3):一天学完GO的进阶语法
摘要 在上一篇文章中,我们聊了聊Golang中的一些基础的语法,如变量的定义.条件语句.循环语句等等.他们和其他语言很相似,我们只需要看一看它们之间的区别,就差不多可以掌握了,所以作者称它们为&quo ...
- Golang入门(2):一天学完GO的基本语法
摘要 在配置好环境之后,要研究的就是这个语言的语法了.在这篇文章中,作者希望可以简单的介绍一下Golang的各种语法,并与C和Java作一些简单的对比以加深记忆.因为这篇文章只是入门Golang的第二 ...
- Golang入门(1):安装与配置环境变量的意义
摘要 在几年前学习Java的时候,环境的配置就会劝退一部分的初学者.而对于Golang来说,也需要从环境的配置开始学起.这一篇文章将从如何安装Golang开始讲起,随后将会提到Golang中的环境变量 ...
随机推荐
- 致远·面向人工智能-逐浪CMS v8.1.2全面发布[全球首个基于dotNET core3.0的中文CMS]
原文:https://www.z01.com/down/3484.shtml 再远, 我都不会停息, 因为技术而生, 因为技术而强, 这是逐浪软件的命与根! 全新打造, 三百多项超级功能, 助你十分钟 ...
- php里面的一些面试经典的函数
<?php /* 这是一个多线程的读取解决的函数 @param1 $fle 传入要读取的文件名 */ function filelock($fle){ $fp=fopen($fls,'w+'); ...
- Python xlsxwriter模块
1.简介: xlsxWriter支持多种excle功能:与excel完美兼容:写大文件,速度快且只占用很小的内存空间不支持读或者改现有的excel文件 2.安装: pip install xlsxwr ...
- Python 3.9 新特性:任意表达式可作为装饰器!
一个月前(2月20日),一则新的 PEP 没有受到任何阻碍就被官方采纳了,这么快的速度,似乎并不多见. 然而,更为高效率的是,仅在半个月内,它的实现就被合入了代码仓.也就是说,我们最快有望在 3 天后 ...
- foobox,基于foobar2000汉化版的CUI配置整合版
名 称:foobox 作 者:dreamawake 发布博客:https://www.cnblogs.com/foobox/ GitHub: https://github.com/dream7180/ ...
- 【Weiss】【第03章】练习3.13:桶排序
[练习3.13] 利用社会安全号码对学生记录构成的数组排序.编写一个程序进行这件工作,使用具有1000个桶的基数排序并且分三趟进行. Answer: 首先,对社会安全号码不了解的就把它当成一个不超过9 ...
- 【TIJ4】第四章全部习题
第四章 没啥好说的...... 4.1 package ex0401; //[4.1]写一个程序打印从1到100的值 public class PrintOneToHundred { public s ...
- [组件封装]微信小程序-底部弹框
描述 模仿ios浏览器底部弹框效果. 遮罩层淡入淡出,弹框高度根据内容自适应. 效果 源码 popup-bottom.wxml <!-- popup-bottom.wxml --> < ...
- [组件封装]微信小程序-日历
描述 切换月份, 当天文案为今天, 日期背景变色, 日期红点标识, 点击选中日期. 效果 源码 calendar.wxml <view class="component"&g ...
- C 2015年真题
1.写出程序输出结果 void main() { char p[10]="abc"; char q[]="xyz"; int i,j; i=0; while(* ...