• [go语言基础]经典案例:使用并发计算


    (A)一些基础知识的使用

    go语言中协程goroutine是一个全新的概念,是一个比线程更小的并发单位,线程可以分为多个协程,可以理解为一个线程中的多个函数进行来回切换.

    相比于其他语言切换线程导致的cpu和线程上下文占用保存带来的消耗,go语言可以借助自身的协程机制实现轻松的并发

    go语言开启协程的机制分简单,一个单独的go标签即可

    1. go runtime()
    2. func runtime() {
    3. for i := 1; i <= 10; i++ {
    4. fmt.Println("协程A", i)
    5. time.Sleep(100)
    6. }
    7. wait.Done()
    8. }

    注意一个问题,如果协程没结束但是主线程结束了,那么协程都会打出gg
    所以一般要使用waitgroup库中的函数或者使用主线程休眠,让主线程可以维持到所有的协程都完成工作.

    使用go语言执行百万级别的并发也很简单,直接循环加上go就可以了,这个的来源也是关于协程

    1. func millionSync() {
    2. for i := 1; i <= 1000000; i++ {
    3. go fmt.Println("输出点什么")
    4. }
    5. }

    但是正如其他语言多线程产生的影响一样,go语言中也会产生资源竞争问题

    1. // 但是线程并发会带来一些安全问题,最显著的就是资源竞争
    2. // 举个例子
    3. func TestSourceCompete() {
    4. wait = sync.WaitGroup{}
    5. wait.Add(2)
    6. a := 0
    7. go func(a *int) {
    8. a1 := *a
    9. time.Sleep(500) //模拟获取数据的时间
    10. a1++
    11. time.Sleep(500) //模拟计算的时间
    12. *a = a1
    13. time.Sleep(500) //模拟赋值的时间
    14. fmt.Println("子线程2", *a)
    15. wait.Done()
    16. }(&a)
    17. go func(a *int) {
    18. a1 := *a
    19. time.Sleep(500) //模拟获取数据的时间
    20. a1++
    21. time.Sleep(500) //模拟计算的时间
    22. *a = a1
    23. time.Sleep(500) //模拟赋值的时间
    24. fmt.Println("子线程1", *a)
    25. wait.Done()
    26. }(&a)
    27. fmt.Println("主线程", a)
    28. wait.Wait()
    29. }
    30. //上面这里例子里,我们原本的思路应该是两个协程分别把数据假+1然后变成2
    31. //但是这里因为可以放缓了读取数据的情况,所以发生了数据不安全的结果
    32. //导致两个进程的运算结果都一样

    对于这种资源竞争,我们有两种处理思路

    (1)锁/临界区

    使用锁和临界区的机制,

    处理方法就是加上锁,也就是我们解决资源冲突的第一种方法,互斥锁的作用是保护临界区(Critical Section),即一段代码或一段逻辑,在同一时间只能由一个协程执行。

    也就是说,可以理解为一旦有一个协程进入了锁区域,则其他协程就会自然阻塞

    (2)使用管道

    管道的使用稍微有点复杂,不过问题也不大

    1.管道的生成方式有两种

    1. make(chan int) 无缓冲
    2. make(chan int,1) 缓冲管道

    无缓冲轨道的特点是:在试图写入的时候,必须在同时有某个位置进行接收,不然就会发生死锁,所以建议管道的两端使用,而不是在一个协程中使用,这是一个经典的死锁情况

    1. ch是一个无缓冲
    2. ch <- 1
    3. fmt.println(<-ch)

    有缓冲轨道的特点则是,内存存在一个长度有限的缓冲区,输入的数据是可以他暂存的,并且这个东西本质上是一个队列的数据结构,符合先进先出

    2.管道的两个注意事项

    并且对于缓冲轨道来说,如果太多数据被阻塞"拒之门外"就会发生数据丢失的情况
    当缓冲区管道已满时,继续尝试发送数据会导致发送操作被阻塞,直到有空间可用。
    在这段时间内,如果没有其他协程进行接收操作,那么后续的发送操作将无法执行,数据将会丢失。

    管道中的数据也可以一次性读取两个数字 val和ok,ok代表这个是否成功读取

    关于成功读取和阻塞之间的区别:
    //管道如果在关闭状态,是不会发生阻塞的,立即执行,并且ok会成为false
    //如果管道是在开启状态,则会在有些是否发生阻塞

    3.使用select对于管道进行监听

    select的特点有两个 ,首先是如果有多个case分支被选中,就会通过伪随机获取一个并且执行

    第二个是case坚挺的是"语句能否立刻执行成功"而不是"能否正确读取数字"

    比如说管道已经关闭的情况下,<-done仍然是可以被选中的,因为这个语句虽然无法读取数据,但是可以正常执行.但是当done放开但是管道为空的时候,是不能立刻读取的,这个时候就会被case忽略

    (B)关于具体的计算方式

    使用并发计算1-100内的素数,首先是最简单的思路,每个协程负责一部分

    1. func CountSu1() {
    2. //利用多个协程计算素数,第一种思路,每个协程负责固定的一部分
    3. wait := sync.WaitGroup{}
    4. wait.Add(10)
    5. for i := 1; i <= 10; i++ {
    6. go func(i int) {
    7. for j := (i - 1) * 10; j <= i*10-1; j++ {
    8. if isPrime(j) {
    9. fmt.Print(j, " ")
    10. }
    11. }
    12. wait.Done()
    13. }(i)
    14. }
    15. wait.Wait()
    16. }

    第二种:使用管道的思路进行处理

    大致思路就是:我们创建两个管道 A和B

    A管道是由主管道往里传递数据的,从1到100,由于管道本身是线程安全的,多个协程可以放心的去竞争管道的数据资源

    B管道是负责信息通知的,这个管道不传递任何数据,如果使用select监听,也无法选中.当主线程往管道中输入原始数据结束以后,B管道就会关闭,则各个协程的select语句就能读取到这个变化

    1. //第二种思路,每个协程自由进行计算
    2. func CountSu2() {
    3. //利用多个协程计算素数,每个携程直接取出数据
    4. wait := sync.WaitGroup{}
    5. wait.Add(10)
    6. channelOfPrime := make(chan int, 10)
    7. done := make(chan int, 10)
    8. defer close(channelOfPrime)
    9. //一共分成十个协程,具体的思路是应用到管道
    10. //另外注意这里为了让管道读取停下来
    11. for i := 1; i <= 10; i++ {
    12. go func() {
    13. go Worker(channelOfPrime, done)
    14. wait.Done()
    15. }()
    16. }
    17. //主线程把数字全都发送到通道之中
    18. for i := 2; i < 100; i++ {
    19. channelOfPrime <- i
    20. time.Sleep(100)
    21. }
    22. close(done)
    23. wait.Wait()
    24. }
    25. func Worker(ch <-chan int, done <-chan int) {
    26. for {
    27. select {
    28. case val, ok := <-ch:
    29. if ok {
    30. if isPrime(val) {
    31. fmt.Print(val, " ")
    32. }
    33. }
    34. case <-done:
    35. return
    36. }
    37. //这个的读取逻辑是这样的,管道在开启状态的时候,能否读取到数据不一定,但是读取操作一定可以进行
    38. //只有管道关闭以后,这个东西才能立刻被选中,否则开启状态会阻塞
    39. //首先select语句的判断是只要能立刻被执行不阻塞,这个就算是可以被选中!
    40. //其次是关于管道的内容
    41. //管道如果在关闭状态,是不会发生阻塞的,立即执行,并且ok会成为false
    42. //如果管道是在开启状态,则会在有些是否发生阻塞
    43. //并且对于缓冲轨道来说,如果太多数据被阻塞"拒之门外"就会发生数据丢失的情况
    44. //当缓冲区管道已满时,继续尝试发送数据会导致发送操作被阻塞,直到有空间可用。
    45. //在这段时间内,如果没有其他协程进行接收操作,那么后续的发送操作将无法执行,数据将会丢失。
    46. }
    47. }

  • 相关阅读:
    MybatisPlus自设模板:填补原模板在controller层对CURD操作的缺乏
    [vue]本地项目的访问
    springboot4:总结前3(图解)
    OpenResty使用漏桶算法实现限流
    Python算法:八大排序算法以及速度比较
    视频转音频怎么转?来试试这三个方法
    调试方法和技巧详解
    【多线程笔记01】多线程之CountDownLatch介绍及其使用
    java笔记37,Lambda表达式
    SpringBoot的约定优于配置,SpringBoot解决了哪些问题?
  • 原文地址:https://blog.csdn.net/weixin_62697030/article/details/130841678