目录

什么是并发

并发是由多个独立执行的计算程序组成的,并发是一种结构化的软件程式。并发并不是并行,尽管他有并行的一些特性,如果只有一个处理器,程序仍然可以并发的,但是不能并行。

并发:同一时间段内执行多个任务 (时间段)

并行:同一时刻执行多个任务 (时刻)

编写一个随机时间内说hi的函数

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	sayHi("hi")
}

func sayHi(msg string) {
	for i := 0; ; i++ {
		fmt.Println(msg,i)
		time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
	}
}

输出:

hi 0
hi 1
hi 2
hi 3
...

goroutine

main中改造一下sayHi()的方式,使用go关键字

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	go sayHi("hi")
}

func sayHi(msg string) {
	for i := 0; ; i++ {
		fmt.Println(msg,i)
		time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
	}
}

以上代码执行之后,发现没有任何的输出。 go 语句会正常运行sayHi()函数,但不会让主函数(调用方)等待。

他启动了一个goroutine,去执行sayHi() ,执行过程中main函数并不会等待他执行完成。

我们让main函数睡个2秒钟看看效果

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	go sayHi("hi")
	fmt.Println("说的不错!")
    time.Sleep(2*time.Second)
    fmt.Println("不想听你说了,我走了。")
}

func sayHi(msg string) {
	for i := 0; ; i++ {
		fmt.Println(msg,i)
		time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
	}
}

以上程序执行效果如下

说的不错!
hi 0
hi 1
hi 2
hi 3
hi 4
hi 5
不想听你说了,我走了。

goroutine 是一个独立执行的函数,由go关键字启动,他具有自己的调用堆栈,且堆栈会根据需要进行扩展何收缩,goroutine不是线程(thread),一个程序中可能只有 一个线程,但是可以有很多个goroutinegoroutine会依据需要动态的多路复用到线程上,以保持所有的goroutine运行。

在之前的例子中,我们看见终端打印goroutine的输出,其实main 函数看不到goroutine的输出,main函数和goroutine之间并没有数据的交互。 如果需要两者进行交互(沟通)需要使用channel

channel

go语言中使用通道来让两个goroutine链接,从而使它们之间能够进行通信。

channel 的声明和初始化

var c chan int
c = make(chan int)
// or
c := make(chan int)

//向channel中发送数据
c <- 1

//从channel中获取数据
value <- c

//箭头的指向代表了数据的流向

本篇文章不讨论有缓冲区的channel,仅讨论没有缓冲区的channel。

使用channel改造下之前的函数

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	c := make(chan string)
	go sayHi("hi", c)
	for i := 0; i < 5; i++ {
		fmt.Printf("你说了:%q\n", <-c)
	}
	fmt.Println("不想听你说了,我走了。")
}

func sayHi(msg string, c chan string) {
	for i := 0; ; i++ {
		c <- fmt.Sprintf("%s %d", msg, i)
		time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
	}
}

运行结果:

你说了:"hi 0"
你说了:"hi 1"
你说了:"hi 2"
你说了:"hi 3"
你说了:"hi 4"
不想听你说了,我走了。

main函数执行到<-c的时候,它会一直等待直到有数据可以读取出来。相反的,当sayHi() 执行c<- 时候,它会一直等待直到可以把数据传递进去(这里仅讨论没有缓冲区的通道)

发送者和接收者都必须准备好各自的状态,才能进行通信。否则就会一直等待他们两个都准备好为止(仅讨论无缓冲区的通道)

go语言中的channel,不通过共享内存来通信,而是通过通信共享内存。

改造下之前的代码,让sayHi()返回一个channel,这样我们可以让更多人说不同的话

package main

import (
	"fmt"
    "math/rand"
    "time"
)

func main() {
	c1 := sayHi("hi")
	c2 := sayHi("ha")
	for i := 0; i < 5; i++ {
		fmt.Printf("你说了:%q\n", <-c1)
		fmt.Printf("你说了:%q\n", <-c2)
	}
	fmt.Println("不想听你说了,我走了。")
}

func sayHi(msg string) <-chan string {
	c := make(chan string)
	go func() {
		for i := 0; ; i++ {
			c <- fmt.Sprintf("%s %d", msg, i)
			time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
		}
	}()
	return c
}

执行结果

你说了:"hi 0"
你说了:"ha 0"
你说了:"hi 1"
你说了:"ha 1"
你说了:"hi 2"
你说了:"ha 2"
你说了:"hi 3"
你说了:"ha 3"
你说了:"hi 4"
你说了:"ha 4"
不想听你说了,我走了。

以上代码在执行过程中会有一个问题,说的快的人必须等说的慢的人说好才能进行下一轮

使用多路复用(Multiplexing)

fan-in

package main

import (
	"fmt"
    "math/rand"
    "time"
)

func main() {
	c := fanIn(sayHi("hi"), sayHi("ha"))
	for i := 0; i < 20; i++ {
		fmt.Println(<-c)
	}
	fmt.Println("不想听你说了,我走了。")
}

func fanIn(input1, input2 <-chan string) <-chan string {
	c := make(chan string)
	go func() {
		for {
			c <- <-input1
		}
	}()

	go func() {
		for {
			c <- <-input2
		}
	}()
	return c
}

func sayHi(msg string) <-chan string {
	c := make(chan string)
	go func() {
		for i := 0; ; i++ {
			c <- fmt.Sprintf("%s %d", msg, i)
			time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
		}
	}()
	return c
}

以上程序多执行几次会发现其中一个会连续地输出,而不是等待另外一个准备好了在一起输出。

select

select 提供了处理多个通道的方法。类似于switch-case语句结构

  • 所有的通道都可以被执行
  • 阻塞(block),直到其中一个channel可以通信,并执行他。
  • 如果多个通道可以通信,则会伪随机的选择一个执行
  • 如果没有可执行的case,在有default的情况下,会立即执行default

使用select改造fanIn()函数达到同样的效果

func fanIn(input1, input2 <-chan string) <-chan string {
    c := make(chan string)
    go func() {
        for {
            select {
            case s := <-input1:  
            	c <- s
            case s := <-input2: 
            	c <- s
            }
        }
    }()
    return c
}

我们还可以使用time.After()返回一个channel达到定时退出效果

package main

func main() {
	c := sayHi("hello")
	timeout := time.After(3 *time.Second)
	for{
		select {
		case <-timeout:
			fmt.Println("timeout")
			return
		case v := <-c:
			fmt.Println(v)
		}
	}
}

使用来channel实现传声游戏(部队中的报数,从队首依次报数+1至队尾)

传声游戏

package main

import "fmt"

//从右至左报数
func baoshu(left, right chan int) {
	left <- 1 + <-right
}

func main() {
	//假定有10000个人
	const n = 10000
	//先定义两个channel
	leftmost := make(chan int)
	right := leftmost
	left := leftmost
	//循环开始报数
	for i := 0; i < n; i++ {
		//初始化最右边的数
		right = make(chan int)
		//开始报数 	左边的数等于右边+1
		go baoshu(left, right)
		//此时
		left = right
	}
	go func(c chan int) {
		c <- 1
	}(right)

	fmt.Println(<-leftmost)
}

go 并发模拟Google搜索demo

Reference

blog

视频

ppt