首页 > 其他分享 >GO 并发

GO 并发

时间:2023-02-04 16:44:07浏览次数:58  
标签:fmt chan 并发 func GO main make 通道

简介

  • Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。

  • goroutine 是轻量级线程也有叫 用户级线程,协程的,

  • goroutine 的调度是由 Golang 运行时进行管理的。

  • 你可理解为一段可以异步执行的代码,一个新的轻量级线程

  • 进程 => 线程 =>协程

为什么会有协程的出现

每一个新鲜事物的出现,都是为了解决某一类问题的,那协程是为了解决什么?

这里用java的线程举例:

java的线程调度是经过内核的,也就是 Thread 和 kernel thread 是一比一的关系

也就是java 所谓的线程其实就是在内核的系统线程上包了一层java的东西,其中涉及到

用户态和内核态之间的切换,中间的过程非常复杂,非常耗时

有关用户态和内核态,可以参考:

https://www.cnblogs.com/zwj-199306231519/articles/16859489.html

这时候有人就开始想办法,想做出一个,

不经过内核,不涉及到用户态的切换,但又类似线程的东西,

这就是最开始协程的概念

goroutine 语法格式

go 函数名( 参数列表 )

例如:

go f(x, y, z)

开启一个新的 goroutine:

f(x, y, z)

Go 允许使用 go 语句开启一个新的运行期线程(协程), 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间

两个例子

例子1:异步

package main

import (
	"fmt"
	"time"
)

func GoRoutine() {
	go func() {
		time.Sleep(10 * time.Second)
	}()
	// 这里直接输出,不会等待十秒
	fmt.Println("I am here")
}

func main() {
	GoRoutine()
}

image-20230204140834428

通过上面例子可以知道,go func开启的方法是异步的

例子2:无序

package main

import (
	"fmt"
	"time"
)

func say(s string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

func main() {
	go say("world")
	say("hello")
}

image-20230204141114115

通过输出可以知道,他们之间执行是无序的

通道(channel)简介

通道(channel)是用来传递数据的一个数据结构。

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

通道(channel) 声明,创建,赋值

声明一个通道很简单,我们使用chan关键字即可

var Channel_name chan Type

通道在使用前必须先创建:

ch := make(chan int)

赋值

ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据
           // 并把值赋给 v

例:

package main

import "fmt"

func main() {

	//使用var关键字创建通道
	var mychannel chan int
	fmt.Println("channel的值: ", mychannel)
	fmt.Printf("channel的类型: %T ", mychannel)

	// 使用 make() 函数创建通道
	mychannel1 := make(chan int)
	fmt.Println("\nchannel1的值:", mychannel1)
	fmt.Printf("channel1的类型: %T ", mychannel1)
}

image-20230204144227032

通道(channel)的缓冲区

通道的种类分为:

  • 带缓冲的
  • 不带缓冲的

通过 make 的第二个参数指定缓冲区大小:

ch := make(chan int, 100)

channel 带缓冲

image-20230204142735869

channel 不带缓冲

image-20230204142939931

案例

package main

import (
	"fmt"
	"time"
)

func main() {
	channelWithoutCache()
	channelWithCache()
}

func channelWithCache() {
	// 带缓冲,缓冲区为1
	ch := make(chan string, 1)
	go func() {

		ch <- "Hello, first msg from channel"
		time.Sleep(time.Second)
		ch <- "Hello, second msg from channel"
	}()

	time.Sleep(2 * time.Second)
	msg := <-ch
	fmt.Println(time.Now().String() + msg)
	msg = <-ch
	fmt.Println(time.Now().String() + msg)
	// 因为前面我们先睡了2秒,所以其实会有一个已经在缓冲了
	// 当我们尝试输出的时候,这个输出间隔就会明显小于1秒
	// 我电脑上的几次实验,差距都在1s以内
}

func channelWithoutCache() {
	// 不带缓冲
	ch := make(chan string)
	go func() {
		time.Sleep(time.Second)
		ch <- "Hello, msg from channel"
	}()

	// 这里比较容易写成 msg <- ch,编译会报错
	msg := <-ch
	fmt.Println(msg)
}

image-20230204144904662

通道的遍历与关闭

Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:

v, ok := <-ch

如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭

package main

import (
	"fmt"
)

func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		// 将 x 的值写入通道
		c <- x
		x, y = y, x+y
	}
	close(c)
}

func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
	// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
	// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
	// 会结束,从而在接收第 11 个数据的时候就阻塞了。
	for i := range c {
		fmt.Println(i)
	}
}

image-20230204161314886

通道的双向通道和单向通道

通道包括双向通道和单向通道,这里双向通道只的是支持发送和接收的通道,而单向通道是只能发送或者只能接收的通道。

双向通道

语法

使用make函数声明并初始化一个通道:

ch1 := make(chan string, 3)
  • chan 是表示通道类型的关键字
  • string 表示该通道类型的元素类型
  • 3 表示该通道的容量为3,最多可以缓存3个元素值。

一个通道相当于一个先进先出(FIFO)的队列,使用操作符 <- 进行元素值的发送和接收:

ch1 <- "1"  //向通道ch1发送数据 "1"

接收元素值:

elem1 := <- ch1 // 接收通道中的元素值

首先接收到的元素为先存入通道中的元素值,也就是先进先出

案例

package main

import "fmt"

func main() {
	str1 := []string{"hello","world", "!"}
	ch1 := make(chan string, len(str1))

	for _, str := range str1 {
		ch1 <- str
	}
	
	for i := 0; i < len(str1); i++ {
		elem := <- ch1
		fmt.Println(elem)
	}
}

执行结果:

image-20230204145720786

单向通道

语法

单向通道包括只能发送的通道和只能接收的通道:

// 写法1
var WriteChan = make(chan<- interface{}, 1) // 只能向通道发送不能接收的
var ReadChan = make(<-chan interface{}, 1) // 只能从通道中接收不能发送的

// 写法2
ch := make(chan int)
// 声明一个只能写入数据的通道类型, 并赋值为ch
var chSendOnly chan<- int = ch
//声明一个只能读取数据的通道类型, 并赋值为ch
var chRecvOnly <-chan int = ch

// 总结
// chan<- 只能向通道写入
// <-chan 只能从通道中读取

单向通道的这种特性可以用来约束函数的输入类型或者输出类型

官方案例-time包中的单向通道

ime 包中的计时器会返回一个 timer 实例,代码如下:

timer := time.NewTimer(time.Second)

timer的Timer类型定义如下:

type Timer struct {    
    C <-chan Time    
    r runtimeTimer
}

image-20230204152100597

第 2 行中 C 通道的类型就是一种只能读取的单向通道。如果此处不进行通道方向约束,一旦外部向通道写入数据,将会造成其他使用到计时器的地方逻辑产生混乱。

因此,单向通道有利于代码接口的严谨性。

案例

下面这个例子,完整的展示了单向通道的流程

package main

import (
	"fmt"
)

func main() {
	// 默认为双向通道
	chan1 := make(chan int)
	chan2 := make(chan int)
	//函数sendChan只允许发送数据,也就是写入到通道
	go sendChan(chan1)
	// 函数squarer将chan1的数据转给chan2
	go squarer(chan2, chan1)
	// 函数recvChan只允许接收数据,也就是从通道中读取
	go recvChan(chan2)
    // 阻塞main,循环随机监听通道
	select {}
}

// (in <-chan int)  参数只允许发送数据,不允许接收
func sendChan(in chan<- int) {
	i := 0
	for {
		in <- i
		i++
	}
}

// (out chan<- int)  参数只允许接收数据,不允许发送数据
func recvChan(out <-chan int) {
	for i := range out {
		fmt.Println("out输出:", i)
	}
}

// (out chan<- int)  参数只允许接收数据,不允许发送数据
// (in <-chan int)  参数只允许发送数据,不允许接收
func squarer(out chan<- int, in <-chan int) {
	for i := range in {
		out <- i
	}
	close(out)
}

image-20230204153632236

select 语句

elect 是 Go 中的一个控制结构,类似于 switch 语句。

select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。

select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。

如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。

语法

Go 编程语言中 select 语句的语法如下:

select {
  case <- channel1:
    // 执行的代码
  case value := <- channel2:
    // 执行的代码
  case channel3 <- value:
    // 执行的代码

    // 你可以定义任意数量的 case

  default:
    // 所有通道都没有准备好,执行的代码
}

以下描述了 select 语句的语法:

  • 每个 case 都必须是一个通道

  • 所有 channel 表达式都会被求值

  • 所有被发送的表达式都会被求值

  • 如果任意某个通道可以进行,它就执行,其他被忽略。

  • 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。

    否则:

    1. 如果有 default 子句,则执行该语句。
    2. 如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。

案例

不断地从两个通道中获取到的数据,当两个通道都没有可用的数据时,会输出 "no message received"。

package main

import "fmt"

func main() {
	// 定义两个通道
	ch1 := make(chan string)
	ch2 := make(chan string)

	// 启动两个 goroutine,分别从两个通道中获取数据
	go func() {
		for {
			ch1 <- "from 1"
		}
	}()
	go func() {
		for {
			ch2 <- "from 2"
		}
	}()

	// 使用 select 语句非阻塞地从两个通道中获取数据
	for {
		select {
		case msg1 := <-ch1:
			fmt.Println(msg1)
		case msg2 := <-ch2:
			fmt.Println(msg2)
		default:
			// 如果两个通道都没有可用的数据,则执行这里的语句
			fmt.Println("no message received")
		}
	}
}

image-20230204155551703

标签:fmt,chan,并发,func,GO,main,make,通道
From: https://www.cnblogs.com/makalochen/p/17091859.html

相关文章