教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 2020-10-19Go语言goroutine和channel

2020-10-19Go语言goroutine和channel

发布时间:2021-12-02   编辑:jiaochengji.com
教程集为您提供2020-10-19Go语言goroutine和channel等资源,欢迎您收藏本站,我们将为您提供最新的2020-10-19Go语言goroutine和channel资源
<h2>Golang 协程 goroutine 介绍</h2> <h2>多线程知识点了解</h2>

进程

进程是(Process)就是程序在操作系统的一次执行过程。是系统进行资源分配和调度的基本单位,进程是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间。一个进程至少有五种状态:初始态,执行态,等待状态,就绪状态,终止状态。

线程

线程是进程的一个执行路径,一个进程中至少有一个线程,它是比进程更小的能独立运行的基本单位,因为真正占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。

并发和并行

并发:多个线程同时竞争一个位置,竞争到才可以执行,每个时间段只有一个线程在执行。

并行:多个线程可以同时执行,每一个时间段,可以有多个线程同时执行。

通俗来讲,多线程在单位CPU上面运行就是并发,多线程在多核CPU上运行就是并行,如果线程数大于CPU核数,则对哦县城程序在多个CPU上既有并行又有并发。


<h3>Golang中协程(goroutine)以及主线程</h3>

Golang 中的主线程:(可以理解为线程、也可以理解为进程),在一个Golang程序的主线程可以其多个协程,Golang中多协程可以实现并行或者并发。

协程

可以理解为用户级线程,这是对内核透明的,也就是系统并不知道有写成的存在,是完全有用户自己的程序进行调度的。Golang 的一大特色就是从语言层面支持协程,在函数或者方法面前加 go 关键字就可以创建一个协程,也就是说 Golang 中的协程就是 goroutine。

Golang 中的多协程有点类似与Java语言中的多线程

多协程和多进程

Golang 中每个 goroutine(协程)默认占用内存远比 java、C的线程少

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个 goroutine (协程)占用内存非常小,只有2kb左右,多协程 goroutine 切换调度开销方面也比线程要小

这也是为什么越来越多的大公司使用 Golang 的原因之一


<h3>goroutine 的使用以及 sync.WaitGroup</h3>

实例:开启一个goroutine,该协程每隔50毫秒秒输出“你好golang"

<pre><code class="language-html">// 协程需要运行的方法 func test() { for i := 0; i < 5; i { fmt.Println("test 你好golang") time.Sleep(time.Millisecond * 100) } } func main() { ​ // 通过go关键字,就可以直接开启一个协程 go test() ​ // 这是主进程执行的 for i := 0; i < 5; i { fmt.Println("main 你好golang") time.Sleep(time.Millisecond * 100) } } ​ ​ //运行结果如下,我们能够看到他们之间不存在所谓的顺序关系了 main 你好golang test 你好golang main 你好golang test 你好golang test 你好golang main 你好golang main 你好golang test 你好golang test 你好golang main 你好golang</code></pre>

但是上述的代码其实还有问题,当主进程执行完毕后,不管协程有没有执行完成,都会退出

转存失败重新上传取消

 

<blockquote>

这是使用我们就需要用到 sync.WaitGroup等待协程

</blockquote> <ul><li>

首先我们需要创建一个协程计数器

</li></ul><pre><code class="language-html">// 定义一个协程计数器 var wg sync.WaitGroup</code></pre>

 

<ul><li>

然后当我们开启协程的时候,我们要让计数器加1

</li></ul><pre><code class="language-html">// 开启协程,协程计数器加1 wg.Add(1) go test2()</code></pre>

 

<ul><li>

当我们协程结束前,我们需要让计数器减1

</li></ul><pre><code class="language-html">// 协程计数器减1 wg.Done()</code></pre>

 

<ul><li>

最后在main方法添加等待方法

</li></ul><pre><code class="language-html"> wg.Wait()</code></pre>

 

完整示例:

<pre><code class="language-html">package main ​ import ( "fmt" "sync" ) ​ var wg sync.WaitGroup ​ func test1() { for i := 0; i < 10; i { fmt.Println("你好 Golang ", i) } wg.Done() } ​ func test2() { for i := 0; i < 10; i { fmt.Println("你好 Java ", i) } wg.Done() } func main() { wg.Add(1) go test1() wg.Add(1) go test2() for i := 0; i < 10; i { fmt.Println("你好 Python ", i) } wg.Wait() fmt.Println("主线程结束") }</code></pre>

<h3>for循环开启多个协程</h3>

类似于Java里面开启多个线程,同时执行,因为我们协程会在主线程退出后就终止,所以我们还需要使用到 sync.WaitGroup来控制主线程的终止。

<pre><code class="language-html">func test(num int) { for i := 0; i < 10; i { fmt.Printf("协程(%v)打印的第%v条数据 \n", num, i) } // 协程计数器减1 vg.Done() } ​ var vg sync.WaitGroup ​ func main() { for i := 0; i < 10; i { go test(i) vg.Add(1) } vg.Wait() fmt.Println("主线程退出") }</code></pre>

<h2>设置Go并行运行时占用的cpu数量</h2>

 

Go 运行时的调度器使用 GOMAXPROCS 参数来确定使用多少个 OS线程来同时执行Go代码,默认值是机器上的 CPU核心数。例如在一个8核的机器上,调度器会把Go代码同时调度到 8个 OS线程上。

Go语言中可以通过 runtime.GOMAXPROCS() 函数设置当前程序并发时占用的CPU逻辑核心数

Go1.5版本之前默认使用的都是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数


<h2>Golang 管道 Channel</h2> <h3>channel是什么</h3>

管道是Golang 在语言级别上提供的 goroutine 间的通讯方式,我们可以使用channel在多个goroutine之间传递消息。如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个groutine发送特定值到另一个goroutine的通信机制。

Golang 的并发模型是 CSP (Communicating Sequential Process),提倡通过通信共享内存而不是通过共享内存而实现通信

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

channel 类型

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

<pre><code class="language-html">// 声明一个传递整型的管道 var ch1 chan int // 声明一个传递布尔类型的管道 var ch2 chan bool // 声明一个传递int切片的管道 var ch3 chan []int</code></pre>

创建channel

声明管道后,需要使用 make 函数初始化之后才能使用

<pre><code class="language-html">// 创建一个能存储10个int类型的数据管道 ch1 = make(chan int, 10) // 创建一个能存储4个bool类型的数据管道 ch2 = make(chan bool, 4) // 创建一个能存储3个[]int切片类型的管道 ch3 = make(chan []int, 3) </code></pre>

channel操作

管道有发送、接受、关闭三个功能,发送和接受都是使用 <- 符号

实例:

<pre><code class="language-html">package main import "fmt" func main() { var ch1 chan int ch1 = make(chan int, 3) //发送操作 ch1 <- 10 ch1 <- 20 ch1 <- 30 fmt.Printf("地址:%v 容量:%v 长度:%v \n", ch1, cap(ch1), len(ch1)) //接受操作 x1 := <-ch1 x2 := <-ch1 x3 := <-ch1 fmt.Println(x1, x2, x3) //关闭操作 close(ch1) // 管道阻塞(当没有数据的时候取,会出现阻塞,同时当管道满了,继续存也会) <-ch1 // 没有数据取,出现阻塞 ch1 <- 10 ch1 <- 10 ch1 <- 10 ch1 <- 10 // 管道满了,继续存,也出现阻塞 } </code></pre>

<h3>for range从管道循环取值</h3>

当向管道中发送完数据时,我们可以通过close关闭管道,当管道被关闭时,再往管道发送值会引发panic错误,从该管道取值的操作会取完管道中的值,再然后去到的值一直都是对应类型的默认值。

<pre><code class="language-html">package main import "fmt" func main() { ch2 := make(chan int, 10) for i := 0; i < 10; i { ch2 <- i } //关闭管道 注意:使用 for range 遍历的时候一定在之前需要先关闭管道 // 否则会出现死锁错误 fatal error: all goroutines are asleep - deadlock! close(ch2) for val := range ch2 { fmt.Println(val) } } </code></pre>

通过 for 循环来遍历管道,不需要关闭管道

<pre><code class="language-html"> ch2 := make(chan int, 10) for i := 0; i < 10; i { ch2 <- i } for i := 0; i < 10; i { fmt.Println(<-ch2) } </code></pre>

 


<h2>Goroutine 结合 channel 管道</h2>

需求1:定义两个方法一个方法向管道里面写数据,一个从管道里面读取数据,要求同步执行。

<ul><li>

开启一个wirter协程向管道写入 100条 数据

</li><li>

开启一个reader协程从管道读取 100条 数据

</li><li>

注意:writer 和 reader操作同一条管道

</li><li>

主线程必须等待所有协程操作完毕后才可以退出

</li></ul><pre><code class="language-html">package main import ( "fmt" "sync" ) var wg sync.WaitGroup func writer(ch chan int) { for i := 0; i < 100; i { ch <- i } wg.Done() } func reader(ch chan int) { for i := 0; i < 100; i { fmt.Println(<-ch) } wg.Done() } func main() { ch2 := make(chan int, 100) wg.Add(1) writer(ch2) wg.Add(1) reader(ch2) wg.Wait() fmt.Println("主线程执行完毕") } </code></pre>

需求2:打印素数 (素数:除了1或者自身不能被其他任何数整除的数)

<pre><code class="language-html">// 想intChan中放入 1~ 120000个数 func putNum(intChan chan int) { for i := 2; i < 120000; i { intChan <- i } wg.Done() close(intChan) } // cong intChan取出数据,并判断是否为素数,如果是的话,就把得到的素数放到primeChan中 func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) { for value := range intChan { var flag = true for i := 2; i <= int(math.Sqrt(float64(value))); i { if i % i == 0 { flag = false break } } if flag { // 是素数 primeChan <- value break } } // 这里需要关闭 primeChan,因为后面需要遍历输出 primeChan exitChan <- true wg.Done() } // 打印素数 func printPrime(primeChan chan int) { for value := range primeChan { fmt.Println(value) } wg.Done() } var wg sync.WaitGroup func main() { // 写入数字 intChan := make(chan int, 1000) // 存放素数 primeChan := make(chan int, 1000) // 存放 primeChan退出状态 exitChan := make(chan bool, 16) // 开启写值的协程 go putNum(intChan) // 开启计算素数的协程 for i := 0; i < 10; i { wg.Add(1) go primeNum(intChan, primeChan, exitChan) } // 开启打印的协程 wg.Add(1) go printPrime(primeChan) // 匿名自运行函数 wg.Add(1) go func() { for i := 0; i < 16; i { // 如果exitChan 没有完成16次遍历,将会等待 <- exitChan } // 关闭primeChan close(primeChan) wg.Done() }() wg.Wait() fmt.Println("主线程执行完毕") } </code></pre>


<h3>单向管道</h3>

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

默认的管道是可读可写的

<pre><code class="language-html">// 定义一种可读可写的管道 var ch = make(chan int, 2) ch <- 10 <- ch // 管道声明为只写管道,只能够写入,不能读 var ch2 = make(chan<- int, 2) ch2 <- 10 // 声明一个只读管道 var ch3 = make(<-chan int, 2) <- ch3 </code></pre>

<h3>Select多路复用</h3>

在某些场景下我们需要同时从多个通道接受数据,这个时候就可以用到 golang 中给我们提供的 select 多路复用。通常情况通道在接受数据时,如果没有数据可以接受将会发生阻塞。这种方式虽然可以实现从多个管道接受值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键字,可以同时想要多个管道的操作

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

示例:

<pre><code class="language-html">intChan := make(chan int, 10) intChan <- 10 intChan <- 12 intChan <- 13 stringChan := make(chan int, 10) stringChan <- 20 stringChan <- 23 stringChan <- 24 // 每次循环的时候,会随机中一个chan中读取,其中for是死循环 for { select { case v:= <- intChan: fmt.Println("从initChan中读取数据:", v) case v:= <- stringChan: fmt.Println("从stringChan中读取数据:", v) default: fmt.Println("所有的数据获取完毕") return } } </code></pre>

<blockquote>

tip:使用select来获取数据的时候,不需要关闭chan,不然会出现问题

</blockquote> <h3>Goroutine Recover解决协程中出现的Panic</h3> <pre><code class="language-html">func sayHello() { for i := 0; i < 10; i { fmt.Println("hello") } } func errTest() { // 捕获异常 defer func() { if err := recover(); err != nil { fmt.Println("errTest发生错误") } }() var myMap map[int]string myMap[0] = "10" } func main { go sayHello() go errTest() } </code></pre>

<blockquote>

当我们出现问题的时候,我们还是按照原来的方法,通过defer func创建匿名自启动

</blockquote> <pre><code class="language-html">// 捕获异常 defer func() { if err := recover(); err != nil { fmt.Println("errTest发生错误") } }() </code></pre>

<h3>Go中的并发安全和锁</h3>

如下面一段代码,我们在并发环境下进行操作,就会出现并发访问的问题

<pre><code class="language-html">var count = 0 var wg sync.WaitGroup func test() { count fmt.Println("the count is : ", count) time.Sleep(time.Millisecond) wg.Done() } func main() { for i := 0; i < 20; i { wg.Add(1) go test() } time.Sleep(time.Second * 10) } </code></pre>

<h3>互斥锁</h3>

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

<blockquote> <pre><code class="language-html">// 定义一个锁 var mutex sync.Mutex // 加锁 mutex.Lock() // 解锁 mutex.Unlock() </code></pre> </blockquote> <pre><code class="language-html">var count = 0 var wg sync.WaitGroup var mutex sync.Mutex func test() { // 加锁 mutex.Lock() count fmt.Println("the count is : ", count) time.Sleep(time.Millisecond) wg.Done() // 解锁 mutex.Unlock() } func main() { for i := 0; i < 20; i { wg.Add(1) go test() } time.Sleep(time.Second * 10) } </code></pre>

通过下面命令,build的时候,可以查看是否具有竞争关系

<blockquote> <pre><code class="language-html">// 通过 -race 参数进行构建 go build -race main.go // 运行插件 main.ex </code></pre> </blockquote> <h3>读写互斥锁</h3>

互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。

其实,当我们对一个不会变化的数据只做“读”操作的话,是不存在资源竞争的问题的。因为数据是不变的,不管怎么读取,多少goroutine同时读取,都是可以的。

所以问题不是出在“读”上,主要是修改,也就是“写”。修改的数据要同步,这样其他goroutine才可以感知到。所以真正的互斥应该是读取和修改、修改和修改之间,读和读是没有互斥操作的必要的。

因此,衍生出另外一种锁,叫做读写锁。

读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作。

GO中的读写锁由结构体类型sync.RWMutex表示。此类型的方法集合中包含两对方法:

到此这篇关于“2020-10-19Go语言goroutine和channel”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
2020-10-19Go语言goroutine和channel
22Go常见的并发模式和并发模型
golang 切片截取 内存泄露_怎么看待Goroutine 泄露
go语言并发编程
go channel 缓冲区最大限制_Golang 入门 : channel(通道)
Go并发编程——channel
Go 语言为什么这么快,带你详细了解Golang CSP并发模型
golang goroutine 通知_深入golang之---goroutine并发控制与通信
【文末有惊喜!】一文读懂golang channel
Go语言中Channel机制

[关闭]
~ ~