【启程Golang之旅】协程和管道操作

2024-06-09 05:52

本文主要是介绍【启程Golang之旅】协程和管道操作,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

欢迎来到Golang的世界!在当今快节奏的软件开发领域,选择一种高效、简洁的编程语言至关重要。而在这方面,Golang(又称Go)无疑是一个备受瞩目的选择。在本文中,带领您探索Golang的世界,一步步地了解这门语言的基础知识和实用技巧。

目录

初识协程

启动多协程

初识锁

初识管道

协程与管道协同


初识协程

在go语言中,"协程"(coroutine)通常指的是go的轻量级线程,也被称为goroutine,它使得并发编程变得更加简单和高效。这里我先对go语言中其他概念进行一个简单叙述:

1)程序(program)

是为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码(程序是静态的)

2)进程(process)

是程序的一次执行过程,正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。(进程是动态的)是一个动的过程,进程的生命周期:有它自身的产生、存在和消亡的过程

3)线程(thread)

进程可进一步细化为线程,是一个程序内部的一条执行路径,若一个进程同一时间并行执行多个线程,就是支持多线程的。

4)协程(goroutine)

又称为微线程,纤程,协程是一种用户态的轻量级线程

与传统的线程相比,goroutine非常轻量,它们的创建和销毁成本很低,因此可以大量创建而不会对系统造成太大的负担,因此它是go实现高效并发编程的核心机制之一。

协程的作用:在执行A函数的时候,可以随时中断,去执行B函数,然后中断继续执行A函数(可以自动切换),注意这一切换过程并不是函数调用(没有调用语句),过程很像多线程,然而协程中只有一个线程在执行 (协程的本质是个单线程),如下图所示:

        对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就将寄存器上下文和栈保存到某个其他地方,然后切换到另外一个任务去计算。在任务切回来的时候,恢复先前保存的寄存器上下文和栈,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态。

        相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而会更多的将cpu的执行权限分配给我们的线程(注意:线程是CPU控制的,而协程是程序自身控制的,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级) 

我们老说协程协程,那么协程到底如何实现呢?这里给出下面的案例进行讲解:

请编写一个程序,完成如下功能:

1)在主线程中,开启一个goroutine,该goroutine每隔1秒输出"hello golang"

2)在主线程中也每隔一秒输出"hello world",输出10次后,退出程序

3)要求主线程和goroutine同时执行

package main
import ("fmt""strconv""time"
)
func test() {for i := 0; i < 10; i++ {fmt.Println("hello golang + " + strconv.Itoa(i))// 阻塞1秒time.Sleep(1 * time.Second)}
}func main() { // 主线程go test() // 开启协程for i := 0; i < 10; i++ {fmt.Println("hello world + " + strconv.Itoa(i))// 阻塞1秒time.Sleep(1 * time.Second)}
}

开启协程之后,协程和主线程交替执行,效果如下:

主线程和协程执行流程:可以从下图所示看到:

主死协从: 在go语言中有如下概念,如果主线程退出了,则协程即使还没有执行完毕,也会退出,当然协程也可以在主线程没有退出前,就自己结束了,比如完成了自己的任务,这里我们对上面的代码进行一个简单的修改,让协程的任务量变大,如下:

当我们运行程序之后,即使协程没有执行完毕,其仍要跟随主程序的结束而结束:

启动多协程

相比于线程,协程的创建和销毁成本非常低,这里可以在同一时间启动多个协程,它们会并发执行,go运行时会自动调度协程,以便它们可以在多个操作系统线程上运行,协程之间通常通过通道进行通信,以避免共享状态导致的竞态条件。下面是一个简单的go代码示例:

package main
import ("fmt""time"
)
func main() {// 匿名函数+外部变量 = 闭包for i := 0; i <= 5; i++ {// 启动协程,使用匿名函数直接调用匿名按时go func(n int) {fmt.Println(n)}(i)}time.Sleep(time.Second * 2)
}

最终实现的效果如下所示,由于它们都在并发地运行,所以输出的顺序可能是不确定的,如果你想要控制协程的执行顺序或等待它们完成,你可以使用通道或其他同步机制,这个后面再讲:

当然如果不想使用time.Sleep的方式的话,也可以采用下面的方式进行实现:

package main
import ("fmt""sync"
)
var wg sync.WaitGroup // 只定义无需赋值
func main() {// 启动五个协程for i := 0; i < 5; i++ {wg.Add(1) // 计数器加一go func(n int) {fmt.Println(n)wg.Done() // 协程执行完成减1}(i)}// 主线程一直在阻塞,什么时候wg减为0就停止wg.Wait()
}

如果多个协程操作同一个数据的情况下,给出如下代码进行示例:

package main
import ("sync"
)
// 定义一个变量
var totalNum int
var wg sync.WaitGroup // 只定义无需赋值func add() {defer wg.Done()for i := 0; i < 100000000; i++ {totalNum++}
}
func sub() {defer wg.Done()for i := 0; i < 100000000; i++ {totalNum--}
}func main() {wg.Add(2)// 启动两个协程go add()go sub()// 等待协程结束wg.Wait()println(totalNum)
}

因为协程是交替不确定的执行,结果可能都不一样,如下图所示:

初识锁

从上文的案例可以看出,如果多个协程操作同一个数据, 因为协程会交替并发执行,所以会出现争抢资源的情况,导致最终的结果可能并不是我们想要的,那么我们如何处理这个问题呢?这里我们就需要通过一个机制,确保一个协程在执行逻辑的时候另外的协程不执行,这里我们就需要引入一个概念:“锁的机制”,即加入互斥锁,示例代码如下,最终结果为0,是我们想要的:

package main
import ("sync"
)
// 定义一个变量
var totalNum int
var wg sync.WaitGroup // 只定义无需赋值
// 加入互斥锁
var lock sync.Mutexfunc add() {defer wg.Done()for i := 0; i < 100000000; i++ {lock.Lock() // 加锁totalNum++lock.Unlock() // 解锁}
}
func sub() {defer wg.Done()for i := 0; i < 100000000; i++ {lock.Lock() // 加锁totalNum--lock.Unlock() // 解锁}
}func main() {wg.Add(2)// 启动两个协程go add()go sub()// 等待协程结束wg.Wait()println(totalNum)
}

当然golang中sync包实现了两种锁Mutex(互斥锁)和RWMutex(读写锁),具体如下:

1)互斥锁:其中Mutex为互斥锁,Lock(加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock(解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别,其性能和效率相对较低。

2)读写锁:RWMutex是一个读写锁,其经常用于读次数远远多于写次数的场景,在读的时候数据之间不产生影响,写和读之间才会产生影响。

package main
import ("fmt""sync""time"
)
// 定义一个变量
var totalNum int
var wg sync.WaitGroup // 只定义无需赋值
// 加入读写锁
var lock sync.RWMutexfunc read() {defer wg.Done()lock.RLock() // 读锁,如果只是读数据那么这个锁不产生任何影响,但是读写同时发生时,就会有影响fmt.Println("开始读取数据")time.Sleep(time.Second)fmt.Println("读取数据完毕")lock.RUnlock() // 关锁
}
func write() {defer wg.Done()lock.Lock() // 写锁fmt.Println("开始写入数据")time.Sleep(time.Second)fmt.Println("写入数据完毕")lock.Unlock() // 关锁
}func main() {wg.Add(6)// 启动两个协程,读多写少for i := 0; i < 5; i++ {go read()}go write()// 等待协程结束wg.Wait()
}

初识管道

在go语言中,管道(Channel)是一种特殊的类型,用于在协程(goroutine)之间进行通信,它允许一个协程将数据发送到管道,并由另一个协程从管道中接收数据。这种机制可以实现协程之间的同步和数据交换,以下是管道相关特点介绍:

1)管道本质就是一个数据结构-队列

2)数据是先进先出

3)自身线程安全,多协程访问时,不需要加锁,channel本身就是线程安全的

4)管道有类型的,一个string的管道只能存放string类型数据

管道的定义: var 变量名chan 数据类型

chan是管道关键字;数据类型指的是管道的类型,里面放入数据的类型,管道是有类型的,intChan只能写入整数int;管道是引用类型,必须初始化才能写入数据,即make后才能使用。

package mainimport "fmt"func main() {// 定义一个int类型管道var intChan chan int// 通过make初始化,管道可以存放3个int类型数据intChan = make(chan int, 3)// 证明管道是引用类型fmt.Printf("intChan的值:%v \n", intChan) // intChan的值:0xc000018200// 向管道存放数据,注意存放的数据不能超出管道的容量intChan <- 1 // 往管道中存放数据num := 20intChan <- num // 往管道中存放数据// 取出管道数据(队列先进先出)fmt.Printf("intChan的值:%v \n", <-intChan) // intChan的值:1fmt.Printf("intChan的值:%v \n", <-intChan) // intChan的值:20// 输出管道的长度fmt.Printf("intChan的长度:%d \n", len(intChan)) // intChan的长度:2
}

管道的关闭使用内置函数close可以关闭管道,当管道关闭后,就不能再向管道写数据了,但是仍然可以从该管道读取数据。

package mainimport "fmt"func main() {// 定义管道var intChan chan int// 通过make初始化管道,可以存放3个int类型数据intChan = make(chan int, 3)// 往管道中写入数据intChan <- 1intChan <- 2// 关闭管道close(intChan)// 再次写入数据会报错//intChan <- 3 // panic: send on closed channel// 当管道关闭后,再读取数据是可以的num := <-intChanfmt.Println(num) // 1
}

管道的遍历:管道支持for-range的方式进行遍历,请注意两个细节:

1)在遍历时,如果管道没有关闭,则会出现deadlock的错误

2)在遍历时,如果管道已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

package mainimport "fmt"func main() {// 定义管道var intChan chan int// 通过make初始化管道,可以存放100个int类型数据intChan = make(chan int, 100)// 往管道中写入数据for i := 0; i < 100; i++ {intChan <- i}// 在遍历前,如果没有关闭管道,就会出现deedlock的错误// 遍历管道中的数据for v := range intChan {fmt.Println("value = ", v)}
}

如上代码由于没有关闭管道,导致出现如下问题:

所以我们需要在遍历管道前,需要进行管道的关闭,如下:

管道的只读只写: 可以在初始化管道的时候,通过代码设置只读只写属性,如下:

func main() { // 主线程// 默认情况下,管道是双向的,可读可写var intChan chan int// 声明为只写管道var intChan1 chan<- int// 声明为只读管道var intChan2 <-chan int
}

select功能:解决多个管道的选择问题,也可以叫做多路复用,可以从多个管道中随机公平地选择一个来执行,如下代码输出的就是hello

1)case后面必须进行的是io操作,不能是等值,随机去选择一个io操作

2)default防止select被阻塞住,加入default 

package mainimport ("fmt""time"
)func main() { // 主线程// 定义一个int类型管道intChan := make(chan int, 1)go func() {time.Sleep(time.Second * 5)intChan <- 1}()// 定义一个string类型管道stringChan := make(chan string, 1)go func() {time.Sleep(time.Second * 2)stringChan <- "hello"}()//fmt.Println(<-intChan) // 阻塞等待,本身取数据就是阻塞的select {case v := <-intChan:fmt.Println("intChan: ", v)case v := <-stringChan:fmt.Println("stringChan: ", v)default:fmt.Println("防止select被阻塞")}
}

异常错误捕获:多个协程工作,其中一个协程出现panic,导致程序崩溃,这里利用refer+recover捕获panic进行处理,即使协程出现问题,主线程仍然不受影响可以继续执行,示例代码如下:

package mainimport ("fmt""time"
)// 输出数字
func printNum() {for i := 0; i < 10; i++ {fmt.Println(i)}
}// 除法操作
func devide() {defer func() {err := recover()if err != nil {fmt.Println("程序异常退出")}}()num1 := 10num2 := 0result := num1 / num2fmt.Println(result)
}func main() {// 启动两个协程go printNum()go devide()time.Sleep(time.Second * 5)
}

得到的结果如下所示:

协程与管道协同

接下来我们通过一个案例来实现协程与管道的共同操作,案例需求如下:

请完成协程和管道协同工作的案例,具体要求:

1)开启一个writeData协程,向管道中写入50个整数。

2)开启一个readData协程,从管道中读取writeData写入的数据。

3)注意:writeData和readDate操作的是同一个管道。

4)主线程需要等待writeData和readDate协程都完成工作才能退出

其对应的原理图如下所示:

package mainimport ("fmt""strconv""sync""time"
)var wg sync.WaitGroup// 写数据
func writeData(intChan chan int) {defer wg.Done()for i := 0; i < 50; i++ {intChan <- ifmt.Println("写入的数据为:" + strconv.Itoa(i))time.Sleep(time.Second)}// 关闭通道close(intChan)
}// 读数据
func readData(intChan chan int) {defer wg.Done()for i := range intChan {fmt.Println("读取的数据为:" + strconv.Itoa(i))time.Sleep(time.Second)}
}
func main() { // 主线程// 写协程和读协程共同操作同一个通道intChan := make(chan int, 50)wg.Add(2)// 开启读和写的协程go writeData(intChan)go readData(intChan)// 主线程一直在阻塞,什么时候wg.Done()减为0,主线程才会结束wg.Wait()
}

最终实现的效果如下所示:

这篇关于【启程Golang之旅】协程和管道操作的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1044369

相关文章

动手学深度学习【数据操作+数据预处理】

import osos.makedirs(os.path.join('.', 'data'), exist_ok=True)data_file = os.path.join('.', 'data', 'house_tiny.csv')with open(data_file, 'w') as f:f.write('NumRooms,Alley,Price\n') # 列名f.write('NA

线程的四种操作

所属专栏:Java学习        1. 线程的开启 start和run的区别: run:描述了线程要执行的任务,也可以称为线程的入口 start:调用系统函数,真正的在系统内核中创建线程(创建PCB,加入到链表中),此处的start会根据不同的系统,分别调用不同的api,创建好之后的线程,再单独去执行run(所以说,start的本质是调用系统api,系统的api

Java IO 操作——个人理解

之前一直Java的IO操作一知半解。今天看到一个便文章觉得很有道理( 原文章),记录一下。 首先,理解Java的IO操作到底操作的什么内容,过程又是怎么样子。          数据来源的操作: 来源有文件,网络数据。使用File类和Sockets等。这里操作的是数据本身,1,0结构。    File file = new File("path");   字

MySQL——表操作

目录 一、创建表 二、查看表 2.1 查看表中某成员的数据 2.2 查看整个表中的表成员 2.3 查看创建表时的句柄 三、修改表 alter 3.1 重命名 rename 3.2 新增一列 add 3.3 更改列属性 modify 3.4 更改列名称 change 3.5 删除某列 上一篇博客介绍了库的操作,接下来来看一下表的相关操作。 一、创建表 create

使用协程实现高并发的I/O处理

文章目录 1. 协程简介1.1 什么是协程?1.2 协程的特点1.3 Python 中的协程 2. 协程的基本概念2.1 事件循环2.2 协程函数2.3 Future 对象 3. 使用协程实现高并发的 I/O 处理3.1 网络请求3.2 文件读写 4. 实际应用场景4.1 网络爬虫4.2 文件处理 5. 性能分析5.1 上下文切换开销5.2 I/O 等待时间 6. 最佳实践6.1 使用 as

封装MySQL操作时Where条件语句的组织

在对数据库进行封装的过程中,条件语句应该是相对难以处理的,毕竟条件语句太过于多样性。 条件语句大致分为以下几种: 1、单一条件,比如:where id = 1; 2、多个条件,相互间关系统一。比如:where id > 10 and age > 20 and score < 60; 3、多个条件,相互间关系不统一。比如:where (id > 10 OR age > 20) AND sco

PHP7扩展开发之流操作

前言 啥是流操作?简单来讲就是对一些文件,网络的IO操作。PHP已经把这些IO操作,封装成流操作。这节,我们将使用PHP扩展实现一个目录遍历的功能。PHP示例代码如下: <?phpfunction list_dir($dir) {if (is_dir($dir) === false) {return;} $dh = opendir($dir);if ($dh == false) {ret

Golang进程权限调度包runtime

关于 runtime 包几个方法: Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行GOMAXPROCS:设置最大的可同时使用的 CPU 核数Goexit:退出当前 goroutine(但是defer语句会照常执行)NumGoroutine:返回正在执行和排队的任务总数GOOS:目标操作系统NumCPU:返回当前系统的 CPU 核数量 p

Golang 网络爬虫框架gocolly/colly(五)

gcocolly+goquery可以非常好地抓取HTML页面中的数据,但碰到页面是由Javascript动态生成时,用goquery就显得捉襟见肘了。解决方法有很多种: 一,最笨拙但有效的方法是字符串处理,go语言string底层对应字节数组,复制任何长度的字符串的开销都很低廉,搜索性能比较高; 二,利用正则表达式,要提取的数据往往有明显的特征,所以正则表达式写起来比较简单,不必非常严谨; 三,使

Golang网络爬虫框架gocolly/colly(四)

爬虫靠演技,表演得越像浏览器,抓取数据越容易,这是我多年爬虫经验的感悟。回顾下个人的爬虫经历,共分三个阶段:第一阶段,09年左右开始接触爬虫,那时由于项目需要,要访问各大国际社交网站,Facebook,myspace,filcker,youtube等等,国际上叫得上名字的社交网站都爬过,大部分网站提供restful api,有些功能没有api,就只能用http抓包工具分析协议,自己爬;国内的优酷、