Golang协程,通道详解

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

关于进程和线程

进程(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.mfbz.cn/a/77224.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

iTOP-RK3588开发板安装TFTP服务端

首先在 ubuntu 中执行以下命令安装 TFTP 服务&#xff1a; apt-get install tftp-hpa tftpd-hpa 安装完成以后创建 TFTP 服务器工作目录,并对 TFTP 的服务配置文件进行修改,具体步骤如下&#xff1a; 输入以下命令在家目录创建 tftpboot 文件夹&#xff0c;如下图所示&#x…

Prompt、RAG、微调还是重新训练?如何选择正确的生成式AI的使用方法

生成式人工智能正在快速发展&#xff0c;许多人正在尝试使用这项技术来解决他们的业务问题。一般情况下有4种常见的使用方法&#xff1a; Prompt EngineeringRetrieval Augmented Generation (RAG 检索增强生成)微调从头开始训练基础模型(FM) 本文将试图根据一些常见的可量化…

爬虫逆向实战(十七)--某某丁简历登录

一、数据接口分析 主页地址&#xff1a;某某丁简历 1、抓包 通过抓包可以发现数据接口是submit 2、判断是否有加密参数 请求参数是否加密&#xff1f; 通过查看“载荷”模块可以发现有一个enPassword加密参数 请求头是否加密&#xff1f; 通过查看请求头可以发现有一个To…

C++学习系列之动态库报错问题

C学习系列之动态库报错问题 啰嗦问题解决总结 啰嗦 动态库已建&#xff0c;C文件一加&#xff0c;全是报错&#xff0c;一片红。 问题 解决 解决办法就是加标头 总结 小问题&#xff0c;记录一下。

基于 KubeSphere 的应用容器化在智能网联汽车领域的实践

公司简介 某国家级智能网联汽车研究中心成立于 2018 年&#xff0c;是担当产业发展咨询与建议、共性技术研发中心、创新成果转化的国家级创新平台&#xff0c;旨在提高我国在智能网联汽车及相关产业在全球价值链中的地位。 目前着力建设基于大数据与云计算的智能汽车云端运营…

边缘智能聚焦嵌入式世界

没有什么超出了我们的想象力的极限&#xff0c;我们习惯于在间谍电影中看到的东西需要进行大规模升级&#xff0c;以超越现在认为的标准。 德国纽伦堡—一切都超出了我们的想象范围&#xff0c;而且我们习惯于在间谍电影中看到的东西需要进行大规模升级&#xff0c;以超越现在认…

段错误核心转储

在linux下运行可执行文件的时候出现了以下错误&#xff1a; error:segmentation fault core dumped解决方法&#xff1a; #查看core文件大小判断是否可写 $ ulimit -a real-time non-blocking time (microseconds, -R) unlimited core file size (blocks, -c) …

30W IP网络有源音箱 校园广播音箱

SV-7042XT是深圳锐科达电子有限公司的一款2.0声道壁挂式网络有源音箱&#xff0c;具有10/100M以太网接口&#xff0c;可将网络音源通过自带的功放和喇叭输出播放&#xff0c;可达到功率30W。同时它可以外接一个30W的无源副音箱&#xff0c;用在面积较大的场所。5寸进口全频低音…

初始C语言(6)——详细讲解表达式求值以及其易错点

系列文章目录 第一章 “C“浒传——初识C语言&#xff08;1&#xff09;&#xff08;更适合初学者体质哦&#xff01;&#xff09; 第二章 初始C语言&#xff08;2&#xff09;——详细认识分支语句和循环语句以及他们的易错点 第三章 初阶C语言&#xff08;3&#xff09;——…

GaussDB 实验篇+openGauss的4种1级分区案例

✔ 范围分区/range分区 -- 创建表 drop table if exists zzt.par_range; create table if not exists zzt.par_range (empno integer,ename char(10),job char(9),mgr integer(4),hiredate date,sal numeric(7,2),comm numeric(7,2),deptno integer,constraint pk_par_emp pri…

Python程序设计——列表

一、引言 关键点&#xff1a;一个列表可以存储任意大小的数据集合。 程序一般都需要存储大量的数值。假设&#xff0c;举个例子&#xff0c;需要读取100个数字&#xff0c;计算出它们的平均值&#xff0c;然后找出多少个数字是高于这个平均值的。程序首先读取100个数字并计算它…

C语言刷题训练DAY.6

1.进制AB 解题思路&#xff1a; 这里我们按照备注的提示&#xff0c;调整输入格式。 注意&#xff1a;%x是十六进制的数字 %o是八进制的数字 解题代码&#xff1a; #include<stdio.h> int main() {int a 0;int b 0;scanf("0x%x 0%o", &a, &b);pri…

自定义Android滑块拼图验证控件

自定义Android滑块拼图验证控件 拼图认证视图默认策略工具类参考 1、继承自AppCompatImageView&#xff0c;兼容ImageView的scaleType设置&#xff0c;可设置离线/在线图片。 2、通过设置滑块模型&#xff08;透明背景的图形块&#xff09;设置滑块&#xff08;和缺省块&#x…

sql server 存储过程 set ansi_nulls set quoted_identifier,out 、output

SQL-92 标准要求在对空值(NULL) 进行等于 () 或不等于 (<>) 比较时取值为 FALSE。 当 SET ANSI_NULLS 为 ON 时&#xff0c;即使 column_name 中包含空值&#xff0c;使用 WHERE column_name NULL 的 SELECT 语句仍返回零行。即使 column_name 中包含非空值&#xff0c…

python列表笔记,python列表用法及基础操作

列表的介绍 定义100个变量&#xff0c;每个变量存放一个学生的姓名可行吗&#xff1f;有更好的办法吗&#xff1f; 答&#xff1a; 列表 一、列表的格式 定义列的格式&#xff1a;[元素1, 元素2, 元素3, ..., 元素n] 变量tmp的类型为列表 tmp [xiaoWang,180, 65.0] 列…

亿赛通电子文档安全管理系统任意文件上传漏洞复现

0x01 产品简介 亿赛通电子文档安全管理系统&#xff08;简称&#xff1a;CDG&#xff09;是一款电子文档安全加密软件&#xff0c;该系统利用驱动层透明加密技术&#xff0c;通过对电子文档的加密保护&#xff0c;防止内部员工泄密和外部人员非法窃取企业核心重要数据资产&…

【人工智能124种任务大集合】-集齐了自然语言处理(NLP),计算机视觉(CV),语音识别,多模态等任务

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能124种任务大集合&#xff0c;任务集合主要包括4大类&#xff1a;自然语言处理&#xff08;NLP&#xff09;、计算机视觉&#xff08;CV&#xff09;、语音识别、多模态任务。 我这里整理了124种应用场景任…

FPGA_学习_14_第一个自写模块的感悟和ila在线调试教程与技巧(寻找APD的击穿偏压)

前一篇博客我们提到了&#xff0c;如果要使用算法找到Vbr&#xff0c;通过寻找APD采集信号的噪声方差的剧变点去寻找Vbr是一个不错的方式。此功能的第一步是在FPGA中实现方差的计算&#xff0c;这个我们已经在上一篇博客中实现了。 继上一篇博客之后&#xff0c;感觉过了很久了…

.net连接mysql,提示找不到请求的 .Net Framework Data Provider。可能没有安装

开发完成的.net程序需要连接mysql数据库&#xff0c;在个人电脑上运行没问题&#xff0c;别人运行时提示“提示找不到请求的 .Net Framework Data Provider。可能没有安装”。经过查询&#xff0c;安装Connector/NET 8.1.0&#xff0c;下载地址如下所示&#xff1a; https://d…

AI抢饭碗!新闻集团将使用生成式AI,每周自动写3000篇新闻丨IDCF

作者&#xff1a;AIGC开放社区 8月1日&#xff0c;英国卫报消息&#xff0c;全球最大新闻媒体公司之一的新闻集团&#xff0c;将使用生成式AI每周自动创建3000篇澳大利亚本地新闻。 据悉&#xff0c;新闻集团在内部成立了一个名为“Data Local”的部门只有4名员工&#xff0c;…
最新文章