第11课 Go并发

lhdalhd1996 · · 2031 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

并发concurrency

  • 很多人都是冲着Go大肆宣扬的高并发而忍不住跃跃欲试,但其实从源码的解析来看,goroutine只是由官方实现的“线程池”而已。不过话说回来,每个实例4-5kb的栈内存占用和由于实现机制而大幅减少的创建和销毁开销,是制造Go号称高并发的根本原因。另外,goroutine的简单易用,也在语言层面上给予了开发者巨大的便利。

并发不是并行:Concurrency Is Not Parallelism

  • 并发主要有切换时间片来实现“同时”运行,在并行则是直接利用多核实现多线程的运行,但Go可以设置使用核数,以发挥多核计算机的能力。

Goroutine奉行通过通信来共享内存,而不是共享内存来通信。

/*一个最最最最基本的goroutine*/
package main

import (
    "fmt"
    "time"
)

func main() {
    go Go()
    time.Sleep(2 * time.Second)
}
func Go() {
    fmt.Println("Go Go Go!!!")

}

Channel

  • Channel 是 goroutine沟通的桥梁,大都是阻塞同步的
  • 通过make创建,close关闭
  • Channel是引用类型
  • 可以使用for range来迭代不断操作channel
  • 可以设置单项或双向通道
  • 可以设置缓存大小,在被填满前不会发生阻塞
package main

import (
    "fmt"
)

func main() {
    c := make(chan bool) //make(chan [所存储的数据类型])
    go func() {
        fmt.Println("Go Go Go!!!")
        c <- true //把true给到c
    }()
    <-c //把c取出来的操作,在这里会等待并发函数把数据给到c之后才执行,保证了并发线程结束后主线程才结束
}
package main

import (
    "fmt"
)

func main() {
    c := make(chan bool) //make(chan [所存储的数据类型]),直接make的都是双向通道,又可以存又可以取
    go func() {
        fmt.Println("Go Go Go!!!")
        c <- true //把true给到c
        close(c)
    }()
    for v := range c {
        fmt.Println(v)
    }//一定要注意在某处把channel关闭掉,否则会成为死锁,无限等待
}

//这个函数的流程是这样的:go关键字并发了一个匿名函数,接着执行for-range,此时for-range的执行需要等待一个c,在匿名函数中,true给到c之后,立刻for-range可以执行了,输出了这个v(v就是c),并且立刻又进入等待下一个c的状态,在匿名函数中,此时关闭了c,for-range立刻接收到了c被关闭的信号,整个函数结束。


运行结果:  
Go Go Go!!!
true

channel分为有缓存和无缓存

无缓存的channel

package main

import (
    "fmt"
)

func main() {
    c := make(chan bool) //这里为无缓存的channel
    go func() {
        fmt.Println("Go Go Go!!!")
        c <- true
    }()
    <-c //运行到这里的时候该取了,可实际还没有放进去,所以在等待,等待放入之后再执行取的操作,无缓存是阻塞的,c里的东西不被读掉,程序无法往下执行
}

把channel的存读操作对调一下

package main

import (
    "fmt"
)

func main() {
    c := make(chan bool) //这里为无缓存的channel
    go func() {
        fmt.Println("Go Go Go!!!")
        <-c
    }()
    c <- true //无缓存是阻塞的,c里的东西不被读掉,程序无法往下执行
}

以上两次操作均可以输出Go Go Go!!!
原因就在于,无缓存的channel是阻塞的。

有缓存的channel

package main

import (
    "fmt"
)

func main() {
    c := make(chan bool, 1) //这就成了有缓存的channel了
    go func() {
        fmt.Println("Go Go Go!!!")
        <-c
    }()
    c <- true
}

//这个程序不会输出Go Go Go!!!
//因为有缓存,所以channel的存和取的操作不是堵塞的,简单说,存了以后,爱读不读。程序都会往下接着走,读会等待,取不会等待哦!

所以调换c的存取操作顺序就又可以输出Go Go Go!!!了

package main

import (
    "fmt"
)

func main() {
    c := make(chan bool, 1) //这就成了有缓存的channel了
    go func() {
        fmt.Println("Go Go Go!!!")
        c <- true
    }()
    <-c
}

一个经典的多核并发程序

package main

import (
    "fmt"
    "runtime"
)

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU()) //设置使用的CPU核数(获取本台计算机的CPU核数)
    c := make(chan bool)
    for i := 0; i < 10; i++ {
        go Go(c, i)
    } //当一个核的时候还是按部就班的一个线程一个线程开始执行,可是现在变成多核以后,指不定先执行哪个后执行哪个,可能先执行i=5的,也可能先执行i=8的,即分配任务不定的特性
    <-c
}
func Go(c chan bool, index int) {
    a := 1
    for i := 0; i < 100000000; i++ {
        a += i
    }
    fmt.Println(index, a)

    if index == 9 {
        c <- true //因此这个地方就错误了,有时候index=9并不是最后一个执行的。
    }
}
//输出的结果是不确定的,因为执行index=9的次序是不确定的。

那么如何解决这个问题呢?
解决方案1—-利用缓存

package main

import (
    "fmt"
    "runtime"
)

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    c := make(chan bool, 10) //缓存个数为10
    for i := 0; i < 10; i++ {
        go Go(c, i)
    } //执行了10个线程

    for i := 0; i < 10; i++ {
        <-c
    } //在这里取10次
}
func Go(c chan bool, index int) {
    a := 1
    for i := 0; i < 100000000; i++ {
        a += i
    }
    fmt.Println(index, a)

    c <- true //在这里存一次
}

解决方案2—-利用WaitGroup(在”sync”包里)

package main

import (
    "fmt"
    "runtime"
    "sync"
)

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    wg := sync.WaitGroup{}
    wg.Add(10) //设置增加10个任务量
    for i := 0; i < 10; i++ {
        go Go(&wg, i) //channel是引用类型,但是此处wg不是引用类型,所以要把它地址传进去
    }

    wg.Wait() //在这里要等待任务都完成
}
func Go(wg *sync.WaitGroup, index int) {
    a := 1
    for i := 0; i < 100000000; i++ {
        a += i
    }
    fmt.Println(index, a)

    wg.Done() //标记一次done
}

Select

  • 可处理一个或多个channel的发送与接收
  • 同时又多个可用的channel的按随机顺序处理
  • 可用空的select来阻塞main函数
  • 可设置超时

select是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。

select随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。

select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s); 
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}

以下描述了 select 语句的语法:

每个case都必须是一个通信
所有channel表达式都会被求值
所有被发送的表达式都会被求值
如果任意某个通信可以进行,它就执行;其他被忽略。
如果有多个case都可以运行,Select会随机公平地选出一个执行。其他不会执行。
否则:
    1、如果有default子句,则执行该语句。
    2、如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

实例

package main

import "fmt"

func main() {
   var c1, c2, c3 chan int
   var i1, i2 int
   select {
      case i1 = <-c1:
         fmt.Printf("received ", i1, " from c1\n")
      case c2 <- i2:
         fmt.Printf("sent ", i2, " to c2\n")
      case i3, ok := (<-c3):  // same as: i3, ok := <-c3
         if ok {
            fmt.Printf("received ", i3, " from c3\n")
         } else {
            fmt.Printf("c3 is closed\n")
         }
      default:
         fmt.Printf("no communication\n")
   }    
}

以上输出结果如下:

no communication

如何设置超时

//设置超时
package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan bool)
    select {
    case v := <-c:
        fmt.Println(v)
    case <-time.After(3 * time.Second):
        fmt.Println("Timeout")
        //可以设置超时,即超过多少时间还没有接收到任务就执行的语句
    }
}
输出结果:
[ `go run temp.go` | done: 3.2158144s ]
  Timeout

有疑问加站长微信联系(非本文作者)

本文来自:CSDN博客

感谢作者:lhdalhd1996

查看原文:第11课 Go并发

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2031 次点击  ∙  1 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传