• GO协程理解和应用场景


    1、概述        

    最近在倒腾GO语言,用来做了一段时间研发后,发现一些特点,在此记录一下。

            首先学习了下他的语言语法,发现规则和其他语言规则有点类似,函数是通过大括号来进行规范,条件语句也是通过大括号在规范,然后就是else语句必须放在if的结束大括号后面,否则会报错;语法简单,不需要像C/C++语言那样需要分号来结束每条语句,直接换行即可,也不需要像python语言那样需要强要求的换行来标识语句和函数;最后就是协程,协程可以算是go语言的最大的特点,也是go语言诞生的初衷。

            很多文章有这么一句话叫做:一核有难多核围观,意思是针对多核CPU一个核忙得要死要活,剩下的核确实闲置的。出现上面的原因就是写的程序是单核处理器,不是针对多核CPU的高并发程序。在这里也记录下并发和并行的区别,借用看到资料的解释,比较形象,并发就是使用同一个锅炒不同的菜,菜品在锅中随时切换;并行就是有多个锅,每个锅同时炒不同的菜。写完这个比喻我突然想到了另一更加形象的比喻:并发就是你拿着一把刀切菜,一会切白菜,一会切萝卜,一会切茄子,你的这把刀就是CPU一个核,然后并发的去切很多菜;并行就是你用多把刀同时切不同的菜,哈哈,感觉更形象了,是不是?

            (补充一下,可能有点乱:并发不是并行,并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。)

    2、协程实现和协程交互       

    协程我们在代码里面实现直接在函数调用前面直接加go就可以实现协程调用,如下所示:

    1. func testfun(){
    2. //do something
    3. }
    4. go testfun()

    这种用法还是相当方便的,只需要加一个关键字就可以实现协程功能。但是这种一般都是独立的协程,如果需要协程之间相互通信,go语言也提供多种方法,第一种就是跟C/C++一样的加锁,方法如下:

    1. lock.Lock()
    2. testname = "newname" //testname为全局变量,多线程操作的时候需要锁住变量
    3. //解锁
    4. lock.Unlock()

    但是go语言虽然支持这种锁的方式进行线程之间的通信,但是go一般不用这种方式,go一般都用通道(channel)的形式来进行协程之间数据交互。下面将简述使用通道channel来实现协程之间的交互。

    3、创建、使用 channel

    channel 是一个通道、队列,那么我们关心的应该就是如何创建这个通道、将数据装到通道中、从通道中提取数据。 golang 为它设计了一个操作符:
    left <- right,当 left 为 channel 时,表示向通道中写入数据(发送),并且如果通道存在数据,写入会被阻塞,所以可以建立一个大小为n的通道,当写入数量大于n时,通道才会堵塞;当 right 为通道时,表示从通道提取数据(接收)。

    1. package main
    2. import "fmt"
    3. func main() {
    4. simpleChan()
    5. }
    6. func simpleChan() {
    7. // 声明一 chan 类型的字符串的变量
    8. var ch chan string
    9. ch = make(chan string)
    10. // 向 channel 中发送 string 类型数据
    11. go func() { //前面括号里传形式参数
    12. ch <- "ping"
    13. }() //前面括号里传实参参数
    14. // 创建一个 string 类型的变量,用来保存从 channel 队列提取的数据
    15. var v string
    16. v = <-ch
    17. fmt.Println(v)
    18. }

            上面的例子中创建了一个通道ch,然后并make了一块内存,这个通道实现了一个类似队列的功能,有数据写入,里面如果没有被读走,就排队等候。上面代码里面的两个操作语句就可以完成了数据入队列(ch <- "ping"),数据出队列(v = <-ch)的动作。这里有个问题需要注意,channel 的接收与发送需要分别在两个 goroutine 中,如果你是直接看英文的文档、或者其他介绍的文章,可能没有指出这个要求。它是跨协程的。如果ch <- "ping"不用协程调用,跑起来会报错。

            从上面的例子可以看到协程通过通道ch来实现数据传递,这个通道ch就类似枷锁操作的变量,通道也是一种数据结构,跟使用枷锁方式操作变量一样,通道定义的时候也需要定下来通道的类型,定义好后不能修改。

    4、数据协程、通道channel使用实例

    这个例子是我在搜索资料看到的,感觉还行,放在记录一下,例子中主要展示的例子原意是有2个干活的worker,然后有五个工作job,需要这两个人来完成这五个工作,功能实现里面还定义了五个工作完成的结果。如下所示:

    1. // channel.go
    2. package main
    3. import (
    4. "fmt"
    5. "time"
    6. )
    7. func main() {
    8. workpools()
    9. }
    10. func workpools() {
    11. const number_of_jobs = 5
    12. const number_of_workers = 2
    13. jobs := make(chan int, number_of_jobs)
    14. results := make(chan string, number_of_jobs)
    15. // 向 任务队列写入任务
    16. for i := 1; i <= number_of_jobs; i++ {
    17. jobs <- i
    18. }
    19. fmt.Println("布置 job 后,关闭 jobs channel")
    20. close(jobs)
    21. // 控制并行度,每个 worker 函数都运行在单独的 goroutine 中
    22. for w := 1; w <= number_of_workers; w++ {
    23. go worker(w, jobs, results)
    24. }
    25. // 监听 results channel,只要有内容就会被取走
    26. for i := 1; i <= number_of_jobs; i++ {
    27. fmt.Printf("结果: %s\n", <-results)
    28. }
    29. }
    30. // worker 逻辑:一个不断从 jobs chan 中取任务的循环
    31. // 并将结果放在 out channel 中待取
    32. func worker(id int, jobs <-chan int, out chan<- string) {
    33. fmt.Printf("worker #%d 启动\n", id)
    34. for job := range jobs {
    35. fmt.Printf("worker #%d 开始 工作%d\n", id, job)
    36. // sleep 模拟 『正在处理任务』
    37. time.Sleep(time.Millisecond * 500)
    38. fmt.Printf("worker #%d 结束 工作%d\n", id, job)
    39. out <- fmt.Sprintf("worker #%d 工作%d", id, job)
    40. }
    41. fmt.Printf("worker #%d 退出\n", id)
    42. }

    从例子中可以看到,逻辑是首先jobs是缓冲区为5的通道,所以先给工作通道布置了五个工作分别是工作1,2,3,4,5,然后就使用两个worker工作者1,2协程来完成这五个工作。运行代码可以看到如下打印信息:

    1. 布置 job 后,关闭 jobs channel
    2. worker #2 启动
    3. worker #2 开始 工作1
    4. worker #1 启动
    5. worker #1 开始 工作2
    6. worker #1 结束 工作2
    7. worker #2 结束 工作1
    8. worker #2 开始 工作4
    9. 结果: worker #1 工作2
    10. 结果: worker #2 工作1
    11. worker #1 开始 工作3
    12. worker #2 结束 工作4
    13. worker #2 开始 工作5
    14. worker #1 结束 工作3
    15. worker #1 退出
    16. 结果: worker #2 工作4
    17. 结果: worker #1 工作3
    18. worker #2 结束 工作5
    19. worker #2 退出
    20. 结果: worker #2 工作5

    从上面的打印信息可以看到,两个工作者worker1,2,同时完成布置的jobs,里面的这句语句有点魔性,

    for job := range jobs

    在协程里面是都会使用这句语句的,根据资料说的是这个range是不管jobs里面的个数,只是从jobs里面去取出一个数据,如果里面没有了,循环就会结束。今天先写到这,后面再补充。

  • 相关阅读:
    6.30模拟赛总结
    redis命令记录
    力扣算法入门刷题2
    【解读】区块链和分布式记账技术标准体系建设指南
    如何使用西门子存储卡清除博途S7-1200的密码
    自学嵌入式,已经会用stm32做各种小东西了
    XML的使用
    R语言贝叶斯广义线性混合(多层次/水平/嵌套)模型GLMM、逻辑回归分析教育留级影响因素数据...
    Flutter 2.10 现已发布
    反向代理软件frp和混合云组网软件openvpn
  • 原文地址:https://blog.csdn.net/u013896064/article/details/127384922