• 多线程同时执行叫做并行
  • 并发就是在不同线程中来回切换执行来达到并行的效果就是并发
  • 通过go可以在当前线程中开启一个协程
  • 保证协程被执行,那么主线程不能挂掉

runtime包中常用的方法

  • runtime.Gosched()
  • 作用:用于出让本次的执行权限
  • 注意点:让出本次执行权限并不意味着不再执行
  • 应用场景: 让某些协程执行的次数少一次, 让某些协程执行的次数多一些

  • runtime.Goexit()
  • 作用:终止调用它的go程,其他go程不受影响
  • 注意点:这个go程将不会再被调用

  • runtime.NumCPU()

  • 获取本地机器的逻辑CPU个数

  • runtime.GOMAXPROCS(num)

  • 设置可同时执行的最大CPU数,并返回上一次设置的CPU个数

  • 注意点:Go1.8之后, 系统会自动设置为最大值,了解——>忘记


互斥锁

  • 当使用互斥锁后,当前go程将不被再次调用
  • 案例一:有序打印
    /*
    需求: 定义个打印的函数, 可以逐个的打印传入字符串的每个字符
    在两个协程中调用这个好方法, 并且还要保持输出的内容有序的
     */
  var lock = sync.Mutex{}
  func printer(str string){
  lock.Lock()//添加锁,如果不添加那么可能执行输出hello也可能执行输出world,那么就是无序的
    for _,val := range str{
        fmt.Printf("%c", ch)
        time.Sleep(time.Millisecond * 300)
    }
  lock.Unlock()
  }
func main(){
  go printer("hello")
  go printer("world")
for{
    ;
  }
}

互斥锁的资源抢夺问题

  • 注意点:如果两个go程上了同一把锁,那么当一个go程被锁上时,另一个go程也会被锁住
  • 案例二:生产者和消费者
  var lock = sync.Mutex{}
  var buff = [10]int
  func producer(){
    lock.Lock()
    rand.Seed(time.Now().UnixNano())
    for i:=0; i 

  • 在上述案例中,只能一个生产者对应一个消费者,当有第二个生产者或者第二个消费者时会因为并发而产生数据混乱。

  • 在上述案例中,无法判断先执行消费者还是先执行生产者,如果先进入了调用者的go程,则会取不到数据,就会发生数据混乱

  • 为了解决上述问题,我们可以用管道来解决这个问题

管道

  • 管道是一种数据类型,和字典切片很像,不用make函数创建就使用会报错
  • 格式: var 变量名称 chan 数据类型 ———> var myCh chan int
  • 作用:在Go语言的协程中, 一般都使用管道来保证多个协程的同步, 或者多个协程之间的通讯
var myCh chan int
myCh = make(chan int, 3)
  • 以上代码创建了一个容量为3的管道(注意:长度默认为0,添加数据以后长度会动态变化)

管道的使用

管道写入数据

  • myChan
var myCh chan int
myCh = make(chan int, 3)
myCh

管道读取数据

var myCh chan int
myCh = make(chan int, 3)
myCh

管道写入和读取的注意点

  • 没有创建管道容量直接使用会报错
var myCh chan int
myCh
  • 管道中没有数据时读取会报错
var myCh chan int
myCh = make(chan int, 3)
fmt.Println(
  • 管道已满,再向管道中写入数据会报错
var myCh chan int
myCh = make(chan int, 3)
myCh

管道的关闭

  • close(管道名称)
  • 注意点:管道关闭以后不能再管道中写入数据,但是可以在管道中读取数据

管道的遍历

  • 可以使用for循环, 也可以使用 for range循环, 以及死循环来遍历。
  • 更推荐使用后两者。因为在企业开发中, 有可能我们不知道管道中具体有多少条数据, 所以如果利用for循环来遍历, 那么无法确定遍历的次数, 并且如果遍历的次数太多, 还会报错

for range遍历

  • 注意点:在写完数据以后必须关闭管道,否则会报错
  • 一般企业开发中,管道数据写完之后都会将管道关闭
var myCh chan int
myCh = make(chan int, 3)
myCh

死循环遍历

  • 注意点: 如果被遍历的管道没有关闭, 那么会报错
  • 如果管道被关闭, 那么会将true返回给ok, 否则会将false返回给Ok
var myCh chan int
myCh = make(chan int, 3)
myCh

管道的阻塞现象(重点)

  • 单独在主线程中操作管道, 写满了会报错, 没有数据去读取也会报错

  • 只要在go程中操作管道, 无论有没有写满, 无论有没有数据都会发生管道阻塞的现象

  • 阻塞现象(和输入缓冲区很相似)

    • 在go程中,如果写满了,再写入数据,则不会报错,等待管道中的数据被取出后再添加

    • 在go程中,没有数据还在被取出,则不会报错,等待管道中的数据被写入后再取出

利用管道阻塞实现并发串行

  var myChan chan int
  myChan = make(chan int,10)
  func producer(){
    rand.Seed(time.Now().UnixNano())
    for i:=0; i 
  • 以上代码可以不止一个生产者或者消费者

利用管道阻塞解决最后写死循环保证主线程不挂

  • 注意点: 如果这个管道在协程中有被使用,那么编译器会认为主线程在取数据的时候会等待协程中输入,并不会报错
  • go程执行完后才向管道中填充数据
    var myCh = make(chan int, 3)
    var exitCh = make(chan bool, 1)
    go func() {
        for i:=0; i

无缓冲管道

  • 无缓冲管道没有容量,在主程中既不可以读也不可以写
  • 无缓冲区管道只能存在在go程中,并且如果在同一个go程中,无论先读或者先写都会阻塞
  • 读写必须都存在,但如果都在主程中会报错,在同一个go程中会阻塞
  • 无缓冲管道如果存在在不同的go程中,先读或者先写无所谓
   //在go程中可以只读或者只写,会阻塞
    myCh := make(chan int, 0)
    go func() {
        fmt.Println("123")
        myCh

无缓冲管道解决死循环

//定义一个没有缓冲的管道
    exitCh := make(chan bool)

    go func() {
        for i:= 0; i 

单向管道和双向管道

  • 默认情况下所有的管道都是双向的管道(可读可写)

  • 那么在企业开发中, 我们可能会需要将管道作为函数的参数, 并且还需要限制函数中如何使用管道,那么这个时候我们就可能会使用单向管道

  • 格式:
    双向格式:
    var myCh chan int;
    myCh = make(chan int, 5)
    myCh = make(chan int)

    单向格式:
    var myCh chan
    var myCh

  • 注意点:
    1.双向管道可以赋值给单向管道
    2.单向管道赋值给双向管道会报错

单向管道作为函数参数
  • 增强了代码的语义
  • 管道是地址传递
// 定义一个函数模拟生产者
func producer(buff chan

管道是指针类型

    var myCh1 chan int = make(chan int, 5)
    //fmt.Println(myCh1) // 0xc042094000
    //fmt.Printf("%pn", myCh1) // 0xc042094000
    //fmt.Printf("%pn", &myCh1) // 0xc042082018
    myCh1

select结构

  • select选择结构和switch很像,如果所有case不满足则会执行default
  • 企业开发中一般不会使用default,因为容易经常跑进default
  • 企业开发中,一般通过select用于消费多个管道中的数据
  • 企业开发中,一般通过select控制是否超时
  • 企业开发中,一般通过select控制退出主线程
  • 以下是一个生产消费的案例
    // 1.创建一个管道
    myCh1 := make(chan int, 5)
    myCh2 := make(chan int, 5)
    exitCh := make(chan bool)

    // 2.开启一个协程生产数据
    go func() {
        //time.Sleep(time.Second * 5) 如果存在这行数据会打印超时了,不存在则会正常消费
        for i := 0; i 

定时器

  • 想使用定时器需要使用time包

一次性定时器

NewTimer函数

  • 有一个Time结构体
    • type Timer struct {
      C
      r runtimeTimer
      }
  • 作用, 就是让系统在指定时间之后, 往Timer结构体的C属性中写入当前的时间
  • NewTimer的函数接收一个时间,代表阻塞多少秒后写入时间
  • 注意:该函数返回的是一个Time结构体,要调用其属性必须 名称.属性
     start := time.Now()
     fmt.Println(start) //打印当前时间
     timer := time.NewTimer(time.Second * 3) // Timer
     fmt.Println(

After函数

  • After的函数接收一个时间,代表阻塞多少秒后写入时间
  • 注意:该函数返回的是一个Time结构体中C的属性
    start := time.Now()
    fmt.Println(start)
    timer := time.After(time.Second * 3) // Timer.C
    fmt.Println(

周期性定时器

  • time包中有一个NewTicker的函数,接收一个时间,表明阻塞多少秒向Time结构体中写入数据
  • 注意点:该函数会反复往结构体中写入数据,所以需要关闭,可以用stop函数进行关闭
    start := time.Now()
    fmt.Println(start)
    ticker := time.NewTicker(time.Second * 2)
    for{
        fmt.Println(

文章来源于互联网,如有雷同请联系站长删除:Go语言并发、锁、channel

发表评论