这是我参与「第三届青训营 -后端场」笔记创作活动的第一篇笔记
并发vs并行
并发:多线程程序在一个核的cpu运行。
并行:多线程程序在多个核的cpu运行。
Goroutine
协程:用户态,轻量级线程,栈MB级别。切换调用由Go语言本身完成,较线程轻量许多。
线程:内核态,线程跑多个协程,栈KB级别。线程的创建,切换,停止都属于很重的系统操作,较消耗资源。
for i := 0; i < 5; i++ {
go func(j int) {
println("hello goroutine : " + fmt.Sprint(j))
}(i)
}
time.Sleep(time.Second)
简单来说就是同时跑,先跑完的先输出
并发安全
并发安全就是程序在并发情况下执行的结果都是正确的。
类型与并发安全
(1)字节型,布尔型,整形,浮点型(取决于操作系统指令集)
由于他们的位宽不会超过64位,所以在64位的指令集架构中可以由一条机器指令完成,不存在被细分为更小的操作单位,所以这些类型的并发赋值是安全的,但是这个也跟操作系统的位数有关,比如int64在32位操作系统中,它的高32位和低32位是分开赋值的,此时是非并发安全的。
(2)复数型(不安全)
因为复数分为实部和虚部,两者的赋值时分开进行的,所以复数时非并发安全的。注意:如果复数并发赋值时,有相同的实部和虚部,那么两个字段的赋值就会退化为一个字段,这种情况下是并发安全的。
(3)字符串(不安全)
字符串在go中是一个只读字节切片,string有两个重要的特点:string可以为空(长度为0),但不会是nil;string对象不可以修改。string底层是一个结构体,包含两个字段:str为字符串的首地址指针,len为字符串的长度。只要底层是结构体类型,都不是并发安全的。但是如果只并发给结构体中的一个字段赋值,这样就是并发安全的。
(4)指针(安全)
指针是保存两一个变量的内存地址的变量,因为是内存地址,所以位宽为32位(x86平台)或64位(x64平台),赋值操作由一条机器指令即可完成,不能被中断,所以是并发安全的。
(5)结构体(不安全)
结构体中有多个字段,每个字段都是单独赋值的,在并发操作的情况下可能会出现问题。
(6)函数(安全)
函数类型的变量赋值时,实际上赋的是函数地址,一条机器指令便可完成,所以并发赋值是安全的。我们使用unsafe.Sizeof()可以查看函数类型的宽度(字节)。
(7)数组,切片,字典,通道,接口(不安全)
数组、切片、字典、通道、接口,这些复合类型,除了数组,其他底层数据结构都是 struct,所以并发都不是安全的,当然数组并发赋值也是不安全的。
互斥锁
互斥锁是一种常用的控制共享资源的办法,它能够同时保证一个gotroutine可以访问共享资源。go语言中使用sync和metux类型来实现互斥锁。 代码如下:
package main
import (
"fmt"
"sync"
)
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
defer wg.Done()
for i := 0; i < 100000; i++ {
lock.Lock()
x++
lock.Unlock()
}
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
这样使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒策略是随机的。