并发的含义
并发:逻辑上具备同时处理多个任务的能力
并行:物理上在同意时刻执行多个并发任务
多线程或多进程是并行的基本条件,但是单线程也可用协程做到并发。尽管协程在单个线程上通过主动切换来实现多任务并发。通常情况下,用多进程来实现分布式和负载均衡。用多线程抢夺更多的处理器资源。使用协程来提高处理器时间片利用率。
Go语言创建并发任务只需在函数调用前添加关键字“go”
go println("hello word!") //创建并发任务
go func(s string){ //使用匿名函数
println(s)
}("hello word!")
关键字“go”并非执行并发操作,而是创建一个创建一个并发任务单元。新建任务被放置在系统队列中,等待调度器安排合适系统线程去获取执行权。当前流程不会阻塞,不会等待该任务启动,且运行时也不保证并发任务的执行次序。
与defer一样,goroutine也会因“延迟执行”而立即计算并复制执行参数。
package main
import(
"fmt"
"time"
)
var c int //未初始化会默认为0
func counter() int{
c++
return c
}
func main(){
a := 10
go func(x,y int){ //goroutine在main逻辑后执行
time.Sleep(time.Second)
fmt.Println("go:",x,y)
}(a,counter()) //立即执行并复制参数
a += 10
fmt.Println("main:",a,counter())
time.Sleep(time.Second *3)
}
输出:
main: 20 2
go: 10 1
wait
进程退出时不会等待并发任务结束,可用通道(channel)阻塞,然后发出退出信号。
package main
import(
"fmt"
"time"
)
func main(){
exit := make(chan struct{}) //创建通道,通道只能使用make创建
go func(){
time.Sleep(time.Second)
fmt.Println("go done!")
close(exit) //关闭通道
}()
fmt.Println("start....")
<-exit //如通道关闭,结束阻塞
fmt.Println("exit....")
}
输出:
start….
go done!
exit….
除了关闭通道外,写入数据也可以解除阻塞。
package main
import(
"fmt"
"time"
)
func main(){
exit := make(chan int)
go func(){
time.Sleep(time.Second)
fmt.Println("go done!")
exit<-1 //写入数据
}()
fmt.Println("start....")
<-exit //读出数据
fmt.Println("exit....")
}
输出:
start….
go done!
exit….
如果要等待多个任务结束,推荐使用sync.WaitGroup,通过设定计数器,让每个goroutine在退出前递减,直至归零时解除阻塞。
package main
import(
"fmt"
"time"
"sync"
)
func main(){
var wg sync.WaitGroup
for i := 0;i < 10;i++{ //累计计数,注意go中只支持后加加
wg.Add(1)
go func(id int){
defer wg.Done() //递减计数
time.Sleep(time.Second)
fmt.Println("go ",id,"done")
}(i)
}
fmt.Println("main:...")
wg.Wait() //阻塞直至计数归零
fmt.Println("main exit")
}
输出:
main:…
go 2 done
go 3 done
go 5 done
go 8 done
go 4 done
go 7 done
go 1 done
go 6 done
go 9 done
go 0 done
main exit
尽管WaitGroup.Add实现了原子操作,但建议在goroutine外累加计数器,以免Add尚未执行,wait以及退出。可在多处使用wait阻塞,它们都能接收到通知。
package main
import(
"fmt"
"time"
"sync"
)
func main(){
var wg sync.WaitGroup
wg.Add(1)
go func(){
wg.Wait()
fmt.Println("wait exit!")
}()
go func(){
time.Sleep(time.Second)
fmt.Println("done")
wg.Done()
}()
wg.Wait()
fmt.Println("main exit")
}
输出:
done
wait exit!
main exit
GOMAXPROCS
运行时可能会创建很多线程,但任何时候仅有限的几个线程参与并发任务执行。该数量默认与处理器核数相等,可使用runtime.GOMAXPROCS函数(或环境变量)修改。如果参数小于1,会返回当前设置值,不做任何调整。runtime.NumCPU函数可返回当前机器的核数。
package main
import(
"fmt"
"runtime"
)
func main(){
n := runtime.GOMAXPROCS(0)
m := runtime.NumCPU()
fmt.Println(n,m)
}
输出:8 8 当前测试机器为8核。
goroutine任务无法设置优先级,无法设置优先级,无法获取编号,没有局部存储,甚至连返回值都会被丢弃。但是除了优先级外,其他的功能都容易实现。
package main
import(
"fmt"
"sync"
)
func main(){
var wg sync.WaitGroup
var gs [5]struct{
id int //编号
result int //返回值
}
for i := 1;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:0} {id:1 result:200} {id:2 result:300} {id:3 result:400} {id:4 result:500}]
Gosched
暂停,释放线程去执行其他任务。当前线程被放回队列,等待下次调度时恢复执行。
package main
import(
"fmt"
"runtime"
)
func main(){
runtime.GOMAXPROCS(1)
exit := make(chan struct{})
go func(){ //任务a
defer close(exit)
fmt.Println("go a")
go func(){ //任务b
fmt.Println("go b")
}()
for i := 0;i < 5;i++{
fmt.Println("a:",i)
if i == 1{
runtime.Gosched() //调度执行b
}
}
}()
<-exit
}
输出:
go a
a: 0
a: 1
go b
a: 2
a: 3
a: 4
该函数很少被使用,只是在主动切换时会被使用
Goexit
Goexit立即终止当前任务,运行时确保所有已注册延迟调用被执行。该函数不影响其他并发任务,不会引发panic,无法捕获。
package main
import(
"fmt"
"runtime"
)
func main(){
exit := make(chan struct{})
go func(){
defer close(exit) //执行
defer fmt.Println("a") //执行
func(){
defer func(){
fmt.Println("b",recover()==nil) //执行,recover返回nil
}()
func(){
fmt.Println("c")
runtime.Goexit() //多层调用中执行Goexi()
fmt.Println("c done") //不会执行
}()
fmt.Println("b done") //不会执行
}()
fmt.Println("a done") //不会执行
}()
<-exit
fmt.Println("main exit")
}
输出:
c
b true
a
main exit
通道 channel
通道只可使用make创建,分为有缓存通道以及无缓存通道。
同步模式必须有配对操作的goroutine出现,否则会一直阻塞。而异步模式在缓冲区未满时或数据未被读完前不会阻塞。多数情况下异步通道有助于提升性能,减少排队阻塞。
package main
import(
"fmt"
)
func main(){
c := make(chan int,3) //创建一个带3个缓冲的异步通道,int后无参数为无缓存的通道
c <- 1 //缓冲区未满不会阻塞
c <- 2
fmt.Println(<-c) //缓冲区尚有数据不会阻塞
fmt.Println(<-c)
}
输出:
1
2
缓冲区大小仅是内部属性,不属于类型组成部分。另外通道变量本身就是指针,可用相等操作符判断是否为同一对象或者为nil。内置函数cap和len返回缓冲区大小和当前缓冲区数量。而对于同步通道则都返回0,据此可判断通道是同步还是异步。
收发
除使用简单发送和接受操作符外,还可以使用ok-idiom或range模式处理数据。
package main
import(
"fmt"
)
func main(){
c := make(chan int)
go func(){
for{
x, ok := <-c
if !ok{
return
}
fmt.Println(x)
}
}()
c <- 1
c <- 2
c <- 3
close(c)
}
输出:
1
2
3
对于循环接收数据,range模式更简洁一些。一次性事件使用close效率更高,没有多余开销。连续多样性事件,可传递不同数据标志实现还可以使用sync.Cond实现单播或广播事件。
对于closed或nil通道,发送和接收操作都有相应的规则:
- 向以关闭通道发送数据会引发panic
- 从已关闭接收数据,返回已缓冲数据或零值
- 无论收发nil通道都会阻塞
- 重复关闭或关闭nil通道会引发panic错误
单向通道
通道默认是双向的,并不区分发送和接收端。但在某些时候,我们可限制收发操作的方向来获得更严谨的操作逻辑。尽管可使用make创建单向通道,但是那没有什么意义。通常使用类型转换类获取单向通道并赋予操作双方。
使用make直接创建单向通道:
ReadOnly := make(<-chan int) //创建只读channel
WriteOnly := make(chan<- int) //创建只写channel
package main
import(
"fmt"
"sync"
)
func main(){
var wg sync.WaitGroup
wg.Add(2)
c := make(chan int)
var send chan<- int = c //单向通道
var recv <-chan int = c
go func(){
defer wg.Done()
for x := range recv{
fmt.Println(x)
}
}()
go func(){
defer wg.Done()
defer close(c)
for i := 0;i < 3;i++{
send <- i
}
}()
wg.Wait()
}
输出:
0
1
2
- 不能再单向通道上做逆向操作
- close不用用于接收端
- 无法将单向通道重新转换回去
选择 select
如要同时处理多个通道,可选用select语句。它会随机选择一个可用通道做收发操作。
package main
import(
"fmt"
"sync"
)
func main(){
var wg sync.WaitGroup
wg.Add(2)
a,b := make(chan int),make(chan int)
go func(){ //接收端
defer wg.Done()
for {
var(
name string
x int
ok bool
)
select{ //随机选择可用channel接收数据
case x,ok = <-a:
name = "a"
case x,ok = <-b:
name = "b"
}
if !ok{ //若任意通道关闭则结束
return
}
fmt.Println(name,x)
}
}()
go func(){ //发送端
defer wg.Done()
defer close(a)
defer close(b)
for i := 0;i < 10;i++{
select{ //随机选择发送channel
case a <- i:
case b <- i*10:
}
}
}()
wg.Wait()
}
输出:
b 0
b 10
b 20
b 30
b 40
b 50
b 60
b 70
a 8
a 9
如果要等全部通道消息处理结束(closed),可将完成通道设置为nil。这样它就会被阻塞,不在被select选中。即便是同一个通道也会随机选择
同步
通道并不是用来取代锁的,它们有各自不同的使用场景。通道倾向于解决逻辑层次的并发处理架构,而锁则用来保护局部范围内的数据安全。标准库sync提供了互斥和读写锁,领有原子操作等,可满足日常开发需求。Mutex、RWMutex的使用并不复杂,只有几个地方需要注意。
- 将Mutex作为匿名字段时,相关方法必须实现为pointer-reveiver,否则会因复制导致锁机制失效。
- 应将Mutex粒度控制在最小范围内,及早释放
- Mutex不支持递归,即便在同一goroutine也会导致死锁
- 对读写性能要求较高时,应避免使用defer Unlock
- 读写并发时,用RWMutex性能会更好
- 对单个数据读写保护可尝试用原子操作
package main
import(
"fmt"
"time"
"sync"
)
type data struct{
sync.Mutex
}
func (d data)test(s string){
d.Lock()
defer d.Unlock()
for i := 0;i < 5;i++{
fmt.Println(s,i)
time.Sleep(time.Second)
}
}
func main(){
var wg sync.WaitGroup
wg.Add(2)
var d data
go func(){
defer wg.Done()
d.test("read")
}()
go func(){
defer wg.Done()
d.test("write")
}()
wg.Wait()
}
输出:
write 0
read 0
read 1
write 1
write 2
read 2
read 3
write 3
write 4
read 4