关键词:
Goroutine
Go语言从语言层面上就支持了并发,这与其他语言大不一样。Go语言中有个概念叫做goroutine,这类似我们熟知的线程,但是更轻。
进程、线程、协程
进程和线程
进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行。
所以程序的类型可以分为以下几种:
- 一个进程,它只有一个线程,就是单线程程序
- 一个进程,它又多个线程,就是多线程程序
- 一个进程,它可能还会fork多个子进程,就是多进程程序
并发和并行的区别
- 多线程程序在单核的cou上运行,这是并发(concurrency)。
- 多线程程序在多个核的cpu上运行(真正的同时运行),这才是并行(parallelism)。
并发,在微观上,任意时刻只有一个程序在运行。因为线程已经是CPU调度的最小单元,一个CPU一次只能处理一个线程。但是宏观上这些程序时同时在那里执行的,所以这个只是并发。
所以在python里,貌似讲的都是高并发,似乎没听过并行的概念。
协程和线程
协程,独一的栈空间,共享堆空间,调度由用户自己控制。本质上类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程,一个线程上可以跑多个协程,协程是轻量级的线程。
goroutine 调度模型
Go的调度器模型:G-P-M模型。
- G代表goroutine,它通过go关键字调用runtime.newProc创建。
- P代表processer,可以理解为上下文。
- M表示machine,可以理解为操作系统的线程。
设置Golang运行的cpu核数
设置当前的程序运行在多少核上,下面的例子是获取CPU的核数,然后运行在所有核上:
package main
import (
"fmt"
"runtime"
)
func main()
num := runtime.NumCPU()
runtim.GOMAXPROCS(num)
fmt.Println(num)
上面P的数目就是这里GOMAXPROCS设置的数目,通常设置为CPU核数。
1.8版本以上的Golang,是不需要做上面的设置的,默认就是运行在所有的核上。当然还是可以设置一下,比如限制只能使用多少核。
goroutine的示例:
package main
import (
"fmt"
"time"
)
func example()
var i int
for
fmt.Println(i)
i++
time.Sleep(time.Millisecond * 30)
func main()
go example() // 起一个goroutine
var j int
for j > -100
fmt.Println(j)
j--
time.Sleep(time.Millisecond * 100)
fmt.Println("运行结束")
Channel
不同goroutine之间要进行通讯,有下面2种方法:
- 全局变量和锁同步
- Channel
先讲管道(channel),然后讲 goroutine 和 channel 结合的一些用法。
这篇的channel可以参考下:
https://www.jianshu.com/p/24ede9e90490
全局变量的实现示例
在下面的例子里定义了变量 m 来实现goroutine之间的通讯:
package main
import (
"fmt"
"time"
"sync"
)
var (
m = make(map[int]uint64)
lock sync.Mutex
)
type task struct
n int
func calc(t *task)
var res uint64
res = 1
for i := 1; i <= t.n; i++
res *= uint64(i)
lock.Lock()
m[t.n] = res // 变量m用来存放结果,这样主线程里就能拿到m的值,操作要加锁
lock.Unlock()
func main()
for i := 0; i < 100; i++
t := &taski
go calc(t)
for j := 0; j < 10; j++
fmt.Printf("
已运行%d秒", j)
time.Sleep(time.Second)
fmt.Println("
运行完毕,输出结果:")
lock.Lock()
for k, v := range m
if v != 0
fmt.Printf("%d! = %v
", k, v)
lock.Unlock()
channel 概念
channel的概念如下:
- 类型Unix中的管道(Pipe)
- 先进先出
- 线程安全,多个goroutine同时访问,不需要加锁
- channel是有类型的,一个整数的channel只能存放整数
channel 声明
var 变量名 chan 类型
var test1 chan int
var test2 chan string
var tesr3 chan map[string]string
var test4 chan stu
var test5 chan *stu
只是声明还不够,使用前还要make,分配内存空间:
package main
import "fmt"
func main()
var intChan chan int // 声明
intChan = make(chan int, 10) // 初始化,长度是10
intChan <- 10 // 存入管道
n := <- intChan // 取出
fmt.Println(n)
定义信号(空结构体)
有一些场景中,一些 goroutine 需要一直循环处理信息,直到收到 quit 信号。作为信号,只需要随便传点什么,并不关注具体的值。那么可以选择使用空结构体,像下面这样定义了2个信号:
msgCh := make(chan struct)
quitCh := make(chan struct)
// 传信号的方法
msgCh <- struct // 前面的 struct 是变量的类型,后面的 则是做初始化传入空值生成实例
quitCh <- struct
通过channel实现通讯
起一个goroutine往管道里存,再起一个goroutine从管道里把数据取出:
package main
import (
"fmt"
"time"
)
func write(ch chan int)
var i int
for
ch <- i
i ++
time.Sleep(time.Millisecond)
func read(ch chan int)
for
b := <- ch
fmt.Println(b)
func main()
intChan := make(chan int, 10)
go write(intChan)
go read(intChan)
time.Sleep(time.Second * 5)
channel 的类型和阻塞
channel 分为不带缓存的 channel 和带缓存的 channel。
channel 一定要初始化后才能进行读写操作,否则会永久阻塞。这个不是这里要讲的重点,顺便带一下。
无缓存的channle
初始化make的时候不传入第二个参数设置容量就是:
ch := make(chan int)
从无缓存的 channel 中读取消息会阻塞,直到有 goroutine 向该 channel 中发送消息;同理,向无缓存的 channel 中发送消息也会阻塞,直到有 goroutine 从 channel 中读取消息。
有缓存的 channel
有缓存的 channel 的声明方式为指定 make 函数的第二个参数,该参数为 channel 缓存的容量:
ch := make(chan int, 10)
有缓存的 channel 类似一个阻塞队列(采用环形数组实现)。当缓存未满时,向 channel 中发送消息时不会阻塞,当缓存满时,发送操作将被阻塞,直到有其他 goroutine 从中读取消息;
相应的,当 channel 中消息不为空时,读取消息不会出现阻塞,当 channel 为空时,读取操作会造成阻塞,直到有 goroutine 向 channel 中写入消息。
缓冲区的大小
通过 len 函数可以获得 chan 中的元素个数,通过 cap 函数可以得到 channel 的缓冲区长度。
无缓存和缓冲区是1的差别
无缓存的 channel 的 len和cap 始终都是0。
通过无缓存的 channel 进行通信时,接收者收到数据 happens before 发送者 goroutine 唤醒
上面这句不好理解,不过可以先看下现象。
下面的这2行函数会报错,说是死锁。但是如果设置了 channel 的容量哪怕是1,就不会报错的:
func main()
ch := make(chan int)
ch <- 1
虽然容量1的channel也只能存1个数,但是无缓冲区的channel似乎1个数都存不了,除非马上能取走:
func main()
ch := make(chan int, 1)
// 要起一个goroutine可以马上接收channel里的数据
go func ()
fmt.Println(<- ch)
()
ch <- 1
time.Sleep(time.Second) // 要给goroutine执行完成的时间
小结:无缓存的channel需要有一个goroutine可以把channel里的数据马上取走。
channel之间的同步
在学习关闭channel之前,先看下下面的例子。由于没有关闭channel,是会有问题的,不过例子里都解决了。先看下不用关闭channel可以怎么搞,然后再接着看关闭channel的用法:
package main
import (
"time"
"fmt"
)
func calc(taskChan chan int, resChan chan int)
for v := range taskChan
// 判断是不是素数
flag := true
for i := 2; i < v; i++
if v % i == 0
flag = false
break
if flag
resChan <- v
func main()
intChan := make(chan int, 1000)
// 这个也是个goroutine
go func()
for i := 2; i < 100000; i++
intChan <- i
() // 管道满了之后,这个匿名函数会阻塞,但是不影响程序继续往下走
resultChan := make(chan int, 1000)
// 同时起8个goroutine
for i := 0; i < 8; i++
go calc(intChan, resultChan)
// 再起一个取结果的goroutine,不阻塞主线程
go func()
for v := range resultChan
fmt.Println("素数:", v)
()
// 给上面的匿名函数几秒钟来输出结果
time.Sleep(time.Second * 5)
上面的例子里用了2个匿名函数,也都是起的goroutine。如果是在主线程里直接for循环的话,那个for循环就会变成死锁,程序不会自己往下走。所以运行在goroutine里的死循环,在主线程退出后也就结束了,不会有问题。后一个匿名函数是对channel的进行遍历,channel取空后,会进入阻塞,如果是运行在主线程里的话也会形成死锁。
range 遍历
channel 也可以使用 range 取值,并且会一直从 channel 中读取数据,直到有 goroutine 对改 channel 执行 close 操作,循环才会结束。
关闭 channel
golang 提供了内置的 close 函数对 channel 进行关闭操作:
ch := make(chan int)
close(ch)
关于 channel 的关闭,有以下的特点:
- 关闭一个未初始化(nil) 的 channel 会产生 panic
- 重复关闭同一个 channel 会产生 panic
- 向一个已关闭的 channel 中发送消息会产生 panic
- 可以从已关闭的 channel 里继续读取消息,若消息均已读出,则会读到类型的零值。从一个已关闭的 channel 中读取消息不会阻塞,并且会返回一个为 false 的 ok-idiom,可以用它来判断 channel 是否关闭
- 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息
有2种方式可以把管道里的数据都取出来,但是都需要把管道关闭:
- 判断管道已关闭并且取完了
- 遍历管道
关闭channel然后读取的示例:
package main
import "fmt"
func main()
var ch chan int
ch = make(chan int, 5)
for i := 0; i < 5; i++
ch <- i
close(ch)
for
var b int
b, ok := <- ch
fmt.Println(b, ok)
if ok == false
break
/* 执行结果
PS H:Gosrcgo_devday8channelclose_chan> go run main.go
0 true
1 true
2 true
3 true
4 true
0 false
PS H:Gosrcgo_devday8channelclose_chan>
*/
上面输出的最后一条,就是channel已经空了,读出来的就是类型的0值,并且ok变false了。
遍历channel的示例:
package main
import "fmt"
func main()
var ch chan int
ch = make(chan int) // 这个管道没有无缓存
// 这个goroutine一次存一个,再存会阻塞,直到主线程后面的for循环遍历的时候取走数据
// 存完100个数后,这里的for循环结束,会关闭管道。主线程后面的for循环的遍历就能正常结束了
go func ()
for i := 0; i < 100; i++
ch <- i
close(ch)
()
for v := range ch
fmt.Println(v)
判断子线程结束
学到这里,再也不需要用Sleep等待子线程结束了,可以通过管道实现。可以单独定义一个专门用来判断子线程结束的管道。子线程完成任务后,就传个值给管道,主线程就阻塞的读管道里的信息,一旦读到信息,就说明子线程完成了,可以继续执行或者退出了。如果起了多个子线程,则主线程就是用for循环多读几次,就能判断出有多少子线程已经结束了。
channel 只读、只写
声明只读的channel:
var ch <-chan int
声明只写的channel:
var ch chan<- int
应用场景,管道需要能够可读可写。但是可以限制它在某个函数里的功能,也就是在定义函数的参数的时候,把管道的类型设置为只读或只写。或者把管道传给结构体,结构体里限制管道的读写限制?
下面是之前的一个例子,仅仅只是把2个函数在设置参数类型的时候把管道的读写限制加上了:
package main
import (
"fmt"
"time"
)
func write(ch chan<- int)
var i int
for
ch <- i
i ++
time.Sleep(time.Millisecond)
func read(ch <-chan int)
for
b := <- ch
fmt.Println(b)
func main()
intChan := make(chan int, 10)
go write(intChan)
go read(intChan)
time.Sleep(time.Second * 5)
配合 select 使用
select 用法类似IO多路复用,可以同时监听多个 channel 的消息状态,用法如下:
select
case <- ch1:
...
case <- ch2:
...
case ch3 <- 10;
...
default:
...
select 可以同时监听多个 channel 的写入或读取:
- 若只有一个 case 通过(不阻塞),则执行这个 case 块
- 若有多个 case 通过,则随机挑选一个 case 执行
- 若所有 case 均阻塞,则执行 default 块。若未定义 default 块,则 select 语句阻塞,直到有 case 被唤醒
- 使用 break 会跳出 select 块
- select 不会循环,就只会执行一个块然后就继续往后执行了
select只会执行一次
这个例子只会输出一次,随机是1或者是2,然后接结束了:
package main
import "fmt"
func main()
ch1 := make(chan int, 1)
ch1 <- 1
ch2 := make(chan int, 1)
ch2 <- 2
select
case v := <- ch1:
fmt.Println(v)
case v := <- ch2:
fmt.Println(v)
default:
fmt.Println(0)
所以如果要把管道里的数取完,或者取多次,就要再套一层for循环。
for循环和break的效果
在select外面用for套了一层死循环,这样就是反复的执行select。不过break在这里就没效果了:
package main
import (
"fmt"
"time"
)
func main()
var ch1, ch2 chan int
ch1 = make(chan int, 10)
ch2 = make(chan int, 10)
for i := 0; i < cap(ch1); i++
ch1 <- i
ch2 <- i * i
// LABEL1:
for
select
case v := <- ch1:
fmt.Println("ch1", v)
case v := <- ch2:
fmt.Println("ch2", v)
default:
fmt.Println("所有元素都已经取完")
break // 这个break没有意义,因为值是跳出select,而不是for循环
// break LABEL1 // 这个break可以直接跳出for循环
time.Sleep(time.Second)
如果要跳出for循环,可以配合标签。上面的代码里已经写好了只是注释掉了。
定时器
定时器是在 time 包里的,
package main
import (
"fmt"
"time"
)
func main()
t := time.NewTicker(time.Second)
for v := range t.C
fmt.Println(v)
上面调用的NewTicker()方法返回的是个结构体,如下:
type Ticker struct
C <-chan Time // The channel on which the ticks are delivered.
// contains filtered or unexported fields
上面的例子里遍历了 t.C 就是一个channel。time包内部应该是会产生一个goroutine,每隔一段时间就传一个数据进去。
设置超时时间
还有一个After()方法,和上面的方法是一样的。不过这个方法直接返回管道,即 NewTimer(d).C 。而NewTimer()方法的管道在返回的结构体的属性C里。这个After()方法用起来更方便。结合select正好可以做成一个设置任务超时时间的功能:
package main
import (
"fmt"
"time"
)
func task(ch chan struct)
time.Sleep(time.Second * 3)
ch <- struct
func main()
ch := make(chan struct) // 定义好信号的管道,传递空结构体
go task(ch) // 启动一个任务
select
case <- ch:
fmt.Println("任务执行结束")
case <- time.After(time.Second * 2): // 2秒后超时
fmt.Println("任务超时")
goroutine 中使用 recover
程序里起的gorountine中如果panic了,并且这个goroutine里面没有捕获错误的话,整个程序就会挂掉。
下面的程序会报错(Panic),是gorountine里的产生的错误:
package main
func divideZero(ch chan int)
zero := 0
ch <- 1 / zero
func main()
ch := make(chan int)
go divideZero(ch)
<- ch
在gorountine中运行错误了,是可以不影响其他线程和主线程的继续执行的。所以,好的习惯是每当产生一个goroutine,就在开头用defer插入recover, 这样在出现panic的时候,就只是自己退出而不影响整个程序。下面是优化后的代码,加入了recover来捕获错误:
package main
import "fmt"
func divideZero(ch chan int)
defer func ()
if err := recover(); err != nil
fmt.Println(err)
// 要给管道传值,否则主线程从空管道里取值会阻塞,形成死锁
ch <- 0
()
zero := 0
ch <- 1 / zero
func main()
ch := make(chan int)
go divideZero(ch)
<- ch
单元测试
测试用例的文件名必须以_test.go结尾,测试的函数也必须以Test开头。符合命名规则,使用 go test 命令的时候就能自动运行测试用例。
这篇的单元测试比较粗糙,不过基本怎么用,以及用法示例都简单记下来了。
被测试的函数
先准备一个需要被测试的函数:
package main
import "fmt"
func get_fullname(first, last string) (fullname string)
fullname = first + " " + last
return
func main()
fullname := get_fullname("Barry", "Allen")
fmt.Println(fullname)
上面的 get_fullname() 函数就是接下来要进行单元测试的函数。
测试用例
package main
import "testing"
func TestName(t *testing.T)
r := get_fullname("Sara", "Lance")
expect := "Sara Lance"
if r != expect
t.Fatalf("ERROR: get_fullname expect: %s actual: %s", expect, r)
t.Log("测试成功")
执行测试
写完测试用例,就可以执行测试了,使用命令 go test。输出如下:
PS H:Gosrcgo_devday8unit_test
ame> go test
PASS
ok go_dev/day8/unit_test/name 0.058s
PS H:Gosrcgo_devday8unit_test
ame>
看到PASS了,但是t.Log()并没有输出,要看到更多信息,要用带上-v参数。使用命令 go test -v ,输出如下:
PS H:Gosrcgo_devday8unit_test
ame> go test -v
=== RUN TestName
--- PASS: TestName (0.00s)
name_test.go:11: 测试成功
PASS
ok go_dev/day8/unit_test/name 0.053s
PS H:Gosrcgo_devday8unit_test
ame>
直接用go test命令,只显示测试的结果。如果有多个测试用例,也只有一个结果。可以用-v参数看到详细的信息,每个测试用例的的结果都会打印出来。
如果某个测试失败了,就会直接退出,不会继续测试下去。
go语言channel(代码片段)
Go语言channel1、什么是channel2、为什么需要channel3、channel的基本介绍4、channel的使用4.1、概念4.2、创建channel4.3、实例分析4.3.1、channel模拟消息队列4.3.2、区分缓冲和非缓冲4.3.3、单向接口的遍历4.4、关于close通道5、for...select操作类... 查看详情
go语言channel(代码片段)
Go语言channel1、什么是channel2、为什么需要channel3、channel的基本介绍4、channel的使用4.1、概念4.2、创建channel4.3、实例分析4.3.1、channel模拟消息队列4.3.2、区分缓冲和非缓冲4.3.3、单向接口的遍历4.4、关于close通道5、for...select操作类... 查看详情
go语言管道(channel)(代码片段)
前言 channel式go语言协程中数据通信的双向通道。但是在实际应用中,为了代码的简单和易懂,一般使用的channel是单向的。使用1.channel的定义和收发数据packagechannelfuncmain() //varcchanintc的默认值位nil,一般不... 查看详情
387集go语言核心编程培训视频教材整理|goroutine和channel
学习视频来源:《韩顺平Go语言核心编程387集完整版》 查看详情
387集go语言核心编程培训视频教材整理|goroutine和channel
学习视频来源:《韩顺平Go语言核心编程387集完整版》 查看详情
387集go语言核心编程培训视频教材整理|goroutine和channel
学习视频来源:《韩顺平Go语言核心编程387集完整版》 查看详情
387集go语言核心编程培训视频教材整理|goroutine和channel
学习视频来源:《韩顺平Go语言核心编程387集完整版》 查看详情
go语言的管道channel用法
本文实例讲述了Go语言的管道Channel用法。分享给大家供大家参考。具体分析如下:channel是有类型的管道,可以用channel操作符<-对其发送或者接收值。ch<-v//将v送入channelch。v:=<-ch//从ch接收,并且赋值给v。(“箭头”就是数... 查看详情
go语言编程:使用条件变量cond和channel通道实现多个生产者和消费者模型
如题,使用条件变量Cond和channel通道实现多个生产者和消费者模型。Go语言天生带有C语言的基因,很多东西和C与很像,但是用起来绝对比C语言方便。今天用Go语言来实现下多消费者和生产者模型。如果对C语言的多生产者和消费... 查看详情
go的通道channel(代码片段)
...特色的数据类型,通道(channel)完全可以与goroutine(也可称为go程)并驾齐驱,共同代表Go语言独有的并发编程模式和编程哲学。Don’tcommunicatebysharingmemory;sharememorybycommunicating.(不要通过共享内存来 查看详情
go的通道channel(代码片段)
...特色的数据类型,通道(channel)完全可以与goroutine(也可称为go程)并驾齐驱,共同代表Go语言独有的并发编程模式和编程哲学。Don’tcommunicatebysharingmemory;sharememorybycommunicating.(不要通过共享内存来 查看详情
go语言之并发编程channel
单向channel:单向通道可分为发送通道和接收通道。但是无论哪一种单向通道,都不应该出现在变量的声明中,假如初始化了这样一个变量varuselessChanchan<-int=make(chan<-int,10)这样一个变量该如何使用呢,这样一个只进不出的通道... 查看详情
go基础--goroutine和channel
goroutine在go语言中,每一个并发的执行单元叫做一个goroutine这里说到并发,所以先解释一下并发和并行的概念:并发:逻辑上具备同时处理多个任务的能力并行:物理上在同一时刻执行多个并发任务当一个程序启动时,其主函数... 查看详情
2021-go语言并发编程
...Go语言并发简略-协程6、并发简略-对比并发模型7、Go语言goroutine8、Go语言channel9、Go语言channel的操作10、Go语言channel的应用11、Go语言select12、Go协程调度模型-113、Go协程调度模型-214、Go语言同步2-等待组15、Go语言同步3-条件变量16、... 查看详情
[日常]go语言圣经--channel习题
...使用CloseRead和CloseWrite方法分别关闭它们。修改netcat3的主goroutine代码,只关闭网络连接中写的部分,这样的话后台goroutine可以在标准输入被关闭后继续打印从reverb1服务器 查看详情
go15---select
...port("fmt")//go语言提供了一个结构或者形式来帮助处理多个channel的发送和接收问题,这个结构叫做select,//select和switch相近,但是select是专门针对channel的,可以同时处理多个channel的发送与接收,//当多个channel可以发送和接收的时候... 查看详情
go语言管道(代码片段)
...箭头 <- 。Channel是CSP模式的具体实现,用于多个goroutine通讯。其内部实现了同步,确保并发安全。Channel是线程安全的,先进先出,多个goroutine同时访问,不需要加锁,channel是有类型的,一个整数的channel只能存放整数 查看详情
go并发编程基础-channel(代码片段)
协程(Goroutine)Go语言中没有线程的概念,只有协程,也称为goroutine。相比线程来说,协程更加轻量,一个程序可以随意启动成千上万个goroutine。goroutine被Goruntime所调度,这一点和线程不一样。也就是说,Go语言的并发是由Go自己... 查看详情