Go语言管道、多线程(21.8.17)

4,249 阅读2分钟

多线程

Go 协程和 Go 主线程

Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个 Go 线程上,可以起多个协程,你可以 这样理解,协程是轻量级的线程[编译器做优化]。

Go 协程的特点

  1. 有独立的栈空间
  2. 共享程序堆空间
  3. 调度由用户控制
  4. 协程是轻量级的线程

快速入门

程序需求

  1. 在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出 "hello,world"
  2. 在主线程中也每隔一秒输出"hello,golang", 输出 10 次后,退出程序
  3. 要求主线程和 goroutine 同时执行.

程序实现

package main
​
import (
    "fmt"
    "strconv"
    "time"
)
​
func goRoutineTest() {
    for i := 1; i<=10; i++ {
        fmt.Println("hello world " + strconv.Itoa(i))
        time.Sleep(time.Second)
    }
}
​
func main() {
    go goRoutineTest()  // 开启一个协程for i :=1; i <= 10; i++ {
        fmt.Println("hello, golang" + strconv.Itoa(i))
        time.Sleep(time.Second)
    }
}
​

快速入门小结

  1. 主线程是一个物理线程,直接作用在 cpu 上的。是重量级的,非常耗费 cpu 资源。
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
  3. Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一 般基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优势了

goroutine 的调度模型

待学

设置 Golang 运行的 cpu 数

介绍:为了充分了利用多 cpu 的优势,在 Golang 程序中,设置运行的 cpu 数目

    num := runtime.NumCPU()  //获取cpu的数量
    runtime.GOMAXPROCS(num)
    fmt.Println("cpu个数", num)

go1.8后,默认程序运行在多个核上,可以不用设置

go1.8前,需要设置一下,可以更高效的cpu利用

Channel(管道)

通过一个Demo理解:

计算1~200各个数的阶乘,使用goroutine实现

package main
​
import (
    "fmt"
    "time"
)
​
// 定义一个map
var myMap = make(map[int]int, 10)
​
​
// 定义一个函数计算n!
func factorial(num int) {
    res :=1
    for i := 1; i <= num; i++{
        res *= i
    }
​
    // 将计算出的结果存入map中
    myMap[num] = res
}
​
func main() {
​
    // 开启 200 个协程完成这个任务
    for i := 1; i <= 200; i++ {
        go factorial(i)
    }
​
    // 休眠 10S
    time.Sleep(time.Second * 10)
​
    // 输出map中的结果
    for index, value := range myMap{
        fmt.Println(index,"的阶乘为",value)
    }
}

运行结果:

fatal error: concurrent map write,原因在于map需要互斥访问

如何解决这个问题?本质上是线程直接通信的问题

  1. 使用全局变量的互斥锁

    操作全局变量前,对其进行上锁,操作完成后,释放锁

  2. 使用管道channel来解决

使用全局变量枷锁同步改进程序

package main
​
import (
    "fmt"
    "sync"
    "time"
)
​
// 定义一个map
var (
    myMap = make(map[int]int,10)
    // 声明一个全局变量互斥锁
    // lock 是一个全局互斥锁
    // sync 是包:sychorinized
    // Mutex : 互斥
    lock sync.Mutex
    )
​
​
​
​
// 定义一个函数计算n!
func factorial(num int) {
    res :=1
    for i := 1; i <= num; i++{
        res += i
    }
​
    // 将计算出的结果存入map中
    // 访问 myMap 前,加锁
    lock.Lock()
    myMap[num] = res
    // 访问完,释放所
    lock.Unlock()
}
​
func main() {
​
    // 开启 200 个协程完成这个任务
    for i := 1; i <= 200; i++ {
        go factorial(i)
    }
​
    // 休眠 10S
    time.Sleep(time.Second * 10)
​
    // 输出map中的结果
    lock.Lock()
    for index, value := range myMap{
        fmt.Printf("map[%d]=[%d]\n", index, value)
        //fmt.Println(index,"的阶乘为",value)
    }
    lock.Unlock()
}

引入channel的概念

为什么需要channel?

  1. 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
  2. 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
  3. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作 状态,这时也会随主线程的退出而销毁
  4. 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
  5. 上面种种分析都在呼唤一个新的通讯机制-channel

什么是channel?

  1. channle 本质就是一个数据结构-队列
  2. 数据是先进先出【FIFO : first in first out】
  3. 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
  4. channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。

image.png

channel的使用

channel的定义

var 变量名 chan 数据类型
var intChan chan int   // intChan用于存放int类型的数据
var mapChan chan map[int]string  // mapChan用以存放map类型的数据
var perChan chan Person
var perChan2 chan *Person

说明

  1. channel 是引用类型
  2. channel 必须初始化才能写入数据, 即 make 后才能使用
  3. 管道是有类型的,intChan 只能写入 整数 int

管道使用Demo

package main
​
import "fmt"func main() {
    // 声明一个管道
    var intChan chan int
    // 为chan分配内存,可以存放三个int类型的管道
    intChan = make(chan int,  3)
​
​
    fmt.Printf("intChan的值是%v    intChan本身的地址是 %p \n", intChan, &intChan)
​
    // 向管道写入数据
    intChan <- 10
    num := 211
    intChan <- num
    intChan <- 50// 查看管道长度和容量
    fmt.Printf("管道的长度是%v,容量是%v \n",len(intChan) ,cap(intChan))
​
    // 从管道读取数据
    var num2 int
    num2 =<-intChan
    fmt.Println("num2的值是%d",num2)
​
    // 发现管道的长度减 1
    fmt.Printf("管道的长度是%v,容量是%v ",len(intChan) ,cap(intChan))
​
    var num3 =<-intChan
    var num4 =<-intChan
     fmt.Println(num3, num4)
​
    fmt.Printf("管道的长度是%v,容量是%v ",len(intChan) ,cap(intChan))
​
}

channel使用注意事项

  1. channel 中只能存放指定的数据类型
  2. channle 的数据放满后,就不能再放入了
  3. 如果从 channel 取出数据后,可以继续放入
  4. 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock

channel关闭

使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然 可以从该 channel 读取数据

package main
​
import "fmt"func main() {
    strChan := make(chan string, 3)
​
    strChan <- "huang"
    strChan <- "rong"
    close(strChan)
​
    // strChan <- "wei"
    fmt.Println("okok")
​
    str := <- strChan
    fmt.Println(str)
}

channel的遍历

channel 支持 for--range 的方式进行遍历,请注意两个细节

  1. 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
  2. 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
package main
import (
	"fmt"
)
func main() {
	strChan := make(chan string, 3)

	strChan <- "huang"
	strChan <- "rong"
	 strChan <- "wei"

	 // 不可以使用普通的for 循环遍历channel
	 close(strChan)
	  for value := range strChan{
	  	fmt.Println(value)
	  }
}

使用channel和goroutine实现多线程

  1. 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
  2. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作 状态,这时也会随主线程的退出而销毁

使用两个channel可以解决如上问题

package main

import (
	"fmt"
)

// Write Data
func write(intChan chan int) {
	for i := 1; i <= 50; i++ {
		// 放入数据
		intChan <- i
		fmt.Println("Write Data", i)
	}
	close(intChan)
}

// Read data
func readData(intChan chan int, exitChan chan bool) {

	for  {
		v, ok :=<- intChan
		if !ok{
			break
		}
		//time.Sleep(time.Second)
		fmt.Println("读到数据",v)
	}

	// 读取完数据后,即任务完成
    // exitChan 是一个标志,当读完数据后,向exit中存放一个true,主线程除非取出这个true,否则不停止
	exitChan <- true
	close(exitChan)
}

func main() {
	intChan := make(chan int, 50)
	exitChan := make(chan bool, 1)

	go write(intChan)

	go readData(intChan,exitChan)

	for  {
		_, ok := <-exitChan
		if !ok {
			break
		}
	}

}

channel 使用细节和注意事项

  1. channel 可以声明为只读,或者只写性质
var chan1 chan int  //默认情况下,管道是双向的,可读可写var chan1 chan <- int  //管道只可写var chan3 chan -> int  // 管道只可以读
  1. 使用 select 可以解决从管道取数据的阻塞问题
    func main() {
    //使用 select 可以解决从管道取数据的阻塞问题
    //1.定义一个管道 10 个数据 int
    intChan := make(chan int, 10)
    for i := 0; i < 10; i++ {
    intChan<- i
    }
    //2.定义一个管道 5 个数据 string
    stringChan := make(chan string, 5)
        for i := 0; i < 5; i++ {
    stringChan <- "hello" + fmt.Sprintf("%d", i)
    }
    //传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
    //问题,在实际开发中,可能我们不好确定什么关闭该管道.
    //可以使用 select 方式可以解决
    //label:
    for {
    select {
    //注意: 这里,如果 intChan 一直没有关闭,不会一直阻塞而 deadlock
    //,会自动到下一个 case 匹配
    case v := <-intChan :
    fmt.Printf("从 intChan 读取的数据%d\n", v)
    time.Sleep(time.Second)
    case v := <-stringChan :
    fmt.Printf("从 stringChan 读取的数据%s\n", v)
    time.Sleep(time.Second)
    default :
    fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n")
    time.Sleep(time.Second)
    return
    //break label
        }
    }
    
  2. goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题

    如果我们起了一个协程,但是这个协程出现了panic,如果没有捕获这个panic,就会造成整个程序的崩溃,这时我们可以在goruotine中使用recover捕获panic,进行处理,这样即使协程出现问题也不会影响主线程的执行

    //函数
    func sayHello() {
    for i := 0; i < 10; i++ {
    time.Sleep(time.Second)
    fmt.Println("hello,world")
        }
    }
    ​
    //函数
    func test() {
    //这里我们可以使用 defer + recover
    defer func() {
    //捕获 test 抛出的 panic
        if err := recover(); err != nil {
            fmt.Println("test() 发生错误", err)
            }
        }()
    //定义了一个 map
    var myMap map[int]string
    myMap[0] = "golang" //error
    }
    ​
    func main() {
    go sayHello()
    go test()
    for i := 0; i < 10; i++ {
        fmt.Println("main() ok=", i)
        time.Sleep(time.Second)
        }
    }