关于go并发编程的总结

关于go并发编程的总结

关于管道数据读写的总结

是否关闭管道 是否有缓冲 读取次数 ?写入次数 现象
> 读取次数多于部分读到的值为管道数据结构的默认值
< 读到多少算多少,多的值读不了了
= 正常读取
> 死锁
< 不会死锁,读到多少算多少,多的值读不了了
= 正常读取
//deadlock
func deadlockCase() {
      //无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,
      //而当前只有1个main的线程(也是一个goroutine),
      //因此在进行存放数据的ch <- 1这一行,就会产生锁,致使后边代码没法执行。
      //而整个程序无其它线程来让这个锁释放,天然就造成了一个死锁。
      c := make(chan int)
      c <- 1
      close(c)
  
      for i := range c { 
          fmt.Println(i)
      }   
  }
An empty select{}statement blocks indefinitely i.e. forever. It is similar to an empty for{}statement.
On most (all?) supported Go architectures, the empty select will yield CPU. An empty for-loop won't, i.e. it will "spin" on 100% CPU.

并发的方法

1.使用sync进行并发:

package main
  
  import (
      "fmt"
      "sync"
  )
  
  func main() {
  
      testSlice := []string{"test1", "test2", "test3"}
      wg := sync.WaitGroup{}
      for _, t := range testSlice {
          wg.Add(1)
          go printSlice(t, &wg)
      }   
      wg.Wait()
  }
  
  func printSlice(s string, wg *sync.WaitGroup) {
      defer wg.Done()
      fmt.Printf("this is %+v\n", s)
  }
sync包实现并发的核心就是waitgroup,初始化一个WaitGroup类型的指针,须要并发时,则使用Add方法,添加计数,并在须要进行并发的函数中将其做为参数传入,当这个函数操做完成后,调用Done方法减掉计数;在主协程中,Wait方法会一直监听计数器的数量,当计数器为0时,说明全部的并发函数都完成了,这时主协程就能够退出了;我的感受这是最简单的实现并发的方式,在须要并发处理一个集合内的全部数据时尤为好用;

2.使用管道进行并发

管道是go原生支持的数据类型,使用它也能达到并发的效果
一般的思路是,在主协程取管道中的数据,这时管道会阻塞,在发送协程中向管道里塞数据,塞完数据后关闭掉管道;当主协程取不到数据,管道也关闭后,任务就完成了编程

package main

  import "fmt"

  var channel = make(chan int, 10)

  func main() {
      go func() {
          for i := 0; i < 10; i++ {
              channel <- i
          }
          close(channel) //放完数据后必定要关闭chan,不然会死锁;
      }()

      for v := range channel {
          fmt.Println(v)
      } //在主协程从chan里取数据,由于取不到会一直阻塞,这样main routine就不会退出;
      //若是另起一个协程取数据,在另外一个协程里阻塞,但主协程并未阻塞,取数据协程还没取到,主协程就退出了;
  }
上面的例子其实并无体现出并发执行,由于十个数按次序塞进管道中,主协程按次序从管道里取出了数据,仍是一个串行的过程

进阶版

package main

  import (
      "fmt"
  )

  var channel = make(chan int)
  var result = make(chan int)

  func main() {
      go func() {
          for i := 0; i < 100; i++ {
              channel <- i
          }
          close(channel)
      }()

      ct := 0
      for c := range channel {
          go func(i int) {
              result <- i + i
          }(c)

      }

      for v := range result {
          ct++
          fmt.Println(v)
          if ct == 100 {
              break
          }
      }
  }
把数据按次序投入到管道中后,遍历管道,每取出一个数据,则开启一个新的协程来作计算工做(i+i),而后将结果放到result队列中,最后在主协程取出result管道的值;因为须要关闭result管道,可是关闭的位置很差肯定,目前暂时按计算的数据量来决定跳出循环的时机,你们也可讨论下何时应该关闭result管道;

注意,关闭管道与监听管道取值须要是两个不一样的协程,若两个操做都在一个协程,要么监听了一个已经关闭的协程,要么监听了一个没有被关闭的协程,都会产生异常数据结构

在这里,管道的容量是0,即每次往管道塞数据须要等里面的数据被消费后才能继续塞;有容量的协程则是能够塞进数量为容量数的数据,以后的数据须要阻塞,直到有空余;并发

3. select关键字

先看代码函数

start := time.Now()
    c := make(chan interface{})
    ch1 := make(chan int)
        ch2 := make(chan int)

    go func() {

        time.Sleep(4*time.Second)
        close(c)
    }()

    go func() {

        time.Sleep(3*time.Second)
        ch1 <- 3
    }()

      go func() {

        time.Sleep(3*time.Second)
        ch2 <- 5
    }()

    fmt.Println("Blocking on read...")
    select {
    case <- c:
        fmt.Printf("Unblocked %v later.\n", time.Since(start))
    case <- ch1:
        fmt.Printf("ch1 case...")
      case <- ch2:
        fmt.Printf("ch2 case...")
    default:
        fmt.Printf("default go...")
    }
运行上述代码,因为当前时间还未到3s。因此,目前程序会走default。
若注释掉default,ch1,ch2分支会随机挑选一个执行,若将ch1 ch2的sleep时间改成10秒,则会执行c分支;

oop

  • 若是有一个或多个关于管道操做能够完成,则Go运行时系统会随机的选择一个执行,不然的话,若是有default分支,则执行default分支语句,若是连default都没有,则select语句会一直阻塞,直到至少有一个管道操做能够进行.
  • 须要有真实的goroutine存在,若全部对管道操做的的子goroutine都已退出,select{}会报panic

部分参考:https://www.jianshu.com/p/2a1...this

相关文章
相关标签/搜索