GO语言入门指南 :GO并发(二)
1. 概要
(一)
- go(协程) : 通过关键字 go 即可创建一个协程.
- chan : golang 中用于并发的通道,用于协程的通信.
-
- 有缓冲通道
- 无缓冲通道
- 单向通道
- select: golang 提供的多路复用机制.
- close() : golang 的内置函数, 可以关闭一个通道.
(二)
- 阻塞: 阻塞是进程(也可以是线程、协程)的状态之一(新建、就绪、运行、阻塞、终止). 指的是当数据未准备就绪,这个进程(线程、协程)一直等待,这就是阻塞.
- 非阻塞: 当数据为准备就绪,该进程(线程、协程)不等待可以继续执行,这就是非阻塞.
- 同步: 在发起一个调用时,在没有得到结果之前,这个调用就不返回,这个调用过程一直在等待. 这是同步.
- 异步: 在发起调用后,就立刻返回了,这次调用过程就结束了. 等到有结果了被调用方主动通知调用者结果. 这是异步.
- sync: golang 标准库之一,提供了锁.
- 定时器: golang 标准库 time 提供的重要功能, 提供了定时器功能,可用于超时处理.
-
- Timer
- Ticker
2.阻塞与死锁
-
当goroutine在等待通道的发送或者接受操作的时候,我们就说它被阻塞了。这似乎跟我们写一个不做任何事情只会空转的无限循环一样,但还是有所区别。goroutine本身占用少量的内存,被阻塞的goroutine并不消耗任何资源,它只会一直等待阻塞的事件发生,然后解除。
-
当goroutine因为某些永远无法发生的事情而被阻塞时,我们称这种情况为死锁,引起死锁的代码可以非常简单,像如下:
func main(){
c :=make(chan int)
<-c
}
3.异步与同步
同步与异步
- 同步异步 , 举个例子来说,一家餐厅吧来了5个客人,同步的意思就是说,来第一个点菜,点了个鱼,好, 厨师去捉鱼杀鱼,过了半小时鱼好了给第一位客人,开始下位一位客人,就这样一个一个来,按顺序来
-
相同, 异步呢,异步的意思就是来第一位客人,点什么,点鱼,给它一个牌子,让他去一边等吧,下一位客人接着点菜,点完接着点让厨师做去吧,哪个的菜先好就先端出来,
-
同步的优点是:同步是按照顺序一个一个来,不会乱掉,更不会出现上面代码没有执行完就执行下面的代码, 缺点:是解析的速度没有异步的快;
-
异步的优点是:异步是接取一个任务,直接给后台,在接下一个任务,一直一直这样,谁的先读取完先执行谁的, 缺点:没有顺序 ,谁先读取完先执行谁的 ,会出现上面的代码还没出来下面的就已经出来了,会报错;
下面是同步和异步的示例代码:
异步:
package main
import (
"fmt"
)
func main() {
go work1()
go work2()
fmt.Println("[全部完成]")
}
func work1(){
fmt.Println("work1")
}
func work2(){
fmt.Println("work2")
}
输出结果:
[全部完成]
结果并没有等goroutine结束就执行完main函数了,异步执行,在调用work1,work2之后不等输出结果,就继续执行了。
要实现同步的话一般会用工具 waitgroup,sync.WaitGroup拥有一个内部计数器。当计数器等于0时,则Wait()方法会立即返回。否则它将阻塞执行Wait()方法的goroutine直到计数器等于0时为止。
要增加计数器,我们必须使用Add(int)方法。要减少它,我们可以使用Done()(将计数器减1),也可以传递负数给Add方法把计数器减少指定大小,Done()方法底层就是通过Add(-1)实现的。
同步:
package main
import (
"fmt"
"sync"
)
var waitGroup sync.WaitGroup
func main() {
waitGroup.Add(2)//增加两个要等待的协程
go work1()
go work2()
waitGroup.Wait()//等待
fmt.Println("[全部完成]")
}
func work1(){
fmt.Println("work1")
waitGroup.Done()//减一
}
func work2(){
fmt.Println("work2")
waitGroup.Done()
}
输出结果:
work2
work1
[全部完成]
4. snyc包
sync包里面的很多函数都是用来处理并发的,下面来介绍一下常用的:
-
sync.Mutex
sync.Mutex可能是sync包中使用最广泛的原语。它允许在共享资源上互斥访问(不能同时访问):
mutex := &sync.Mutex{}
mutex.Lock()
// Update共享变量 (比如切片,结构体指针等)
mutex.Unlock()
必须指出的是,在第一次被使用后,不能再对sync.Mutex进行复制。(sync包的所有原语都一样)。如果结构体具有同步原语字段,则必须通过指针传递它。
-
sync.RWMutex
sync.RWMutex是一个读写互斥锁,它提供了我们上面的刚刚看到的sync.Mutex的Lock和UnLock方法(因为这两个结构都实现了sync.Locker接口)。但是,它还允许使用RLock和RUnlock方法进行并发读取:
mutex := &sync.RWMutex{}
mutex.Lock()
// Update 共享变量
mutex.Unlock()
mutex.RLock()
// Read 共享变量
mutex.RUnlock()
sync.RWMutex允许至少一个读锁或一个写锁存在,而sync.Mutex允许一个读锁或一个写锁存在。
sync.RWMutex的运行速度比sync.Mutex要快,因此,只有在频繁读取和不频繁写入的场景里,才应该使用sync.RWMutex。
-
sync.WaitGroup
如3的同步代码里面的,不重复介绍
其他的如:sync.Map,sync.Pool,sync.Once等请读者自行了解
5.time包
在看time包的函数之前,先了解一下基本的概念:
-
- GMT和UTC时间: GMT格林尼治时间,作为世界标准时间,UTC与GMT误差非常小,可以认为是一致的, 圈子里都以UTC时间作为标准时间。
-
- 时区: 英国为0时区,中国在东八区 (UTC+08),UTC时间加上8小时就是我们本地时间了 有时会看到CST +0800 就是代表的中国标准时间。
-
- 时间戳: 是从1970年1月1日(UTC/GMT的午夜 1970-01-01T00:00:00)开始所经过的秒数。
1.时间格式化
1.1 格式化是使用time包中time类型的Format方法 , layout 字符串类型代表的是要格式化成的格式

1.2 golang的格式化比较特殊,是固定的格式:2006 01 02 15 04 05 中间的分隔符大家可以自己定义,看下 面示例代码:
func main(){
// 获取当前时间
now := time.Now()
// 格式化
nowStr1 := now.Format("2006年01月02日 15:04:05")
nowStr2 := now.Format("2006-01-02 15:04:05")
// 其他格式表示
unixData := now.Format(time.UnixDate)
fmt.Println("当前时间:", now)
fmt.Println("当前时间格式化字符串:", nowStr1)
fmt.Println("当前时间格式化字符串:", nowStr2)
fmt.Println("当前时间unixData表示:", unixData)
}
输出结果:
当前时间: 2021-03-05 10:57:25.20616 +0800 CST m=+0.000260036 //中国标准时间表示形式
当前时间格式化字符串: 2021年03月05日 10:57:25
当前时间格式化字符串: 2021-03-05 10:57:25
当前时间unixData表示: Fri Mar 5 10:57:25 CST 2021
- 时间转换
常用的转换就是时间与 时间戳互转、时间与字符串互转
时间戳与时间互转 是 time包下公共方法func Unix(sec int64, nsec int64) Time 与time类型func (t Time) Unix() int64 互为一对
时间字符串转时间类型是用到 func Parse(layout, value string) (Time, error) layout参数代表是转换的格式, value就是你要转的时间字符串.
func main(){
// 获取当前时间
now := time.Now()
// 时间转时间戳
timestamp := now.Unix()
fmt.Println("now:", now)
fmt.Println("时间戳:", timestamp)
// 时间戳转时间
dt := time.Unix(timestamp, 0 ) //time包的Unix第一个参数是代表从1970年1月1日午夜+timestamp
fmt.Println("时间戳转时间:", dt)
// 时间字符串转时间类型
now_str := now.Format("2006-01-02 15:04:05")
t, _ := time.Parse("2006-01-02 15:04:05", now_str)
fmt.Println("时间字符串转时间:", t)
}
输出:
now: 2021-03-05 13:25:10.07141 +0800 CST m=+0.000360923
时间戳: 1614921910
时间戳转时间: 2021-03-05 13:25:10 +0800 CST
时间字符串转时间: 2021-03-05 13:25:10 +0000 UTC
- 时间加减操作
3.1 时间加减使用的是 time类型的Add方法func (t Time) Add(d Duration) Time,可以看到他的参数类型是Duration,
确定要加或减的时间范围,可以通过func ParseDuration(s string) (Duration, error) 来吧要加减的时间范围s转换成Duration类型,这里的s参数的有效单 位"ns", "us" (or "µs"), "ms", "s", "m", "h",
func main(){
// 获取当前时间
now := time.Now()
fmt.Println("now:", now)
afterD, _ := time.ParseDuration("10h")
after10h := now.Add(afterD)
fmt.Println("10小时之后:", after10h)
beforeD,_ := time.ParseDuration("-10h")
before10h := now.Add(beforeD)
fmt.Println("10小时之前:", before10h)
}
输出:
now: 2021-03-05 13:55:14.614533 +0800 CST m=+0.000240738
10小时之后: 2021-03-05 23:55:14.614533 +0800 CST m=+36000.000240738
10小时之前: 2021-03-05 03:55:14.614533 +0800 CST m=-35999.999759262
3.2 求时间差,程序逻辑执行的耗时经常会用到:
func main(){
// 获取开始时间
start := time.Now()
// 处理业务逻辑代码
time.Sleep(5)
// 获取结束时间
end := time.Now()
diff := end.Sub(start)
fmt.Println("业务逻辑耗时:",diff)
}
输出:
业务逻辑耗时: 13.711µs