GO初识并发编程

概念了解

进程/线程

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

线程是进程的一个执行实体,是 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)
	}
}

在这里插入图片描述
如果主线程输出 5 次 hello golang结果会怎么样呢
在这里插入图片描述
输出结果如下
在这里插入图片描述
当主线程退出时,协程即使没有执行完毕,也会退出。当然,协程也可以在主线程结束前自己退出。

设置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!
}

在这里插入图片描述

关闭和遍历

使用内置函数 close 可以关闭 channel, 当 channel 关闭后, 就不能再向 channel 写数据了, 但是仍然可以从该 channel 读取数据。
在这里插入图片描述
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)
	}

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值