GO语言入门指南 :GO并发(二)| 青训营

68 阅读8分钟

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.MutexLockUnLock方法(因为这两个结构都实现了sync.Locker接口)。但是,它还允许使用RLockRUnlock方法进行并发读取:

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包的函数之前,先了解一下基本的概念:

    1. GMT和UTC时间: GMT格林尼治时间,作为世界标准时间,UTC与GMT误差非常小,可以认为是一致的, 圈子里都以UTC时间作为标准时间。
    1. 时区: 英国为0时区,中国在东八区 (UTC+08),UTC时间加上8小时就是我们本地时间了 有时会看到CST +0800 就是代表的中国标准时间。
    1. 时间戳: 是从1970年1月1日(UTC/GMT的午夜 1970-01-01T00:00:00)开始所经过的秒数。

1.时间格式化

1.1 格式化是使用time包中time类型的Format方法 , layout 字符串类型代表的是要格式化成的格式

v2-975460055abd1ed1c4a35ff297a436a7_r.jpg (462×36) (zhimg.com)

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

  1. 时间转换

常用的转换就是时间与 时间戳互转、时间与字符串互转

时间戳与时间互转 是 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

  1. 时间加减操作

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