GO初识并发编程

并发编程

  • 概念了解
    • 进程/线程
    • 并发/并行
    • 协程/线程
  • Goroutine 介绍
    • goroutine的使用
    • 小案例
    • 设置CPU数目
  • channel管道
    • 简介
    • 特性
    • 基本使用
      • 声明以及初始化
      • 写入和读取
      • 关闭和遍历
      • 注意事项
      • 发送数据
      • 接收数据
    • goroutine和channel结合
    • 管道注意事项

概念了解

进程/线程

进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。

线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行。

并发/并行

多线程程序在单核心的 cpu 上运行,称为并发;多线程程序在多核心的 cpu 上运行,称为并行。

并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。

协程/线程

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

线程:一个线程上可以跑多个协程,协程是轻量级的线程

Goroutine 介绍

goroutine 是一种非常轻量级的实现,可在单个进程里执行成千上万的并发任务,它是Go语言并发设计的核心。

使用 go 关键字就可以创建 goroutine,将 go 声明放到一个需调用的函数之前,在相同地址空间调用运行这个函数,这样该函数执行时便会作为一个独立的并发线程,这种线程在Go语言中则被称为 goroutine。

goroutine的使用

Go 程序中使用 go 关键字为一个函数创建一个 goroutine。一个函数可以被创建多个 goroutine,一个 goroutine 必定对应一个函数。

方法一 : 在调用方法时
go 函数名( 参数列表 )
使用 go 关键字创建 goroutine 时,被调用函数的返回值会被忽略。

方法二:

//新建一个匿名方法并执行
go func(param1, param2) {
}(val1, val2)

方法三:

//直接新建一个 goroutine 并在 goroutine 中执行代码块
go {
    //do someting...
}

如果需要在 goroutine 中返回数据,请使用后面介绍的通道(channel)特性,通过通道把数据从 goroutine 中作为返回值传出。

小案例

开启子线程 每隔1秒输出 hello world,输出10次,在主线程同时每隔1秒输出hello golang,输出10次

package main

import (
	"fmt"
	"strconv"
	"time"
)

func main() {
	// 开启协程 子线程
	go printHelloWorld()

	for i := 1; i <= 10; i++ {
		fmt.Println("主线程 hello golang " + strconv.Itoa(i))
		// 休眠1秒
		time.Sleep(time.Second)
	}
}

// 每个1秒输出hello world
func printHelloWorld(){
	for i := 1; i <= 10; i++ {
		fmt.Println("子线程 hello world " + strconv.Itoa(i))
		// 休眠1秒
		time.Sleep(time.Second)
	}
}

GO初识并发编程_第1张图片
如果主线程输出 5 次 hello golang结果会怎么样呢
GO初识并发编程_第2张图片
输出结果如下
GO初识并发编程_第3张图片
当主线程退出时,协程即使没有执行完毕,也会退出。当然,协程也可以在主线程结束前自己退出。

设置CPU数目

Go 地中也可以通过 runtime.GOMAXPROCS() 函数做到设置CPU数目

runtime.GOMAXPROCS(逻辑CPU数量)

这里的逻辑CPU数量可以有如下几种数值:

 <1:不修改任何数值。
 =1:单核心执行。
 >1:多核并发执行。

可以使用 runtime.NumCPU() 查询 CPU 数量,并使用 runtime.GOMAXPROCS() 函数进行设置
runtime.GOMAXPROCS(runtime.NumCPU())

channel管道

简介

如果说 goroutine 是 Go语言程序的并发体的话,那么 channels 就是它们之间的通信机制。一个 channels 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。

特性

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

基本使用

声明以及初始化

声明语法格式如下:
var 变量名称 chan 管道类型
注意:

  • channel 是引用类型
  • channel声明后必须初始化,即 make后才能使用
  • channel是有类型的

管道实例 := make(chan 数据类型)

写入和读取


package main

import "fmt"

func main() {

	var intChan chan int
	intChan = make(chan int,3)// 初始管道容量3

	//注意,写入数据时,不要超过初始容量,否则会报错
	intChan <- 3
	intChan <- 4

	fmt.Printf("channel 长度 = %v,容量 = %v\n",len(intChan),cap(intChan))

	//从管道中接收数据
	var num int
	num = <- intChan
	fmt.Println("num = ",num) //
	fmt.Printf("channel 长度 = %v,容量 = %v\n",len(intChan),cap(intChan))
	// 在没有使用协程的情况下,将channel接收完毕后,再接收就会报 all goroutines are asleep - deadlock!
}

GO初识并发编程_第4张图片

关闭和遍历

使用内置函数 close 可以关闭 channel, 当 channel 关闭后, 就不能再向 channel 写数据了, 但是仍然可以从该 channel 读取数据。
GO初识并发编程_第5张图片
channel 支持 for–range 的方式进行遍历, 请注意两个细节

  1. 在遍历时, 如果 channel 没有关闭, 则回出现 deadlock 的错误
  2. 在遍历时, 如果 channel 已经关闭, 则会正常遍历数据, 遍历完后, 就会退出遍历
	intChan2 := make(chan int,100)
	for i := 0; i < 100; i++ {
		intChan2 <- i * 2
	}

	// 遍历时,channel没有关闭,就会报 all goroutines are asleep - deadlock!
	close(intChan2)
	for data := range intChan2 {
		fmt.Println("v = ",data)
	}

注意事项

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

发送数据

向管道写入数据
通道的发送使用特殊的操作符 <-
格式如下
通道变量 <- 值

这里的值可以是变量、常量、表达式或者函数返回值等。值的类型必须与ch通道的元素类型一致。


    var intChan chan int
	intChan = make(chan int,3)// 初始管道容量3

	//注意,写入数据时,不要超过初始容量,否则会报错
	intChan <- 3
	intChan <- 4

	fmt.Printf("channel 长度 = %v,容量 = %v\n",len(intChan),cap(intChan)) //长度 = 2,容量 = 3

接收数据

通道接收同样使用<-操作符,通道接收有如下特性:

  1. 通道的收发操作在不同的两个 goroutine 间进行。
    由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另外一个 goroutine 中进行。

  2. 接收将持续阻塞直到发送方发送数据。
    如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。

  3. 通道一次只能接收一个数据元素。

通道的数据接收一共有以下 4 种写法。

  1. 阻塞接收数据
    阻塞模式接收数据时,将接收变量作为<-操作符的左值,格式如下:
    data := <-ch
    执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量

  2. 非阻塞接收数据
    data, ok := <-ch

  • data:表示接收到的数据。未接收到数据时,data 为通道类型的零值。
  • ok:表示是否接收到数据。

非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel 进行。

  1. 接收任意数据,忽略接收的数据
    <-ch
    执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。这个方式实际上只是通过通道在 goroutine 间阻塞收发实现并发同步。

使用通道做并发同步的写法,可以参考下面的例子:

package main

import (
    "fmt"
)

func main() {

    // 构建一个通道
    ch := make(chan int)

    // 开启一个并发匿名函数
    go func() {

        fmt.Println("start goroutine")

        // 通过通道通知main的goroutine
        ch <- 0

        fmt.Println("exit goroutine")

    }()

    fmt.Println("wait goroutine")

    // 等待匿名goroutine
    <-ch

    fmt.Println("all done")

}

输出如下:

wait goroutine
start goroutine
exit goroutine
all done
  1. 循环接收
    通道的数据接收可以借用 for range 语句进行多个元素的接收操作,格式如下:
for data := range ch {
}

通道 ch 是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过 for 遍历获得的变量只有一个,即上面例子中的 data。


package main

import (
    "fmt"

    "time"
)

func main() {

    // 构建一个通道
    ch := make(chan int)

    // 开启一个并发匿名函数
    go func() {

        // 从3循环到0
        for i := 3; i >= 0; i-- {

            // 发送3到0之间的数值
            ch <- i

            // 每次发送完时等待
            time.Sleep(time.Second)
        }

    }()

    // 遍历接收通道数据
    for data := range ch {

        // 打印通道数据
        fmt.Println(data)

        // 当遇到数据0时, 退出接收循环
        if data == 0 {
                break
        }
    }

}

输出如下:

3
2
1
0

goroutine和channel结合

案例演示:
开启一个写协程,向管道写入50个整数,再开启一个读协程,接收这50个整数。写协程和读协程操作的是同一个管道。主线程main需要等写协程和读协程都完毕之后才能退出。

package main

import "fmt"

func main() {
	intChan := make(chan int)
	exitChan:= make(chan bool)
	go writeData(intChan)
	go readData(intChan,exitChan)
	<- exitChan //阻塞 直到exitChan写入
}
func writeData(intChan chan int){
	for i := 1; i <=  50; i++ {
		intChan <- i//写入数据
	}
	close(intChan)//关闭管道
}

func readData(intChan chan int,exitChan chan bool){
	for {
		v,ok := <- intChan
		if !ok { //如果没有读取到就退出循环
			break
		}
		fmt.Printf("readData 读取到数据 %v \n",v)
	}
	exitChan <- true //告诉main 已经读取完毕,可以退出了
}

案例2:
统计1至8000的素数,

package main

import "fmt"

func main() {

	intChan := make(chan int, 1000)
	primeChan := make(chan int, 2000)
	exitChan := make(chan bool, 4)
    // 开启协程,写入8000个整数
	go putNNum(intChan)
	// 开启4个线程,从intChan取出整数并判断是否为素数,是素数就放入到primeChan
	for i := 0; i < 4; i++ {
		go primeNum(intChan,primeChan,exitChan)
	}

	go func() {
		for i := 0; i < 4; i++ {
			<- exitChan //main 阻塞状态,直到exitChan写入了4个数据
		}
		close(primeChan)
	}()

	// 遍历primeChan,取出所有的素数
	for {
		res, ok := <- primeChan
		if !ok {
			break
		}
		fmt.Printf("素数 = %v\n",res)
	}
	fmt.Println("主main退出")

}

// 向管道写入 1---8000的整数
func putNNum(intChan chan int)  {

	for i := 1; i <= 8000; i++ {
		intChan <- i
	}
	close(intChan)
}

func primeNum(intChan chan int,primeChan chan int,exitChan chan bool)  {
	//var num int
	var isPrime bool
	for {
		num,ok := <- intChan
		if !ok {
			break
		}
		//判断 num 是不是素数
		isPrime = true
		for i := 2; i < num; i++ {
			if num % i == 0 { // num 不是素数
				isPrime = false
				break
			}
		}
		if isPrime {
			// 将这个素数放入到primeChan
			primeChan <- num
		}
	}
	fmt.Println("本primeNum协程已完成工作")
	exitChan <- true
}

管道注意事项

  1. channel 可以声明为只读, 或者只写性质【单通道】
var 通道实例 chan<- 元素类型    // 只能写入数据的通道
var 通道实例 <-chan 元素类型    // 只能读取数据的通道
  1. 使用 select 可以解决从管道取数据的阻塞问题
    select 的用法与 switch 语言非常类似,由 select 开始一个新的选择块,每个选择条件由 case 语句来描述。

与 switch 语句相比,select 有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个 IO 操作

select {
    case <-chan1:
    // 如果chan1成功读到数据,则进行该case处理语句
    case chan2 <- 1:
    // 如果成功向chan2写入数据,则进行该case处理语句
    default:
    // 如果上面都没有成功,则进入default处理流程
}

代码演示

package main
import (
	"fmt"
	"time"
)

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
		}
	}
}

  1. goroutine 中使用 recover, 解决协程中出现 panic, 导致程序崩溃问题
    在一个协程执行过程中如果出现panic,异常错误,如果没有捕获该panic则会导致整个程序崩溃退出。此时,就需要 recover捕获panic,使程序继续执行下去。

演示

package main
import (
	"fmt"
	"time"
)

//函数
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" //map需要先make才能使用,所以在这里会报错
}

func main() {

	go sayHello()
	go test()


	for i := 0; i < 10; i++ {
		fmt.Println("main() ok=", i)
		time.Sleep(time.Second)
	}

}

你可能感兴趣的:(go,go,并发编程)