并发案例
一、无缓冲:生成者消费者
1.1、案例
package main
import (
"fmt"
"time"
)
func Producer(p chan<- int) {
for i := 0; i < 10; i++ {
p <- i
fmt.Println("生产者-->商品:", i)
}
}
func Consumer(c <-chan int) {
for i := 0; i < 10; i++ {
v := <-c
fmt.Println("消费者-->消费商品:", v)
}
}
func main() {
queue := make(chan int)
// ch :=make(chan int)
// ch <- 1
// 这个错误的意思是说线程陷入了死锁,程序无法继续往下执行。那么造成这种错误的原因是什么呢?
// 我们创建了一个无缓冲的channel,然后给这个channel赋值了,程序就是在赋值完成后陷入了死锁。
// 因为我们的channel是无缓冲的,即同步的,赋值完成后来不及读取channel,程序就已经阻塞了。
// queue <- 1
// queue <- 2 //满阻塞
// 开启协程执行函数单独去执行,主程序直接往下执行
go Producer(queue)
// i = 0 的时候,写入阻塞,生产者和消费者是异步执行,消费者会去读取channel值。
go Consumer(queue)
// 让Producer与Consumer完成
time.Sleep(1e9)
}
输出
消费者-->消费商品: 0
生产者-->商品: 0
生产者-->商品: 1
消费者-->消费商品: 1
消费者-->消费商品: 2
生产者-->商品: 2
生产者-->商品: 3
消费者-->消费商品: 3
消费者-->消费商品: 4
生产者-->商品: 4
生产者-->商品: 5
消费者-->消费商品: 5
消费者-->消费商品: 6
生产者-->商品: 6
生产者-->商品: 7
消费者-->消费商品: 7
消费者-->消费商品: 8
生产者-->商品: 8
生产者-->商品: 9
消费者-->消费商品: 9
1.2、说明
因为channel是没有缓冲的,所以当生产者给channel赋值后,生产者这个线程会阻塞,直到消费者线程将channel中的数据取出。
消费者第一次将数据取出后,进行下一次循环时,消费者的线程也会阻塞,因为生产者还没有将数据存入,这时程序会去执行生产者的线程。
程序就这样在消费者和生产者两个线程间不断切换,直到循环结束。
二、缓冲:生成者消费者
2.1、案例
package main
import (
"fmt"
"time"
)
func Producer(p chan<- int) {
for i := 0; i < 10; i++ {
p <- i
fmt.Println("生产者-->商品:", i)
}
}
func Consumer(c <-chan int) {
for i := 0; i < 10; i++ {
v := <-c
fmt.Println("消费者-->消费商品:", v)
}
}
func main() {
queue := make(chan int, 10) //带缓冲的例子
// 开启协程执行函数单独去执行,主程序直接往下执行
go Producer(queue)
go Consumer(queue)
// 让Producer与Consumer完成
time.Sleep(1e9)
}
输出
生产者-->商品: 0
生产者-->商品: 1
生产者-->商品: 2
生产者-->商品: 3
消费者-->消费商品: 0
消费者-->消费商品: 1
消费者-->消费商品: 2
消费者-->消费商品: 3
消费者-->消费商品: 4
生产者-->商品: 4
生产者-->商品: 5
生产者-->商品: 6
生产者-->商品: 7
生产者-->商品: 8
生产者-->商品: 9
消费者-->消费商品: 5
消费者-->消费商品: 6
消费者-->消费商品: 7
消费者-->消费商品: 8
消费者-->消费商品: 9
2.2、说明
在这个程序中,缓冲区可以存储10个int类型的整数,在执行生产者线程的时候,线程就不会阻塞,一次性将10个整数存入channel。
在读取的时候,也是一次性读取。读取的话,没有数据就阻塞
三、 随机向通道中写入0或者1
3.1、案例
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
///不停向channel中随机写入 0 或者1
select {
case ch <- 0:
case ch <- 1:
}
//从通道中取出数据
i := <-ch
fmt.Println("管道的数据是:", i)
time.Sleep(1e9)
}
}
输出
管道的数据是: 0
管道的数据是: 1
管道的数据是: 1
管道的数据是: 0
管道的数据是: 1
管道的数据是: 0
管道的数据是: 1
管道的数据是: 0
管道的数据是: 0
管道的数据是: 0
四、 高性能并发编程 必须设置GOMAXPROCS 为最大核数目 这个值由runtime.NumCPU()获取
在执行一些昂贵的计算任务时, 我们希望能够尽量利用现代服务器普遍具备的多核特性来尽量将任务并行化,从而达到降低总计算时间的目的。
此时我们需要了解CPU核心的数量,并针对性地分解计算任务到多个goroutine中去并行运行。
下面我们来模拟一个完全可以并行的计算任务:计算N个整型数的总和。
4.1、 案例:计算N个整型数的总和
我们可以将所有整型数分成M份,M即CPU的个数。
让每个CPU开始计算分给它的那份计算任务,最后将每个CPU的计算结果再做一次累加,这样就可以得到所有N个整型数的总和
package main
type Vector []float64
// 分配给每个CPU的计算任务
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
for ; i < n; i++ {
v[i] += u[i]
}
c <- 1
// 发信号告诉任务管理者我已经计算完成了
}
const NCPU = 16
// 假设总共有16核
func (v Vector) DoAll(u *Vector) {
c := make(chan int, NCPU) // 用于接收每个CPU的任务完成信号
for i := 0; i < NCPU; i++ {
go v.DoSome(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c)
}
// 等待所有CPU的任务完成
for i := 0; i < NCPU; i++ {
<-c // 获取到一个数据,表示一个CPU计算完成了
}
// 到这里表示所有计算已经结束
}
func main() {
}
DoAll()会根据CPU核心的数目对任务进行分割,然后开辟多个goroutine来并行执行这些计算任务。
我们可以先通过设置环境变量GOMAXPROCS的值来控制使用多少个CPU核心。
具体操作方法是通过直接设置环境变量GOMAXPROCS的值,或者在代码中启动goroutine之前先调用以下这个语句以设置使用16个CPU核心:
runtime.GOMAXPROCS(16)
到底应该设置多少个CPU核心呢,其实runtime包中还提供了另外一个函数NumCPU()来获取核心数。
五、 主动出让时间片给其他 goroutine,在未来的某一时刻再来执行当前goroutine
使用runtime包中的Gosched()函数实现。