如何进行并发编程
1.并发
-
并行与并发
- 并发:多个任务作用在一个
cpu
,在一个时间点上,只有一个任务在执行 - 并行:多个任务作用在多个
cpu
,在一个时间点上,多个任务在同时执行
- 并发:多个任务作用在一个
-
进程就是程序在操作系统的一次执行过程,是系统进行资源分配和调度的基本单位
-
线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
-
线程(轻量级进程)的最大优势是可以充分利用单台服务器的多核
cpu
计算资源,并发的处理任务。随人使用进程能够达到同样的目的,但使用了线程,速度可以提高一个数量级。 -
一个程序至少有一个进程,一个进程至少有一个线程
-
即使被称为轻量级进程,线程消耗的内存资源仍然不少,通常情况下,操作系统创建一个线程需要消耗
1Mb
的内存。 -
创建过多的线程对整体性能提高也没有帮助,同时执行的线程数收
cpu
内核数目的限制,比如服务器的cpu
是16核,那么最多可有16个线程同时(并发)执行,其他的线程也只能等待下一个cpu
时间片。 -
轻量级线程:
- 它的内存占用比线程更少,可以在一个线程内分“时间片”,在一个操作系统的线程上,创建多个“轻量级线程”,实现多任务切换,而只占用一个线程的内存
使用sleep
实例一
代码
package main
import (
"fmt"
"time"
)
func main(){
//非并发
/*task1()
task2()*/
go task1()
go task2()
fmt.Println("hhhhhhhhhh")
time.Sleep(time.Second*6)
/*如果没有sleep语句
运行时,系统会并发地执行go函数。而go语句之后没有任何语句,main函数至此执行完毕,这样意味着go程序结束,而那个并发go函数还没来得及执行,即封装这个go函数的这个`Goroutine`还没来得及被调度并运行。所以没有输出结果。
*/
}
func task1(){
for i:=0;i<5;i++{
fmt.Println("hello")
time.Sleep(time.Second)
}
}
func task2(){
for i:=0;i<5;i++{
fmt.Println("world")
time.Sleep(time.Second)
}
}
运行截图
hhhhhhhhhh
world
hello
hello
world
hello
world
hello
world
hello
world
实例二
代码
package main
import (
"fmt"
"time"
)
func main() {
go spinner(100*time.Millisecond)//计算的同时显示动画
const n = 45
fibN := fib(n) // slow
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
func fib(x int) int {
if x < 2 {
return x
}
return fib(x-1) + fib(x-2)
}
运行截图
Fibonacci(45) = 1134903170
runtime.Gosched
runtime包提供和go运行时环境的互操作,如控制go程的函数
func Gosched
func Gosched()
Gosched
使当前go程放弃处理器,以让其它go程运行。它不会挂起当前go程,因此当前go程未来会恢复执行。
用runtime.Gosched
替换Sleep是一种保险的手段.Gosched
作用是让其他Goroutine
有机会被CPU运行,CPU出让后,当前goroutine
会被加入到就绪队列,等待再次被调度
实例三
代码
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 2; i++ {
runtime.Gosched()//当前挂起,使其他线程执行
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")//第二次hello后会退出
}
运行截图
hello
world
hello
实例四
代码
package main
import (
"fmt"
"runtime"
)
func main() {
names := []string{"aa", "bb", "cc", "dd", "ee"}
for _, name := range names {
go func(who string) {
fmt.Printf("hello ,%s.\n", who)
}(name)
}
runtime.Gosched()
//运行结果不确定
}
sync.WaitGroup
-
sync包提供了基本的同步基元,如互斥锁。除了Once和
WaitGroup
类型,大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些。 -
golang
中的同步时通过sync.WaitGroup
来实现的
type WaitGroup
type WaitGroup struct {
// 包含隐藏或非导出字段
}
WaitGroup
用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。
func (*WaitGroup) Add
func (wg *WaitGroup) Add(delta int)
Add方法向内部计数加上delta,delta可以是负数;如果内部计数器变为0,Wait方法阻塞等待的所有线程都会释放,如果计数器小于0,方法panic。注意Add加上正数的调用应在Wait之前,否则Wait可能只会等待很少的线程。一般来说本方法应在创建新的线程或者其他应等待的事件之前调用。
func (*WaitGroup) Done
func (wg *WaitGroup) Done()
Done方法减少WaitGroup
计数器的值,应在线程的最后执行。
func (*WaitGroup) Wait
func (wg *WaitGroup) Wait()
Wait方法阻塞直到WaitGroup
计数器减为0。
与runtime.Gosched
的区别
sync.WaitGroup
是用来等到goroutine
完成的runtime.Gosched
是通知CPU,让出CPU时间的,但不能发布Goroutine
完成信号,以及传递Goroutine
的执行结果。
实例五
代码
package main
import (
"fmt"
"sync" //WaitGroup
)
var waitgroup sync.WaitGroup
func main() {
names := []string{"aa", "bb", "cc", "dd", "ee"}
for _, name := range names {
//每创建一个goroutine,九八任务队列中的任务数量+1
waitgroup.Add(1)
go func(who string) {
fmt.Printf("hello %s.\n", who)
//任务完成,将任务队列中的任务数量-1,其实.Done就是.Add(-1)
waitgroup.Done()
}(name)
}
waitgroup.Wait() //这里会发生阻塞,直到队列中的所有任务结束就会解除阻塞
}
//结果不一定按顺序
hello ee. hello bb. hello aa. hello cc. hello dd.
hello aa. hello cc. hello dd. hello ee. hello bb.
WaitGroup
的特点是Wait()可以用来阻塞直到队列中的所有任务都完成时才解除阻塞,而不需要sleep一个固定的时间来等待- 但是其缺点是无法指定固定的
goroutine
数目和实现多个goroutine
间的通信
2.通道
- 主线程在等待所有
goroutine
全部完成的时间很难确定 - 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有
goroutine
处于工作状态,这时也会随主线程的退出而销毁 channel
本质就是一个数据结构-队列- 数据是先进先出【
FIFO : first in first out
】 - var 变量名
chan
数据类型 ch:=make(chan int)
声明并初始化ch:=make(chan int,10)
带缓冲区的通道,容量是10channel
是引用类型channel
必须初始化才能写入数据,即make
后才能使用- 管道是有类型的,只能放置特定类型的数据
channel
的数据满了之后,不能再放入,除非取出数据,才可以继续放入- 写入数据 : channel变量名 <- 数据;向channel写入数据通常会导致程序堵塞,直到有其他
goroutine
来从这个通道中读取数据 - 读取数据 : value := <- channel变量名;如果通道之前没有写入数据,那么从通道中读取数据也会导致程序堵塞,直到channel中被写入数据
- 关闭: close(channel变量名)‘关闭通道后不能再写入数据,但是仍然可以读取数据
- 无论怎样都不应该在接收端关闭通道,因为那样无法判断发送端是否还会向该通道发送值。
- 我们在发送端调用close关闭通道却不会对接收端接收该通道中已有的元素值产生任何影响。
- 遍历:支持
for-range
的方式进行遍历,但遍历前,channel需要关闭,如若不然,会出现deadlock报错
实例一
代码
package main
import (
"fmt"
)
func consumer(data chan int, done chan bool) {
for x := range data {//遍历前channel需要关闭
fmt.Println("recv:", x)
}
done <- true//消费结束,向done通道写入true
}
//生产者
func producer(data chan int) {
for i := 0; i < 5; i++ {
fmt.Println("send message", i)
data <- i
//写入数据
}
close(data)//莫忘关闭
}
func main() {
//各个并发/行体之间如何通信
//布尔通道
done:=make(chan bool)//channel make后才能使用
//整形通道
data:=make(chan int)
//启动生产者
go producer(data)
go consumer(data,done)
<-done
//阻塞,直到接受消费者发出的结束信号
}
运行截图
send message 0 send message 1 recv: 0 recv: 1 send message 2 send message 3 recv: 2 recv: 3 send message 4 recv: 4
实例二
代码
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
fmt.Println("game begin 1-石头 2-剪刀 3-布")
done := make(chan bool, 1) //第二个参数表示容量
gamer1 := make(chan int)
gamer2 := make(chan int)
go game(gamer1)
//time.Sleep(time.Second)
go game(gamer2)
//time.Sleep(time.Second)
go pk(gamer1, gamer2, done)
<-done
//阻塞,直到接收done的结束信号
}
func game(gamer chan int) {
rand.Seed(time.Now().UnixNano())
//产生1-3的随机数
gamer <- rand.Intn(3) + 1
}
func turn(i int) string {
if i == 1 {
return "石头"
} else if i == 2 {
return "剪刀"
} else {
return "布"
}
}
func pk(one, two chan int, done chan bool) {
a := <-one //读取
b := <-two
if a == b {
fmt.Println(turn(a), "PK", turn(b), "平手")
} else if a == 1 && b == 2 || a == 2 && b == 3 || a == 3 && b == 1 {
fmt.Println(turn(a), "PK", turn(b), "胜利")
} else {
fmt.Println(turn(a), "PK", turn(b), "败北")
}
done <- true
}
game begin 1-石头 2-剪刀 3-布
布 PK 布 平手
实例三
代码
package main
import (
"fmt"
"math"
)
func main() {
var n int //pi的精度取决于n的取值
fmt.Println("input n(int):") //提示用户输入
fmt.Scanln(&n) //从键盘获取n
fmt.Println(Calpi(n)) //调用Calpi函数并输出结果
}
//Calpi 并发计算pi,并返回
func Calpi(n int) float64 {
var sum float64
ch := make(chan float64) //建立float64的通道,存储每一项
for i := 0; i < n; i++ {
go func(k float64) { //建立n个带参匿名函数的goroutine来计算每一项的值
ch <- math.Pow(-1, k) / (2*k + 1) //将每一项写入通道
}(float64(i)) //调用匿名函数并传参
}
for i := 0; i < n; i++ {
sum += <-ch //累加ch中的值
}
return sum * 4 //返回计算结果
}
input n(int):
5000
3.141392653591791
3.解决通信中的死锁问题
-
在并发编程的通信过程中,最需要处理的就是死锁问题
- 像channel写数据时,发现channel已满
- 试图从channel读数据时,发现channel为空
-
解决方案是引入超时限制
- 如果一个
Goroutine
,超过设定的时间,仍然没有完成处理的任务(如因为不能向channel
读写数据而被阻塞的情况),则该方法会立即终止并返回对应的超时信息 - 超时机制可能带来一些问题,如在高速机器或网络上运行的程序,到了慢速机器或网络上就会出问题,从而出现结果不一致的现象
- 从根本上来说引入超时机制解决通信死锁这一问题的价值要大于所带来的问题
- 如果一个
-
select用于解决处理异步IO问题,用法与switch类似,要求所有的case必须是一个IO操作
-
如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。
否则:- 如果有 default 子句,则执行该语句。
- 如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
实例一
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
c := make(chan interface{})
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
time.Sleep(4 * time.Second)
close(c)
}()
go func() {
time.Sleep(3 * time.Second)
ch1 <- 3
}()
go func() {
time.Sleep(3 * time.Second)
ch2 <- 5
}()
fmt.Println("Blocking on read...")
select {
case <-c:
fmt.Printf("Unblocked %v later.\n", time.Since(start))
case <-ch1:
fmt.Printf("ch1 case...")
case <-ch2:
fmt.Printf("ch1 case...")
default:
fmt.Printf("default go...")
}
}
Blocking on read…
default go…