1.并发与并行的区别
并行:多核cpu在同一时间片内并行处理多个任务。
并发:如单核cpu在多个任务间进行时间片切换,并非同一时间片执行多个任务,只是上下文切换时间很短,看似多个任务并行。
多线程和多线程是并行的基本前提条件,单线程也可用协程做到并发。
在golang中是通过goroutine来实现并发的,goroutine并不能简单的归纳为协程,其运行时会创建多个线程来实现并发任务,且任务单元可被调度到其他线程并行执行。所以goroutine更像是多线程和协程的综合体,能最大限度提升执行效率,发挥多核处理能力。
goroutine
关键字go并非执行并发操作,而是创建一个并发任务单元。新建任务被放置在系统队列中,等待调度器安排合适的系统线程去获取执行权。
当前流程不会阻塞,不会等待该任务启动,且运行时也不保证并发任务的执行顺序。
每个任务单元除保存函数指针、调用参数外,还会分配执行所需的栈内存空间。相比系统默认MB级别的线程栈,goroutine自定义栈仅需2KB,所以才能创建成千上万的并发任务。自定义栈采取按需分配策略,在需要时仅需扩容,最大能到GB规模。
与defer一样,goroutine也会因延迟执行而立即计算并复制执行参数。
var c int
func counter()int{
c++
return c
}
func main() {
a:=100
go func(x,y int) {
time.Sleep(time.Second)
fmt.Println("go:",x,y)
}(a,counter())
a+=100
fmt.Println("main:",a,counter())
time.Sleep(time.Second*3)
}
输出:
main: 200 2
go: 100 1
wait
进程退出时不会等待并发任务结束,可用channel阻塞,然后发出退出信号。
func main() {
exit:=make(chan interface{}) //创建通道。因为仅是通知,此处channel可为任何类型。
go func() {
time.Sleep(time.Second)
fmt.Println("goroutine done")
close(exit) //关闭通道,发出信号。
}()
fmt.Println("main...")
<-exit //通道关闭则立即解除。
fmt.Println("main exit")
}
输出:
main...
goroutine done
main exit
除了关闭通道外,向通道内写入数据也可解除阻塞。channel的更多信息,后面再做详述。
如要等待多个任务结束,推荐使用sync.WaitGruop。通过设定计数器,让每个goroutine在退出前递减,直至归零时解除阻塞。
func main() {
var wg sync.WaitGroup
for i:=0;i<10;i++{
wg.Add(1) //累加计数
go func(id int) {
defer wg.Done() //递减计数
time.Sleep(time.Second)
fmt.Println("goroutine",id,"done")
}(i)
}
fmt.Println("main...")
wg.Wait() //阻塞,直到计数归零
fmt.Println("main exit")
}
尽管WaitGroup.Add实现了原子操作,但建议在goroutine外累加计数器,以免Add尚未执行,Wait以及推出。
func main() {
var wg sync.WaitGroup
go func() {
wg.Add(1) //可以运行试一下,不是每次都能设置上
defer wg.Done() //递减计数
fmt.Println("goroutine", "done")
}()
fmt.Println("main...")
wg.Wait() //阻塞,直到计数归零
fmt.Println("main exit")
}
可在多处用Wait阻塞,他们都能接收到通知。上例就可在go func前加wg.Wait().
GOMAXPROCS
运行时可能会创建很多线程,但任何时候仅有限的几个线程参与并发任务执行。该数量默认与CPU核数相等,可用runtime.GOMAXPROCS函数(或环境变量)修改。
如参数小于1,GOMAXPROCS仅返回当前设置值,不做任何调整。
import (
"math"
"fmt"
"sync"
"runtime"
)
//测试目标函数
func count(){
x:=0
for i:=0;i<math.MaxUint32;i++{
x+=i
}
fmt.Println(x)
}
//循环执行
func test(n int){
for i:=0;i<n;i++{
count()
}
}
//并发执行
func test2(n int){
var wg sync.WaitGroup
wg.Add(n)
for i:=0;i<n;i++{
go func() {
count()
wg.Done()
}()
}
wg.Wait()
}
func main() {
n:=runtime.GOMAXPROCS(0)
n1:=runtime.NumCPU()
fmt.Println(n1)
test(n)
}
n:=runtime.GOMAXPROCS(0)
n1:=runtime.NumCPU()
上述两个都可用来获取当前系统的cpu核数。
Local Storage
与线程不同,goroutine任务无法设置优先级,无法获取编号,没有局部存储(TLS),甚至连返回值都会被抛弃。但除优先级外,其他功能都很容易实现。
func main() {
var wg sync.WaitGroup
var gs [5]struct{ //用于实现类似TLS功能
id int //编号
result int //返回值
}
for i:=0;i<len(gs);i++{
wg.Add(1)
go func(id int) { //使用参数避免参数闭包延迟求值
defer wg.Done()
gs[id].id = id
gs[id].result=(id + 1) * 100
}(i)
}
wg.Wait()
fmt.Printf("%+v\n",gs)
}
输出:
[{id:0 result:1