同步操作将从 JustryDeng/notebook 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
Go语言最大的特色是并发,而且Go的并发并不像线程或进程那样,受CPU核心数的限制,只要你愿意,你可以启动成千上万个Goroutine协程
。
Goroutine
):协程是比线程还要小的执行单位,准确地说,协程是通过线程来执行的。 在操作系统层面,线程是最小的执行单位。Go语言调度算法会为每个线程提供一个Goroutine
协程执行列表,CPU在不同的线程间切换时需要记录上下文信息,如图所示:
当线程调度执行某个Goroutine
协程的时,该协程阻塞了,调度该协程的线程会被挂起,此时,该线程也就没法执行其他Goroutine
协程了。此时,Go的调度算法会将该线程的Goroutine
协程队列转移到其他线程的Goroutine
协程队列中去,如图所示:
使用关键字
go
+ 函数调用,即可启动一个协程
示例一:
import (
"fmt"
"time"
)
func main() {
fmt.Println("start")
// 开启协程
go func() {
fmt.Println("协程执行了")
}()
// 睡眠1s,给足够的时间,观察协程输出
time.Sleep(time.Second * 1)
fmt.Println("end")
}
输出:
start
协程执行了
end
示例二:
import (
"fmt"
"time"
)
func main() {
// 开启协程,每隔200毫秒打印一下符号,提升用户体验
go spinner(time.Millisecond * 200)
// 计算斐波那契数列
fmt.Printf("\n%d\n", fib(45))
}
// 计算斐波那契数列
func fib(x int) int {
if x < 2 {
return x
}
return fib(x-2) + fib(x-1)
}
// 打印符号
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
输出:
# 动态效果,循环输出 - \ | /
\
在Go中,常用的同步机制有
WaitGroup
注:类似于Java的倒计时锁
CountDownLatch
互斥锁(
Mutex
)读写锁(
RWMutex
)条件变量(
Cond
)
示例:使用同步机制前
import (
"fmt"
)
func main() {
for i := 0; i < 10; i++ {
go func(num int) {
fmt.Println(num)
}(i)
}
fmt.Println("main线程执行了")
}
输出:
0
main线程执行了
示例:使用同步机制后
import (
"fmt"
"sync"
)
var w sync.WaitGroup
func main() {
for i := 0; i < 10; i++ {
// 计数+1
w.Add(1)
go func(num int) {
fmt.Println(num)
// 计数-1
w.Done()
}(i)
}
// 阻塞等待,直到w值为0
w.Wait()
fmt.Println("main线程执行了")
}
输出:
0
2
6
7
4
5
9
3
8
1
main线程执行了
Linux
的管到机制:Linux
的管道机制是借助内核开辟的缓冲区实现一个像是"水管"的通道,"水管"两端的进程可以通过这个"水管"进行传递,以达到进程间通信的目标。Go语言的
channel
:Go语言的channel类似于Linux
管道,channel
通过阻塞读和阻塞写的方式,可以精准控制Goroutine
协程的运行、通信,即:实现了Goroutine
协程间的同步。注:channel传递数据的方式有点"不见不散"的意思,读和写的双方同时操作的时候才会解除彼此的阻塞,否者一方会死等另一方的到来。
创建channel
make(chan chantype) // 或者 make(chan chantype, d uint)
chan
:channel的关键字注:
chantype
代表了通道内可以传递的数据类型,可以是原生类型,也可以是自定义结构。d:代表通道的缓冲区大小;通道本身是同步机制,使用缓冲区可以做到异步操作
channel的读写行为
- 写行为
- 通道缓冲区已满(无缓冲区):写阻塞直到缓冲区有空间(或读端有读行为)
- 通道缓冲区未满:顺利写入,结束
- 读行为
- 缓冲区无数据(无数数据时写端未写数据):读阻塞直到写端有数据写入
- 缓冲区有数据:顺利读数据,结束
channel的读写语法
msg := <- c // 读,将channel c中的数据读取至msg c <- msg // 写,将msg中的数据写入channel c提示:通过观察channel和箭头的位置即可轻松区分读和写。
示例一:
import (
"fmt"
"sync"
"time"
)
var c chan string
var w sync.WaitGroup
func reader() {
msg := <-c // 从c读
fmt.Println("I am reader,", msg)
}
func main() {
c = make(chan string)
w.Add(1)
go reader()
fmt.Println("begin sleep") // 睡眠3秒是为了看执行效果,验证channel阻塞读
time.Sleep(time.Second * 3)
c <- "hello" // 往c写
time.Sleep(time.Second * 1) // 睡眠1秒是为了避免main结束得太快而看不到执行效果
}
输出:
begin sleep
I am reader, hello
示例二:
提示:
channel使用完毕后,需要作出close关闭,以达到广播的目的,这样一来其它使用到该channel的"家伙"就能感知到
注:试想一下,假设
协程writterA
通过通道channelB
向另一个协程readerC
跨协程传输数据,传输完毕后,通道自动关闭(不可能再有人往里面写数据了,因为通道都没了)而没有通知协程readerC
,那么由于channel的读写阻塞机制,协程readerC
就会一直等下去,进而形成死锁。关闭channel的动作一定要由写端发起;如果读端关闭了,写端不知情,写端再往已经关闭了的channel写数据就会报错
下述代码示例为:做一个数字传递的游戏使用3个协程,第一个协程负责将0-9传递给第二个协程,第二个协程将受收到的数据平方后传递给第三个协程,第三个协程负责将收到的数据打印到控制台。
import (
"fmt"
"time"
)
func main() {
var c1 = make(chan int)
var c2 = make(chan int)
// 数数的协程
go func() {
for i := 0; i < 10; i++ {
c1 <- i // 向通道c1写入数据
time.Sleep(time.Second * 1) // 这里睡眠1秒,是为了方便观察执行效果,要不然程序一下子就全部输出来了
}
close(c1) // 写完后, 关闭c1
}()
// 计算平方的协程
go func() {
for {
num, ok := <-c1 // 读取c1数据
if ok {
c2 <- num * num // 将平方写入c2
} else {
break // 如果ok==false,即:c1关闭了,那么结束循环
}
}
close(c2) // 写完后, 关闭c2
}()
// main最后负责打印(msin本身也属于一个协程)
for {
for {
num, ok := <-c2 // 读取c2数据
if ok {
fmt.Println(num)
} else {
break // 如果ok==false,即:c2关闭了,那么结束循环
}
}
}
}
输出:
0
1
4
9
16
25
36
49
64
81
单方向channel
和channel
用法其实是一样的,不同的是:单方向channel通过主动声明读通道(或写通道)的方式,从语法上强制约束了该通道只能读(或只能写),降低了程序员对代码的理解难度,让通道的使用更加明了,让channel更友好了。
单方法channel的声明:
chan_name chan <- chan_type // 只写通道 chan_name <- chan chan_type // 只读通道
示例:
下述代码示例为:做一个数字传递的游戏使用3个协程,第一个协程负责将0-9传递给第二个协程,第二个协程将受收到的数据平方后传递给第三个协程,第三个协程负责将收到的数据打印到控制台。
import (
"fmt"
"time"
)
func main() {
var c1 = make(chan int)
var c2 = make(chan int)
// 数数的协程
go func(writeChannel chan<- int) {
for i := 0; i < 10; i++ {
writeChannel <- i // 向通道writeChannel写入数据
time.Sleep(time.Second * 1) // 这里睡眠1秒,是为了方便观察执行效果,要不然程序一下子就全部输出来了
}
close(writeChannel) // 写完后, 关闭writeChannel
}(c1)
// 计算平方的协程
go func(readChannel <-chan int, writeChannel chan<- int) {
for {
num, ok := <-readChannel // 读取readChannel数据
if ok {
writeChannel <- num * num // 将平方写入writeChannel
} else {
break // 如果ok==false,即:readChannel关闭了,那么结束循环
}
}
close(writeChannel) // 写完后, 关闭writeChannel
}(c1, c2)
// main最后负责打印
for {
for {
num, ok := <-c2 // 读取c2数据
if ok {
fmt.Println(num)
} else {
break // 如果ok==false,即:c2关闭了,那么结束循环
}
}
}
}
输出:
0
1
4
9
16
25
36
49
64
81
定时器是Go语言对channel的一个非常典型的应用;当你声明运行一个定时器后,Go语言会每隔一段时间(这个时间在你声明定时器时由你指定)作为写的一方往channel写数据,应用程序作为读的一方,每当收到一次数据,那么即说明过去了指定的时间长度,即达到了定时的效果
示例:
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Second)
num := 5
for {
// 从channel C中读取数据(注:这里虽然读取数据了,但是没有使用变量接收这个数据,这是可以的)
<-ticker.C
fmt.Println(num)
num--
if num == 0 {
break
}
}
ticker.Stop()
launch() // 倒计时结束,发射
}
func launch() {
fmt.Println("发射!")
}
输出:
5
4
3
2
1
发射!
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。