(A)一些基础知识的使用
go语言中协程goroutine是一个全新的概念,是一个比线程更小的并发单位,线程可以分为多个协程,可以理解为一个线程中的多个函数进行来回切换.
相比于其他语言切换线程导致的cpu和线程上下文占用保存带来的消耗,go语言可以借助自身的协程机制实现轻松的并发
go语言开启协程的机制分简单,一个单独的go标签即可
go runtime()
func runtime() {
for i := 1; i <= 10; i++ {
fmt.Println("协程A", i)
time.Sleep(100)
}
wait.Done()
}
注意一个问题,如果协程没结束但是主线程结束了,那么协程都会打出gg
所以一般要使用waitgroup库中的函数或者使用主线程休眠,让主线程可以维持到所有的协程都完成工作.
使用go语言执行百万级别的并发也很简单,直接循环加上go就可以了,这个的来源也是关于协程
func millionSync() {
for i := 1; i <= 1000000; i++ {
go fmt.Println("输出点什么")
}
}
但是正如其他语言多线程产生的影响一样,go语言中也会产生资源竞争问题
// 但是线程并发会带来一些安全问题,最显著的就是资源竞争
// 举个例子
func TestSourceCompete() {
wait = sync.WaitGroup{}
wait.Add(2)
a := 0
go func(a *int) {
a1 := *a
time.Sleep(500) //模拟获取数据的时间
a1++
time.Sleep(500) //模拟计算的时间
*a = a1
time.Sleep(500) //模拟赋值的时间
fmt.Println("子线程2", *a)
wait.Done()
}(&a)
go func(a *int) {
a1 := *a
time.Sleep(500) //模拟获取数据的时间
a1++
time.Sleep(500) //模拟计算的时间
*a = a1
time.Sleep(500) //模拟赋值的时间
fmt.Println("子线程1", *a)
wait.Done()
}(&a)
fmt.Println("主线程", a)
wait.Wait()
}
//上面这里例子里,我们原本的思路应该是两个协程分别把数据假+1然后变成2
//但是这里因为可以放缓了读取数据的情况,所以发生了数据不安全的结果
//导致两个进程的运算结果都一样
对于这种资源竞争,我们有两种处理思路
(1)锁/临界区
使用锁和临界区的机制,
处理方法就是加上锁,也就是我们解决资源冲突的第一种方法,互斥锁的作用是保护临界区(Critical Section),即一段代码或一段逻辑,在同一时间只能由一个协程执行。
也就是说,可以理解为一旦有一个协程进入了锁区域,则其他协程就会自然阻塞
(2)使用管道
管道的使用稍微有点复杂,不过问题也不大
1.管道的生成方式有两种
make(chan int) 无缓冲
make(chan int,1) 缓冲管道
无缓冲轨道的特点是:在试图写入的时候,必须在同时有某个位置进行接收,不然就会发生死锁,所以建议管道的两端使用,而不是在一个协程中使用,这是一个经典的死锁情况
ch是一个无缓冲
ch <- 1
fmt.println(<-ch)
有缓冲轨道的特点则是,内存存在一个长度有限的缓冲区,输入的数据是可以他暂存的,并且这个东西本质上是一个队列的数据结构,符合先进先出
2.管道的两个注意事项
并且对于缓冲轨道来说,如果太多数据被阻塞"拒之门外"就会发生数据丢失的情况
当缓冲区管道已满时,继续尝试发送数据会导致发送操作被阻塞,直到有空间可用。
在这段时间内,如果没有其他协程进行接收操作,那么后续的发送操作将无法执行,数据将会丢失。
管道中的数据也可以一次性读取两个数字 val和ok,ok代表这个是否成功读取
关于成功读取和阻塞之间的区别:
//管道如果在关闭状态,是不会发生阻塞的,立即执行,并且ok会成为false
//如果管道是在开启状态,则会在有些是否发生阻塞
3.使用select对于管道进行监听
select的特点有两个 ,首先是如果有多个case分支被选中,就会通过伪随机获取一个并且执行
第二个是case坚挺的是"语句能否立刻执行成功"而不是"能否正确读取数字"
比如说管道已经关闭的情况下,<-done仍然是可以被选中的,因为这个语句虽然无法读取数据,但是可以正常执行.但是当done放开但是管道为空的时候,是不能立刻读取的,这个时候就会被case忽略
(B)关于具体的计算方式
使用并发计算1-100内的素数,首先是最简单的思路,每个协程负责一部分
func CountSu1() {
//利用多个协程计算素数,第一种思路,每个协程负责固定的一部分
wait := sync.WaitGroup{}
wait.Add(10)
for i := 1; i <= 10; i++ {
go func(i int) {
for j := (i - 1) * 10; j <= i*10-1; j++ {
if isPrime(j) {
fmt.Print(j, " ")
}
}
wait.Done()
}(i)
}
wait.Wait()
}
第二种:使用管道的思路进行处理
大致思路就是:我们创建两个管道 A和B
A管道是由主管道往里传递数据的,从1到100,由于管道本身是线程安全的,多个协程可以放心的去竞争管道的数据资源
B管道是负责信息通知的,这个管道不传递任何数据,如果使用select监听,也无法选中.当主线程往管道中输入原始数据结束以后,B管道就会关闭,则各个协程的select语句就能读取到这个变化
//第二种思路,每个协程自由进行计算
func CountSu2() {
//利用多个协程计算素数,每个携程直接取出数据
wait := sync.WaitGroup{}
wait.Add(10)
channelOfPrime := make(chan int, 10)
done := make(chan int, 10)
defer close(channelOfPrime)
//一共分成十个协程,具体的思路是应用到管道
//另外注意这里为了让管道读取停下来
for i := 1; i <= 10; i++ {
go func() {
go Worker(channelOfPrime, done)
wait.Done()
}()
}
//主线程把数字全都发送到通道之中
for i := 2; i < 100; i++ {
channelOfPrime <- i
time.Sleep(100)
}
close(done)
wait.Wait()
}
func Worker(ch <-chan int, done <-chan int) {
for {
select {
case val, ok := <-ch:
if ok {
if isPrime(val) {
fmt.Print(val, " ")
}
}
case <-done:
return
}
//这个的读取逻辑是这样的,管道在开启状态的时候,能否读取到数据不一定,但是读取操作一定可以进行
//只有管道关闭以后,这个东西才能立刻被选中,否则开启状态会阻塞
//首先select语句的判断是只要能立刻被执行不阻塞,这个就算是可以被选中!
//其次是关于管道的内容
//管道如果在关闭状态,是不会发生阻塞的,立即执行,并且ok会成为false
//如果管道是在开启状态,则会在有些是否发生阻塞
//并且对于缓冲轨道来说,如果太多数据被阻塞"拒之门外"就会发生数据丢失的情况
//当缓冲区管道已满时,继续尝试发送数据会导致发送操作被阻塞,直到有空间可用。
//在这段时间内,如果没有其他协程进行接收操作,那么后续的发送操作将无法执行,数据将会丢失。
}
}