Golang协程,通道详解

news2025/1/19 7:36:38

进程、线程以及并行、并发

关于进程和线程

进程(Process)就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位,进程是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间。
一个进程至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。
通俗的讲进程就是一个正在执行的程序。
线程 是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位;
一个进程可以创建多个线程,同一个进程中的多个线程可以并发执行,一个程序要运行的话至少有一个进程。

关于并行和并发

并发:多个线程同时竞争一个位置,竞争到的才可以执行,每一个时间段只有一个线程在执行。
并行:多个线程可以同时执行,每一个时间段,可以有多个线程同时执行。
总结而言:
多线程程序在单核 CPU 上面运行就是并发
多线程程序在多核 CUP 上运行就是并行。
如果线程数大于 CPU 核数,则多线程程序在多个 CPU 上面运行既有并行又有并发;

 Golang 中的协程(goroutine)以及主线程

golang 中的主线程:(可以理解为线程/也可以理解为进程),在一个 Golang 程序的主线程上可以起多个协程
Golang 中多协程可以实现并行或者并发。
协程:可以理解为用户级线程,这是 对内核透明的,也就是系统并不知道有协程的存在 ,是完全由用户自己的程序进行调度的。Golang 的一大特色就是从语言层面原生支持协程,在函数或者方法前面加 go 关键字就可创建一个协程。可以说 Golang 中的协程就是goroutine 。
Golang 中的多协程有点类似其他语言中的多线程。
多协程和多线程:Golang 中每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。 OS 线程(操作系统线程)一般都有固定的栈内存(通常为 2MB 左右),一个 goroutine (协程) 占用内存非常小,只有 2KB 左右,多协程 goroutine 切换调度开销方面远比线程要少。 这也是为什么越来越多的大公司使用 Golang 的原因之一。

Goroutine 的使用

案例如下:
package main

import(
	"fmt"
	"time"
)

// 在主线程中也每隔10毫输出"卫宫士郎", 输出2次后,退出程序
// 要求主线程和goroutine同时执行
func test() {
	for i := 0; i < 10; i++ {
		fmt.Println("test() 测试专用..........")
		time.Sleep(time.Millisecond * 100)
	}
}



func main(){

	go test()
	for i := 1; i <=2; i++ {
		fmt.Println("main () 卫宫士郎")
		time.Sleep(time.Millisecond*10)
	}

}

暴露出一个问题:主线程执行完毕后即使协程没有执行完毕

所以我们对代码进行改造,可以让主线程和协程并行的同时,主线程执行完毕还不会同时带领协程退出运行。

 注意:
1、主线程执行完毕后即使协程没有执行完毕程序也会退出

2、协程可以在主线程没有执行完毕前提前退出协程是否执行完毕不会影响主线程的执行为了保证我们的程序可以顺利执行我们想让协程执行完毕后在执行主进程退出。

这个时候我们可以使用sync.WaitGroup 等待协程执行完毕

 sync.WaitGroup

sync.WaitGroup 可以实现主线程等待协程执行完毕。
package main

import(
	"fmt"
	"time"
	"sync"
)

// 在主线程中也每隔10毫输出"卫宫士郎", 输出2次后,退出程序
// 要求主线程和goroutine同时执行
//主线程退出后所有的协程无论有没有执行完毕都会退出,所以我们在主进程中可以通过WaitGroup等待协程执行完毕
var sw sync.WaitGroup

func test() {
	for i := 0; i < 10; i++ {
		fmt.Println("test() 测试专用..........")
		time.Sleep(time.Millisecond * 100)
	}
	sw.Done() //协程计数器-1
}



func main(){
	sw.Add(1) //协程计数器+1
	go test()//表示开启一个协程

	for i := 1; i <=2; i++ {
		fmt.Println("main () 卫宫士郎")
		time.Sleep(time.Millisecond*10)
	}
	sw.Wait() //等待协程执行完毕...
	fmt.Println("主线程执行完毕、、、、、、")
}

启动多个 Goroutine 

package main

import(
	"fmt"
	"time"
	"sync"
)

// 多个协程Goroutine启动

var sw sync.WaitGroup

func test0() {
	for i := 0; i < 5; i++ {
		fmt.Println("test0() 测试专用..........")
		time.Sleep(time.Millisecond * 100)
	}
	sw.Done() //协程计数器-1
}

func test1() {
	for i := 0; i < 5; i++ {
		fmt.Println("test1() 测试专用..........")
		time.Sleep(time.Millisecond * 100)
	}
	sw.Done() //协程计数器-1
}



func main(){
	sw.Add(1) //协程计数器+1
	go test0()//表示开启一个协程
	sw.Add(1)//协程计数器+1
	go test1()//表示开启一个协程
	for i := 1; i <=2; i++ {
		fmt.Println("main () 卫宫士郎")
		time.Sleep(time.Millisecond*10)
	}
	sw.Wait() //等待协程执行完毕...
	fmt.Println("主线程执行完毕、、、、、、")
}

多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为 10 个 goroutine
是并发执行的,而 goroutine 的调度是随机的。

设置 Golang 并行运行的时候占用的 cup 数量

Go 运行时的调度器使用 GOMAXPROCS 参数来确定需要使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的机器上,调度器会把 Go 代码同时调度到 8 OS 线程上。
Go 语言中可以通过 runtime.GOMAXPROCS() 函数设置当前程序并发时占用的 CPU 逻辑核心数。
Go1.5 版本之前,默认使用的是单核心执行。 Go1.5 版本之后,默认使用全部的 CPU 逻辑核心数。
package main

import (
	"fmt"
	"runtime"
)

func main() {
	//获取当前计算机上面的Cup个数
	cpuNum := runtime.NumCPU()
	fmt.Println("cpuNum=", cpuNum)

	//可以自己设置使用多个cpu
	runtime.GOMAXPROCS(cpuNum - 1)
	fmt.Println("设置完成")
}



//cpuNum= 8
//设置完成

来求一个素数的操作如下:

package main

import (
	"fmt"
	"time"
)


func main() {

	start := time.Now().Unix()
	fmt.Println(start)
	for num := 2; num < 10; num++ {
		var flag = true
		for i := 2; i < num; i++ {
			if num%i == 0 {
				flag = false
				break
			}
		}
	if  flag {
			fmt.Println(num, "是素数")
		}
	}
	end := time.Now().Unix()
	
	fmt.Println(end)
	fmt.Println(end-start) 

}

goroutine  for循环实现

package main

import (
	"fmt"
	"sync"
	"time"
)

//需求:要统计1-120000的数字中那些是素数?goroutine  for循环实现

/*
1 协程  统计  1-30000

2 协程  统计  30001-60000

3 协程  统计  60001-90000

4 协程  统计  90001-120000

// start:(n-1)*30000+1       end:n*30000
*/
var wg sync.WaitGroup

func test(n int) {
	for num := (n-1)*30000 + 1; num < n*30000; num++ {
		if num > 1 {
			var flag = true
			for i := 2; i < num; i++ {
				if num%i == 0 {
					flag = false
					break
				}
			}
			if flag {
				// fmt.Println(num, "是素数")
			}
		}
	}
	wg.Done()
}

func main() {

	for i := 1; i <= 4; i++ {
		wg.Add(1)
		go test(i)
	}
	wg.Wait()
	fmt.Println("执行完毕")



}

Channel 管道

channel

单纯地将函数并发执行是没有意义的。

函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。

channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel类型

channel是一种类型,一种引用类型。声明通道类型的格式如下:

    var 变量 chan 元素类型  

举几个例子:

    var ch1 chan int   // 声明一个传递整型的通道
    var ch2 chan bool  // 声明一个传递布尔型的通道
    var ch3 chan []int // 声明一个传递int切片的通道    

创建channel

通道是引用类型,通道类型的空值是nil。

var ch chan int
fmt.Println(ch) // <nil>
package main

import "fmt"

func main() {

	ch1 := make(chan int ,4)

	ch1<- 1
	ch1<- 2
	ch1<- 3

	ch2 := ch1
	ch2<-4
	<-ch1
	<-ch1
	<-ch1
	d:= <-ch1
	fmt.Println(d)
}

//4

副本ch2的值添加后,取出ch1的值改变了

声明的通道后需要使用make函数初始化之后才能使用。

创建channel的格式如下:

    make(chan 元素类型, [缓冲大小])   

channel的缓冲大小是可选的。

举几个例子:

//创建一个能存储 10 个 int 类型数据的管道
ch1 := make(chan int, 10)
//创建一个能存储 4 个 bool 类型数据的管道
ch2 := make(chan bool, 4)
//创建一个能存储 3 个[]int 切片类型数据的管道
ch3 := make(chan []int, 3)
package main

import "fmt"

func main() {
	//创建channel
	ch := make(chan int, 3)
	//2、给管道里面存储数据
	ch <- 12
	ch <- 33
	ch <- 3234
	//获取管道里面的内容
	a := <-ch
	fmt.Println(a) //12
	<-ch //从管道里面取值   //33
	c := <-ch
	fmt.Println(c) //3234
	ch <- 1
	ch <- 22
	//打印管道的长度和容量
	fmt.Printf("值:%v 容量:%v 长度%v\n", ch, cap(ch), len(ch)) 
}

已经消费了的,就相当于没有,再添加的从新算 

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。

现在我们先使用以下语句定义一个通道:

ch := make(chan int)    

发送

将一个值发送到通道中。

ch <- 10 // 把10发送到ch中   

接收

从一个通道中接收值。

x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果   

关闭

我们通过调用内置的close函数来关闭通道。

    close(ch)   

 关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

    1.对一个关闭的通道再发送值就会导致panic。
    2.对一个关闭的通道进行接收会一直获取值直到通道为空。
    3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
    4.关闭一个已经关闭的通道会导致panic。  

管道阻塞

无缓冲的通道

如果创建管道的时候没有指定容量,那么我们可以叫这个管道为无缓冲的管道
无缓冲的管道又称为阻塞的管道。我们来看一下下面的代码:
package main
import (
	"fmt"
)

func main() {
    ch := make(chan int)
    ch <- 123
    fmt.Println("传递成功......")
}   

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        E:/goroutine_channel_demo/route_demo/main.go:8 +0x31
exit status 2

为什么会出现死锁

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有接收值的时候才能发送值。(小区没代收快递点,需要快递小哥直接送到手上)

上面的代码会阻塞在ch <- i这一行代码形成死锁

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。

上面的代码会阻塞在ch <- 123这一行代码形成死锁,那如何解决这个问题呢?

一种方法是启用一个goroutine去接收值,例如:

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}   

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。

有缓冲的通道

解决上面问题的方法还有一种就是使用有缓冲区的通道。

package main
import (
	"fmt"
)


// func recover(ch chan int){
// 	rec := <- ch
// 	fmt.Println("接收成功",rec)
// }




func main() {
    ch := make(chan int,1)
	// go recover(ch)
    ch <- 123
    fmt.Println("传递成功......")
}   

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。(小区快递格子就一个,你取走了,别人能再放)

 循环遍历管道数据

循环的话,我们就会提到for,但是for有两种循环形式

for range 和 for 用两种方式来操作

for range循环遍历管道的值  ,注意:管道没有key

package main

import "fmt"

func main() {


	ch1 := make(chan int,5)

	for i := 1; i <= 5; i++ {
		ch1 <- i
	}

	for v := range ch1 {
		fmt.Println(v)
	}

}

我们发现虽然可以正常编译,运行,但是会出现如下情况:

1
2
3
4
5
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        E:/goroutine_channel_demo/route_demo/main.go:14 +0xb4
exit status 2

这样也会产生死锁,使用for range遍历通道,当通道被关闭的时候就会退出for range,如果没有关闭管道就会报错fatal error: all goroutines are asleep - deadlock!

如果通过for range循环的方式来从管道取数据,在插入数据的时候一定要close()

package main
import (
	"fmt"
)


func main() {
	var ch1 = make(chan int, 5)
	for i := 1; i <= 5; i++ {
		ch1 <- i
	}
	close(ch1) //关闭管道

	//for range循环遍历管道的值  ,注意:管道没有key
	for v := range ch1 {
		fmt.Println(v)
	}
}

通过内置的close()函数关闭channel(如果你的管道不往里存值或者取值的时候一定记得关闭管道)

 第二种方法

package main
import (
	"fmt"
)


func main() {
	//通过for循环遍历管道的时候管道可以不关闭
	var ch2 = make(chan int, 5)
	for i := 1; i <= 5; i++ {
		ch2 <- i
	}

	for j := 0; j < 5; j++ {
		fmt.Println(<-ch2)
	}
}

并发安全和锁

有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。

互斥锁

互斥锁是传统并发编程中对共享资源进行访问控制的主要手段,它由标准库 sync 中的 Mutex结构体类型表示。sync.Mutex 类型只有两个公开的指针方法,Lock 和 Unlock。Lock 锁定当前的共享资源,Unlock 进行解锁
package main

import (
	"fmt"
	"sync"
	"time"
)

var count = 0
var sw sync.WaitGroup

var mutex sync.Mutex

func test() {
	mutex.Lock()
	count++
	fmt.Println("the count is : ", count)
	time.Sleep(time.Millisecond)
	mutex.Unlock()
	sw.Done()
}

func main() {
	for r := 0; r < 20; r++ { //开启20个协程来进行这个操作
		wg.Add(1)
		go test()
	}
	sw.Wait()

}
使用互斥锁能够保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 则在等 待锁;当互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区,多个 goroutine 同时等
待一个锁时,唤醒的策略是随机的。
虽然使用互斥锁能解决资源争夺问题,但是并不完美,通过全局变量加锁同步来实现通讯,
并不利于多个协程对全局变量的读写操作。这个时候我们也可以通过另一种方式来实现上面
的功能管道(Channel)

读写互斥锁

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。

读写锁在Go语言中使用sync包中的RWMutex类型。

读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;

当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

package main

import(
	"fmt"
	"sync"
	"time"
)

var (
    x      int64
    wg     sync.WaitGroup
    lock   sync.Mutex
    rwlock sync.RWMutex
)

func write() {
    // lock.Lock()   // 加互斥锁
    rwlock.Lock() // 加写锁
    x = x + 1
    time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
	fmt.Println("=========进行写操作")
    rwlock.Unlock()                   // 解写锁
    // lock.Unlock()                     // 解互斥锁
    wg.Done()
}

func read() {
    // lock.Lock()                  // 加互斥锁
    rwlock.RLock()               // 加读锁
    time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
	fmt.Println("=========进行读操作")
    rwlock.RUnlock()             // 解读锁
    // lock.Unlock()                // 解互斥锁
    wg.Done()
}

func main() {
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go write()
    }

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go read()
    }

    wg.Wait()

}

/*



*/
总结:
当我们对一个不会变化的数据只做“读”操作的话,是不存在资源竞争的问题的。
因为数据是不变的,不管怎么读取,多少 goroutine 同时读取,都是可以的。
所以问题不是出在“读”上,主要是修改,也就是“写”。
修改的数据要同步,这样其他goroutine 才可以感知到。
所以真正的互斥应该是读取和修改、修改和修改之间,读和读是没有互斥操作的必要的。
因此,衍生出另外一种锁,叫做读写锁
读写锁可以让 多个读操作并发,同时读取 ,但是 对于写操作是完全互斥 的。
(也就是说,当一个 goroutine 进行写操作的时候,其他 goroutine 既不能进行读操作,也不能进行写操作)

Goroutine 结合 Channel 管道

需求 1

定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据。要求同步进行。
1 、开启一个 fn1 的的协程给向管道 inChan 中写入 100 条数据
2 、开启一个 fn2 的协程读取 inChan 中写入的数据
3 、注意: fn1 fn2 同时操作一个管道
4 、主线程必须等待操作完成后才可以退出
package main

import (
	"fmt"
	"sync"
	"time"
)
//这是一个无缓存通道案例
//定义sync等待协程完毕
var wg sync.WaitGroup

func fn1(intChan chan int) {
	for i := 0; i < 10; i++ {
		intChan <- i + 1
		fmt.Println("写入数据=", i+1)
		time.Sleep(time.Millisecond * 100)
	}
	close(intChan)  //写入操作完毕,关闭写入的协程
	wg.Done()
}
func fn2(intChan chan int) {
	for v := range intChan {  //通道回显只有一个值
		fmt.Printf("读到数据=%v\n", v)
		time.Sleep(time.Millisecond * 50)
	}
	wg.Done()
}
func main() {
	allChan := make(chan int, 100)
	wg.Add(1)
	go fn1(allChan)
	wg.Add(1)
	go fn2(allChan)
	wg.Wait()
	fmt.Println("读取完毕...")
}

 需求 2

goroutine 结合 channel 实现统计 1-120 的数字中那些是素数?

package main

import(
	"fmt"
	"sync"
)

var sw sync.WaitGroup
//向 intChan放入 1-120个数,创建协程
func putNum(intChan chan int ){
	for i := 0; i < 120; i++ {
		intChan <- i
	}
	close(intChan)
	sw.Done()
}

// 从 intChan取出数据,并判断是否为素数,如果是,就把得到的素数放在primeChan

func primeNum(intChan chan int,primeChan chan int, exitChan chan bool ){
	for num := range intChan {
		var flag = true
		for i := 2; i < num; i++ {
			if num%i == 0 {
				flag = false
				break
			}
		}
		if flag {
			primeChan <- num //num是素数
		}
}
	//要关闭 primeChan
	// close(primeChan) //如果一个channel关闭了就没法给这个channel发送数据了
	//什么时候关闭primeChan?
	//给exitChan里面放入一条数据
	exitChan <- true 
	sw.Done()

}

//printPrime打印素数的方法
func printPrime(primeChan chan int) {
	for v := range primeChan {
		fmt.Println(v)
	}
	sw.Done()
}


func main(){

	intChan := make(chan int,1000) //在intchan中放入数字
	primeChan := make(chan int,1000) //从 intChan取出数据,判断是否是素数
	exitChan := make(chan bool ,20) //标识primeChan close,内部数据满足设定的缓存数量就关闭

	//存放数字的协程
	sw.Add(1)
	go putNum(intChan)

	//统计素数的协程
	for i := 0; i < 20; i++ {   //你要开启几个primechan的协程就写几个,对应的exitchan要一致
		sw.Add(1)
		go primeNum(intChan ,primeChan , exitChan )
	}
	//打印素数的协程
	sw.Add(1)
	go printPrime(primeChan)

	//判断exitChan是否存满值
	sw.Add(1)
	go func() {
		for i := 0; i < 20; i++ {
			<-exitChan
		}

		close(primeChan) //关闭primeChan
		sw.Done()
	}()

	sw.Wait()
	fmt.Println("执行完毕....")


}

单向管道

有的时候我们会将管道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用管道都会对其进行限制,比如限制管道在函数中只能发送或只能接收。
案例如下:
package main

import "fmt"

//单向管道
func main() {

	// 1、在默认情况下下,管道是双向
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	a := <-ch
	b := <-ch
	fmt.Println(a, b) //1,2

	// 2、管道声明为只写
	ch1 := make(chan<- int, 2)
	ch1 <- 10
	ch1 <- 12
	// <-ch1   //receive from send-only type chan<- int

	// 3、管道声明为只读

	ch2 := make(<-chan int, 2)
	ch2 <- 3
	c := <-ch2
	fmt.Println(c) //.\main.go:25:2: invalid operation: cannot send to receive-only channel ch2 (variable of type <-chan int)

}

修改之前的案例如下:

package main

import (
	"fmt"
	"sync"
	"time"
)
//这是一个无缓存通道案例
//定义sync等待协程完毕
var wg sync.WaitGroup

func fn1(intChan chan<- int) {
	for i := 0; i < 10; i++ {
		intChan <- i + 1
		fmt.Println("写入数据=", i+1)
		time.Sleep(time.Millisecond * 100)
	}
	close(intChan)  //写入操作完毕,关闭写入的协程
	wg.Done()
}
func fn2(intChan <-chan int) {
	for v := range intChan {  //通道回显只有一个值
		fmt.Printf("读到数据=%v\n", v)
		time.Sleep(time.Millisecond * 50)
	}
	wg.Done()
}
func main() {
	allChan := make(chan int, 100)
	wg.Add(1)
	go fn1(allChan)
	wg.Add(1)
	go fn2(allChan)
	wg.Wait()
	fmt.Println("读取完毕...")
}

/*
写入数据= 1
读到数据=1
写入数据= 2
读到数据=2
写入数据= 3
读到数据=3
写入数据= 4
读到数据=4
写入数据= 5
读到数据=5
写入数据= 6
读到数据=6
写入数据= 7
读到数据=7
写入数据= 8
读到数据=8
写入数据= 9
读到数据=9
写入数据= 10
读到数据=10
读取完毕...
*/

 select 多路复用

在某些场景下我们需要同时从多个通道接收数据,这个时候就可以用到golang中给我们提供的select多路复用

如果只想在main方法内进行,就可以用这个方法,其他的就是定义协程了

使用select来获取channel里面的数据的时候不需要关闭channel

package main

import(
	"fmt"
	"time"
)


func main(){
// 在某些场景下我们需要同时从多个通道接收数据,这个时候就可以用到golang中给我们提供的select多路复用
	//如果只想在main方法内进行,就可以用这个方法,其他的就是定义协程了
	//1.定义一个管道 10个数据int
intoChan := make(chan int ,10)
for i := 0; i < 10; i++ {
	intoChan <- i
}
//2.定义一个管道 5个数据string
stringChan := make(chan string,5)
for i := 0; i < 5; i++ {
	stringChan <- "卫宫士郎" 
}
//定义一个for的无限循环
for{
	select{
	case value := <- intoChan:
		fmt.Printf("从 intChan 读取的数据%d\n", value)
	case value := <-stringChan:
		fmt.Printf("从 stringChan 读取的数据%v\n", value)
		time.Sleep(time.Millisecond * 50)
	default:
		fmt.Printf("数据获取完毕")
		return //注意退出...
	}
}


}

/*
从 stringChan 读取的数据卫宫士郎
从 stringChan 读取的数据卫宫士郎
从 intChan 读取的数据0
从 intChan 读取的数据1
从 stringChan 读取的数据卫宫士郎
从 intChan 读取的数据2
从 intChan 读取的数据3
从 stringChan 读取的数据卫宫士郎
从 intChan 读取的数据4
从 stringChan 读取的数据卫宫士郎
从 intChan 读取的数据5
从 intChan 读取的数据6
从 intChan 读取的数据7
从 intChan 读取的数据8
从 intChan 读取的数据9
数据获取完毕

*/
select 的使用类似于 switch 语句,它有一系列 case 分支和一个默认的分支。每个 case 会对
应一个管道的通信(接收或发送)过程。
select 会一直等待,直到某个 case 的通信操作完成 时,就会执行 case 分支对应的语句。

Goroutine Recover 解决协程中出现的 Panic

defer + recover

延迟执行(定义的func自执行函数出现问题就交给defer)其他的协程还可以继续进行

package main

import (
	"fmt"
	"time"
)

//函数
func test0() {
	for i := 0; i < 10; i++ {
		time.Sleep(time.Millisecond * 50)
		fmt.Println("远坂凛")
	}
}

//函数
func test1() {
	//这里我们可以使用defer + recover 
	//延迟执行(定义的func自执行函数出现问题就交给defer)
	//其他的协程还可以继续进行
	defer func() {
		//捕获test抛出的panic
		if err := recover(); err != nil {
			fmt.Println("test1() 发生错误", err)
		}
	}()
	//定义了一个map
	var myMap map[int]string
	myMap[0] = "golang" //error

	
}

func main() {

	go test0()
	go test1()

	//防止主进程退出这里使用time.Sleep演示,搭建也可以用sync.WaitGroup
	time.Sleep(time.Second)
}

注意,调用recover()来捕获 goroutine 恐慌只在一个defer函数内部有用;否则,该函数将返回nil并且没有其他作用。这是因为defer函数也是在周围函数恐慌时执行的。

在 Go 中,panic是一个停止普通流程的内置函数:

func main() {
    fmt.Println("a")
    panic("foo")
    fmt.Println("b")
}

该代码打印a,然后在打印b之前停止:

a
panic: foo

goroutine 1 [running]:
main.main()
        main.go:7 +0xb3

一旦恐慌被触发,它将继续在调用栈中向上运行,直到当前的 goroutine 返回或者panicrecover捕获:

func main() {
    defer func() {                       // ❶
        if r := recover(); r != nil {
            fmt.Println("recover", r)
        }
    }()

    f()                                  // ❷
}

func f() {
    fmt.Println("a")
    panic("foo")
    fmt.Println("b")
}

❶ 延迟闭包内调用recover

❷ 调用ff恐慌。这种恐慌被前面的recover所抓住。

f函数中,一旦panic被调用,就停止当前函数的执行,并向上调用栈:main。在main中,因为恐慌是由recover引起的,所以并不停止 goroutine:

a
recover foo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/884330.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

leetcode611. 有效三角形的个数(java)

有效三角形的个数 有效三角形的个数排序加二分排序 双指针 上期算法 有效三角形的个数 给定一个包含非负整数的数组 nums &#xff0c;返回其中可以组成三角形三条边的三元组个数。 示例 1: 输入: nums [2,2,3,4] 输出: 3 解释:有效的组合是: 2,3,4 (使用第一个 2) 2,3,4 (使…

python进阶做题日记365-1,

第一天&#xff1a; 1&#xff1a;题目名称&#xff1a;代写匿名信时间限制&#xff1a;1000ms内存限制&#xff1a;256M 题目描述小Q想要匿名举报XX领导不务正业&#xff01; 小Q害怕别人认出他的字迹。 他选择从报纸上剪裁下来英文字母组成自己的举报信。 现在小Q找来了报纸…

【分布式存储】数据存储和检索~倒排索引pageRank

为什么需要倒排索引 通过前两篇的文章介绍&#xff0c;B树主要针对的是读多写少的场景&#xff0c;而LSM针对的是写多读少的场景&#xff0c;其实在日常开发中&#xff0c;我们会将数据存储到搜索引擎中&#xff0c;然后进行数据的搜索&#xff0c;这种场景其实针对的是快速根…

AI幕后的关键技术之一——HBM

半导体产业步入下行周期之际&#xff0c;2023年ChatGPT的“走红”为产业带来新的发展方向&#xff1a;AI人工智能。 ChatGPT正掀起一场声势浩大的AI浪潮&#xff0c;AI时代下&#xff0c;为满足海量数据存储以及日益增长的繁重计算要求&#xff0c;半导体存储器领域也迎来新的变…

shell第九章iptables防火墙

防火墙&#xff1a;隔离功能 部署点&#xff1a;部署在网络边缘&#xff0c;或者主机边缘&#xff0c;在工作中&#xff0c;防火墙的主要作用&#xff0c;决定那些数据可以被外网访问&#xff0c;以及哪些数据可以进入内网访问 讨论防火墙&#xff0c;主要就是在网络层进行讨…

无涯教程-Perl - setsockopt函数

描述 此函数将SocketoptionsOPTNAME的值设置为SOCKET上指定级别的OPTVAL值。您需要导入Socket模块,以获取Tabl中显示的OPTNAME的有效值 语法 以下是此函数的简单语法- setsockopt SOCKET, LEVEL, OPTNAME, OPTVAL返回值 如果失败,此函数返回undef&#xff1b;如果成功,则返…

java.lang.NoClassDefFoundError: org/apache/tez/dag/api/TezConfiguration

错误&#xff1a; java.lang.NoClassDefFoundError: org/apache/tez/dag/api/TezConfigurationat org.apache.hadoop.hive.ql.exec.tez.TezSessionPoolSession$AbstractTriggerValidator.startTriggerValidator(TezSessionPoolSession.java:74)at org.apache.hadoop.hive.ql.e…

数据集成革新:去中心化微服务集群的无限潜能

在当今数据密集型的业务环境下&#xff0c;传统的集中式架构已经难以满足高可用性和高并发性的要求。而去中心化微服务集群则通过分散式的架构&#xff0c;将系统划分为多个小型的、独立部署的微服务单元&#xff0c;每个微服务负责特定的业务功能&#xff0c;实现了系统的高度…

bytesec靶场

靶场下载 https://www.vulnhub.com/entry/hacknos-os-bytesec,393/ 下载完成后进入配置修改网卡 教程 配置 第一步&#xff1a;启动靶机时按下 shift 键&#xff0c; 进入以下界面 第二步&#xff1a;选择第二个选项&#xff0c;然后按下 e 键&#xff0c;进入编辑界面 将…

python快速两两元素求相似矩阵

目录 1. 计算相似度矩阵2. 基于sklearn 1. 计算相似度矩阵 计算相似度矩阵的方法有很多种&#xff0c;发现了sklearn中直接有通过计算余弦相似度得到相似度矩阵的方法 1 sklearn.metrics.pairwise.cosine_similarity # 余弦相似度 2 sklearn.metrics.pairwise.pairwise_distan…

【NetCore】09-中间件

文章目录 中间件&#xff1a;掌控请求处理过程的关键1. 中间件1.1 中间件工作原理1.2 中间件核心对象 2.异常处理中间件:区分真异常和逻辑异常2.1 处理异常的方式2.1.1 日常错误处理--定义错误页的方法2.1.2 使用代理方法处理异常2.1.3 异常过滤器 IExceptionFilter2.1.4 特性过…

NTN(六) switchover

NTN中的switchover包括feeder link switchover和 serving link switch。所谓feeder link switchover就是将feeder link从source NTN 网关更改为特定 NTN payload的target NTN 网关的过程。 feeder link switchover是网络层过程。 而service link switch则是指serving NTN paylo…

OpenCV分析tfboys十周年演唱会灯牌大战结果

前言 在Android音视频开发中&#xff0c;网上知识点过于零碎&#xff0c;自学起来难度非常大&#xff0c;不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》&#xff0c;结合我自己的工作学习经历&#xff0c;我准备写一个音视频系列blog。本文是音视频系…

【代码质量】认知复杂度(COGNITIVE COMPLEXITY)一种衡量可理解性的新方法

白皮书地址 摘要&#xff1a;圈复杂度最初是作为“可测试性和模块控制流的“可维护性”。虽然它擅长于衡量前者&#xff0c;但它的数学模型不能产生一个令人满意的值来衡量后者。本文描述一种打破数学度量模型的新度量模型来评估代码&#xff0c;以弥补圈复杂度的缺点&#xf…

【科研论文配图绘制】task1 掌握科研绘图的基本知识

【科研论文配图绘制】task1 掌握科研绘图的基本知识 写在最前 8月份Datawhale组队学习&#xff0c;写下该博客记录学习内容 1.科研论文配图的分类与构成 2.科研论文配图的格式和尺寸 3.科研论文配图中的字体和字号设置 4.科研论文配图的版式设计、结构布局和颜色搭配 占个…

【校招VIP】CSS校招考点之选择器优先级

考点介绍&#xff1a; 选择器是CSS的基础&#xff0c;也是校招中的高频考点&#xff0c;特别是复合选择器的执行优先级&#xff0c;同时也是实战中样式不生效的跟踪依据。 因为选择器的种类较多&#xff0c;很难直接记忆&#xff0c;可以考虑选择一个相对值&#xff0c;比如id类…

day4 IO模型

IO多路复用 1.select函数 服务器&#xff1a; 客户端 poll函数 客户端&#xff1a;

《Java-SE-第三十八章》之注解

前言 在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!” 博客主页&#xff1a;KC老衲爱尼姑的博客主页 博主的github&#xff0c;平常所写代码皆在于此 共勉&#xff1a;talk is cheap, show me the code 作者是爪哇岛的新手&#xff0c;水平很有限&…

每日记--前端解决方案--el-select下拉样式-el-option内容过长-鼠标悬停到文字不修改光标样式-设置透明

文章目录 el-select下拉样式el-select中el-option内容过长解决办法鼠标悬停到文字不修改光标样式设置透明 el-select下拉样式 element-ui自带样式设置popper-class el-select中el-option内容过长解决办法 问题&#xff1a;像这样选项太长了&#xff0c;不好看 解决&#xf…

关于Linux文件系统只读问题的修改笔记

1.问题 2. 原因 系统异常关机或者代码修改错误导致硬盘挂载出现问题开启只读模式&#xff0c;但是重启有时候可以解决。 3.解决方法 1. mount查看那个挂载的硬盘出现问题(ro标识只读) mount | grep ro2.找到硬盘&#xff0c;重新挂载即可 sudo mount -o remount,rw /sys/f…