Go语言 并发-Channel

838 阅读7分钟

协程的基本概念

注意 协程的概念 每个语言都有所不同,这里只说下go的概念

简单来说 go的协程 具有下面几个特点

轻量级,比线程要小很多。

非抢占式

啥叫非抢占式?

线程是抢占的,因为线程的调度 会有操作系统的干预,而go的协程 并不属于os的资源,os是没办法调度的,只有go的编程者自己有能力来处理,如果你不处理 就不会干预

多个协程 有可能在 一个或者多个线程上运行

go的协程是一个 编译器 层面的概念,大家不用深究他的原理,初学者只要掌握其用法就好

举个例子:

func print(i int) {
   for {
      fmt.Printf("i=%d\n", i)
   }
}

func main() {
   for i := 0; i < 10; i++ {
      go print(i)
   }
   time.Sleep(10 * time.Millisecond)
}

这段程序的输出 显然是 会有很多种 i=0 一直到i=9 在打印

channel

下面就是一个简单的channel 例子

func chanDemo() {
   c := make(chan int)
   go func() {
      for {
         n := <-c
         fmt.Println(n)
      }
   }()
   c <- 1
   c <- 2
   time.Sleep(time.Millisecond)
}

func main() {
   chanDemo()
}

往c 里面写了1和2 然后在另外一个goroutine里面 打印

接下来构建一个稍微复杂的程序,构建10个chan,分别打印自己的信息

func worker(id int, c chan int) {
   for {
      fmt.Println("worker ", id, "received ", string(rune(<-c)))
   }
}

func chanDemo() {

   var chanArray [10]chan int
   for i := 0; i < 10; i++ {
      chanArray[i] = make(chan int)
      go worker(i, chanArray[i])
   }

   for i := 0; i < 10; i++ {
      chanArray[i] <- 'a' + i
   }

   time.Sleep(time.Millisecond)
}

image.png

这里可能会有人问 我发的顺序是abcdefg 为啥接收的时候 顺序就乱掉了?

其实这里其实是因为 打印语句 本身是个io操作,这个io操作是资源共享的,会存在资源竞争的问题

所以最终出来的结果就是乱序的,因为谁能抢到这个io资源 并不确定,这是随机的。

更明确的chan 写法

上述的代码我们了解到,chan 可以收也可以发,但是有时候 我们作为 chan的提供者 希望明确告诉使用者你的chan到底是发还是收的时候 应该怎么办?

// 创建一个chan 这个chan 只能用于发数据
func createWorker(id int) chan<- int {
   c := make(chan int)
   go func() {
      for {
         // 但是在内部 我们仍旧可以用这个chan 来收数据
         fmt.Println("worker ", id, "received ", string(rune(<-c)))
      }
   }()
   return c
}

func chanDemo() {
   
   //这里定的chan 就是一个只能发输出的chan了
   var chanArray [10]chan<- int
   for i := 0; i < 10; i++ {
      chanArray[i] = createWorker(i)
      //go worker(i, chanArray[i])
   }

   for i := 0; i < 10; i++ {
      chanArray[i] <- 'a' + i
   }
   
   time.Sleep(time.Millisecond)
}

func main() {
   chanDemo()
}

bufferchannel

func bufferChan() {
   c := make(chan int)
   c <- 1
}

func main() {
   //chanDemo()
   bufferChan()
   time.Sleep(time.Millisecond)

}

这里程序执行会错误,因为你这个c 只有发送 没有收,在go中 这样操作是不被允许的 image.png

但是有时候我们在创建chan的时候 发前面几组数据的时候并不需要接收 这个时候 就可以利用buffer 来处理了

func bufferChan() {
   c := make(chan int, 3)
   c <- 1
   c <- 2
   c <- 3
}

比如说 这样就完全没有问题,因为我们发送的数据 先到了chan

关闭chan

func bufferChan() {
   c := make(chan int, 3)
   go func() {
      for {
         fmt.Println("received ", <-c)
      }
   }()
   c <- 1
   c <- 2
   c <- 3
   close(c)
}

可以看下 这里的运行结果,

image.png

这里就很奇怪了,为啥我们close 以后 还会收到 0 这个数据? 其实这是go的特性,你关闭了chan以后 chan还会不停的发送 chan类型的默认值,我们这里类型是int 那自然默认值就是0了,

func bufferChan() {
   c := make(chan int, 3)
   go func() {
      for {
         v, ok := <-c
         if !ok {
            fmt.Println("closed")
            break
         }
         fmt.Println("received ", v)
      }
   }()
   c <- 1
   c <- 2
   c <- 3
   close(c)
}

image.png

当然我们还有一种更加简单的写法:

func bufferChan2() {
   c := make(chan int, 3)
   go func() {
      for v := range c {
         fmt.Println("received ", v)

      }
   }()
   c <- 1
   c <- 2
   c <- 3
   close(c)
}

不要通过共享内存来通信,通过通信来共享内存

看看如何来理解这个go语言并发的设计哲学

前面的例子 我们能看出来 我们是通过time sleep 来控制 让程序不要退出的。 这个十分的粗暴,我们想做到的是 当你全部打印结束的时候 程序自动退出就可以了。这应该怎么做?

// 首先定义一个 worker 
// in 代表 输入
// done 代表其实就是输出了 我要告诉外面 我的活干完了
type Worker struct {
   in   chan int
   done chan bool
}

看看怎么告诉外面我们活干完了? 其实就是打印结束以后 向done里面发送一个数据

func doWorker(worker *Worker) {
   go func() {
      for {
         fmt.Println("receive:", <-worker.in)
         worker.done <- true
      }
   }()
}

再看看 如何创建worker?

func createWorker(id int) Worker {
   w := Worker{
      in:   make(chan int),
      done: make(chan bool),
   }
   doWorker(&w)
   return w
}

创建的过程比较简单

最后看下主函数


func main() {
   var chanArray [10]Worker
   // 先创建10个worker
   for i := 0; i < 10; i++ {
      chanArray[i] = createWorker(i)
   }
   // 再向worker里面写入数据
   for i := 0; i < 10; i++ {
      chanArray[i].in <- i
      <-chanArray[i].done
   }
}

先看下执行结果:

image.png

这一次我们没有在main函数里面执行 time。sleep 但是依旧 执行了每个打印语句

但是这个打印语句有问题吧?

为啥变成顺序的了?

你这顺序执行等于没并发能力啊

仔细看啊 我们for循环中 这里 发送完数据以后 有一个获取done这个chan的操作, 这个操作 不执行完 程序不会继续执行循环的, 所以等于是一个顺序的操作了, 这里我们再次换一个写法:

func main() {
   var chanArray [10]Worker
   // 先创建10个worker
   for i := 0; i < 10; i++ {
      chanArray[i] = createWorker(i)
   }
   // 再向worker里面写入数据
   for i := 0; i < 10; i++ {
      chanArray[i].in <- i
   }

   for _, w := range chanArray {
      <-w.done
   }
}

这次就变成了 我先发, 发完了以后 我再弄个循环去接受:

image.png

这次这个执行结果就正确了。

再修改一下 提高一下难度

我们这次 发完10个数据以后 想接着再发下一个10数据, 要两次发送的数据 都print结束以后 程序才能退出

怎么处理?

func main() {
   var chanArray [10]Worker
   // 先创建10个worker
   for i := 0; i < 10; i++ {
      chanArray[i] = createWorker(i)
   }
   // 再向worker里面写入数据
   for i := 0; i < 10; i++ {
      chanArray[i].in <- i
   }

   // 再向worker里面写入数据 这次写的是10-19
   for i := 0; i < 10; i++ {
      chanArray[i].in <- i + 10
   }

   for _, w := range chanArray {
      <-w.done
      <-w.done
   }
}

看样子好像没错呀, 连续发送2次,然后接收端 收到2个消息才能 结束

image.png

但是run起来以后 程序报错了,为啥?

因为我们第一次0-9的数据发送完毕以后 我们对每个worker触发了一次done的写入, 但是这次done触发写入以后 没有地方去收这个done的写入了, 然后 我们马上又开始对worker进行10-19的 写入了

针对于这个问题呢,有一种简单的修复写法:

func doWorker(worker *Worker) {
   go func() {
      for {
         fmt.Println("receive:", <-worker.in)
         go func() {
            worker.done <- true
         }()

      }
   }()
}

image.png

这里为啥正确呢

因为你重新开了一个 goroutine 去 写入数据 不会卡死 之前的 routine的循环了。

更简单的等待写法

type Worker struct {
   in chan int
   wg *sync.WaitGroup
}
func createWorker(id int, group *sync.WaitGroup) Worker {
   w := Worker{
      in: make(chan int),
      wg: group,
   }
   doWorker(&w)
   return w
}

func doWorker(worker *Worker) {
   go func() {
      for {
         fmt.Println("receive:", <-worker.in)
         worker.wg.Done()
      }
   }()
}

其实这里就是利用官方提供的 waitgroup来做等待

func main() {

   var wg sync.WaitGroup

   var chanArray [10]Worker
   // 先创建10个worker
   for i := 0; i < 10; i++ {
      chanArray[i] = createWorker(i, &wg)
   }
   wg.Add(20)

   // 再向worker里面写入数据
   for i := 0; i < 10; i++ {
      chanArray[i].in <- i
   }
   // 再向worker里面写入数据 这次写的是10-19
   for i := 0; i < 10; i++ {
      chanArray[i].in <- i + 10
   }

   wg.Wait()
}

Add 代表 有20个任务进去了, wait 就代表等待 等待这20个任务 全部done以后 就结束。