我的编程空间,编程开发者的网络收藏夹
学习永远不晚

一文带你了解Golang中的并发性

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

一文带你了解Golang中的并发性

并发是一个很酷的话题,一旦你掌握了它,就会成为一笔巨大的财富。说实话,我一开始很害怕写这篇文章,因为我自己直到最近才对并发性不太适应。我已经掌握了基础知识,所以我想帮助其他初学者学习Go的并发性。这是众多并发性教程中的第一篇,请继续关注更多的教程。

什么是并发性,为什么它很重要

并发是指在同一时间运行多个事物的能力。你的电脑有一个CPU。一个CPU有几个线程。每个线程通常一次运行一个程序。当我们通常写代码时,这些代码是按顺序运行的,也就是说,每项工作都是背对背运行的。在并发代码中,这些工作是由线程同时运行的。

一个很好的比喻是对一个家庭厨师的比喻。我还记得我第一次尝试煮意大利面的时候。我按照菜谱一步步地做。我切了蔬菜,做了酱汁,然后煮了意大利面条,再把两者混合起来。在这里,每一步都是按顺序进行的,所以下一项工作必须等到当前工作完成后才能进行。

快进到现在,我在烹饪意大利面条方面变得更有经验。我现在先开始做意大利面,然后在这期间进行酱汁的制作。烹饪时间几乎减少到一半,因为烹饪意大利面条和酱汁是同时进行的。

并发性与平行性

并发性与并行性有些不同。并行性与并发性类似,即同时发生多项工作。然而,在并行性中,多个线程分别在进行不同的工作,而在并发性中,一个线程在不同的工作之间游走。

因此,并发性和并行性是两个不同的概念。一个程序既可以并发地运行,也可以并行地运行。你的代码可以按顺序写,也可以按并发写。该代码可以在单核机器或多核机器上运行。把并发性看作是你的代码的一个特征,而把并行性看作是执行的一个特征。

Goroutines, the worker Mortys

Go使编写并发代码变得非常简单。每个并发的工作都由一个goroutine来表示。你可以通过在函数调用前使用go关键字来启动一个goroutine。看过《瑞克和莫蒂》吗?想象一下,你的主函数是一个Rick,他把任务委托给goroutine Mortys。

让我们从一个连续的代码开始。

package main

import (
    "fmt"
    "time"
)

func main() {
    simple()
}

func simple() {
    fmt.Println(time.Now(), "0")
    time.Sleep(time.Second)

    fmt.Println(time.Now(), "1")
    time.Sleep(time.Second)

    fmt.Println(time.Now(), "2")
    time.Sleep(time.Second)

    fmt.Println("done")
}

2022-08-14 16:22:46.782569233 +0900 KST m=+0.000033220 0
2022-08-14 16:22:47.782728963 +0900 KST m=+1.000193014 1
2022-08-14 16:22:48.782996361 +0900 KST m=+2.000460404 2
done

上面的代码打印出当前时间和一个字符串。每条打印语句的运行时间为一秒。总的来说,这段代码大约需要三秒钟的时间来完成。

现在让我们把它与一个并发的代码进行比较。

func main() {
    simpleConc()
}

func simpleConc() {
    for i := 0; i < 3; i++ {
        go func(index int) {
            fmt.Println(time.Now(), index)
        }(i)
    }

    time.Sleep(time.Second)
    fmt.Println("done")
}

2022-08-14 16:25:14.379416226 +0900 KST m=+0.000049175 2
2022-08-14 16:25:14.379446063 +0900 KST m=+0.000079012 0
2022-08-14 16:25:14.379450313 +0900 KST m=+0.000083272 1
done

上面的代码启动了三个goroutines,分别打印当前时间和i。这段代码花了大约一秒钟完成。这比顺序版本快了三倍左右。

"等一下,"我听到你问。"为什么要等整整一秒?难道我们不能删除这一行以使程序尽可能快地运行吗?"好问题!让我们看看会发生什么。

func main() {
    simpleConcFail()
}

func simpleConcFail() {
    for i := 0; i < 3; i++ {
        go func(index int) {
            fmt.Println(time.Now(), index)
        }(i)
    }

    fmt.Println("done")
}

done

嗯......。程序确实在没有任何慌乱的情况下退出了,但我们缺少来自goroutines的输出。为什么它们被跳过?

这是因为在默认情况下,Go并不等待goroutine的完成。你知道main也是在goroutine里面运行的吗?主程序通过调用simpleConcFail来启动工作程序,但它在工作程序完成工作之前就退出了。

让我们回到烹饪的比喻上。想象一下,你有三个厨师,他们分别负责烹饪酱料、意大利面和肉丸。现在,想象一下,如果戈登-拉姆齐命令厨师们做一盘意大利面条和肉丸子。这三位厨师将努力工作,烹制酱汁、意大利面条和肉丸。但是,在厨师们还没有完成的时候,戈登就按了铃,命令服务员上菜。很明显,食物还没有准备好,顾客只能得到一个空盘子。

这就是为什么我们在退出节目前等待一秒钟。我们并不总是确定每项工作都会在一秒钟内完成。有一个更好的方法来等待工作的完成,但我们首先需要学习另一个概念。

总结一下,我们学到了这些东西:

  • 工作被委托给goroutines。
  • 使用并发性可以提高你的性能。
  • 主goroutine默认不等待工作goroutine完成。
  • 我们需要一种方法来等待每个goroutine完成。

Channels, the green portal

goroutines之间是如何交流的?当然是通过通道。通道的作用类似于门户。你可以通过通道发送和接收数据。下面是你如何在Go中制作一个通道。

ch := make(chan int)

每个通道都是强类型的,并且只允许该类型的数据通过。让我们看看我们如何使用这个。

func main() {
    unbufferedCh()
}

func unbufferedCh() {
    ch := make(chan int)

    go func() {
        ch <- 1
    }()

    res := <-ch
    fmt.Println(res)
}

1

很简单,对吗?我们做了一个名为ch的通道。我们有一个goroutine,向ch发送1,我们接收该数据并将其保存到res。

你问,为什么我们在这里需要一个goroutine?因为不这样做会导致死锁。

func main() {
    unbufferedChFail()
}

func unbufferedChFail() {
    ch := make(chan int)
    ch <- 1
    res := <-ch
    fmt.Println(res)
}

fatal error: all goroutines are asleep - deadlock!

我们碰到了一个新词。什么是死锁?死锁就是你的程序被卡住了。为什么上面的代码会卡在死锁中?

为了理解这一点,我们需要知道通道的一个重要特性。我们创建了一个无缓冲的通道,这意味着在某一特定时间内没有任何东西可以被存储在其中。这意味着发送方和接收方都必须同时准备好,才能在通道上传输数据。

在失败的例子中,发送和接收的动作依次发生。我们发送1到ch,但在那个时候没有人接收数据。接收发生在稍后的一行,这意味着在接收行运行之前,1不能被发送。可悲的是,1不能先被发送,因为ch是没有缓冲的,没有空间来容纳任何数据。

在这个工作例子中,发送和接收的动作同时发生。主函数启动了goroutine,并试图从ch中接收,此时goroutine正在向ch发送1。

另一种从通道接收而不发生死锁的方法是先关闭通道。

func main() {
    unbufferedCh()
}

func unbufferedCh() {
    ch2 := make(chan int)
    close(ch2)
    res2 := <-ch2
    fmt.Println(res2)
}

0

关闭通道意味着不能再向它发送数据。我们仍然可以从该通道中接收它。对于未缓冲的通道,从一个关闭的通道接收将返回一个通道类型的零值。

总结一下,我们学到了这些东西:

  • 通道是goroutines之间相互交流的方式。
  • 你可以通过通道发送和接收数据。
  • 通道是强类型的。
  • 没有缓冲的通道没有空间来存储数据,所以发送和接收必须同时进行。否则,你的代码就会陷入死锁。
  • 一个封闭的通道将不接受任何数据。
  • 从一个封闭的非缓冲通道接收数据将返回一个零值。

如果通道能保持数据一段时间,那不是很好吗?这里就是缓冲通道发挥作用的地方。

Buffered channels, the portal that is somehow cylindrical?

缓冲通道是带有缓冲器的通道。数据可以存储在其中,所以发送和接收不需要同时进行。

func main() {
    bufferedCh()
}

func bufferedCh() {
    ch := make(chan int, 1)
    ch <- 1
    res := <-ch
    fmt.Println(res)
}

1

在这里,1被储存在ch里面,直到我们收到它。

很明显,我们不能向一个满了缓冲区的通道发送更多的信息。你需要在缓冲区内有空间才能发送更多。

func main() {
    bufferedChFail()
}

func bufferedChFail() {
    ch := make(chan int, 1)
    ch <- 1
    ch <- 2
    res := <-ch
    fmt.Println(res)
}

fatal error: all goroutines are asleep - deadlock!

你也不能从一个空的缓冲通道接收。

func main() {
    bufferedChFail2()
}

func bufferedChFail2() {
    ch := make(chan int, 1)
    ch <- 1
    res := <-ch
    res2 := <-ch
    fmt.Println(res, res2)
}

fatal error: all goroutines are asleep - deadlock!

如果一个通道已满,发送操作将等待,直到有可用的空间。这在这段代码中得到了证明。

func main() {
    bufferedCh2()
}

func bufferedCh2() {
    ch := make(chan int, 1)
    ch <- 1
    go func() {
        ch <- 2
    }()
    res := <-ch
    fmt.Println(res)
}

1

我们接收一次是为了取出1,这样goroutine就可以发送2到通道。我们没有从ch接收两次,所以只接收1。

我们也可以从封闭的缓冲通道接收。在这种情况下,我们可以在封闭的通道上设置范围来迭代里面的剩余项目。

func main() {
    bufferedChRange()
}

func bufferedChRange() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3
    close(ch)
    for res := range ch {
        fmt.Println(res)
    }
    // you could also do this
    // fmt.Println(<-ch)
    // fmt.Println(<-ch)
    // fmt.Println(<-ch)
}

1
2
3

在一个开放的通道上测距将永远不会停止。这意味着在某些时候,通道将是空的,测距循环将试图从一个空的通道接收,从而导致死锁。

总结一下:

  • 缓冲通道是有空间容纳项目的通道。
  • 发送和接收不一定要同时进行,与非缓冲通道不同。
  • 向一个满的通道发送和从一个空的通道接收将导致一个死锁。
  • 你可以在一个封闭的通道上进行迭代,以接收缓冲区内的剩余值。

等待戈多...我的意思是,goroutines来完成,使用通道

通道可以用来同步goroutines。还记得我告诉过你,在通过无缓冲通道传输数据之前,发送方和接收方必须都准备好了吗?这意味着接收方将等待,直到发送方准备好。我们可以说,接收是阻断的,意思是接收方将阻断其他代码的运行,直到它收到东西。让我们用这个巧妙的技巧来同步我们的goroutines。

func main() {
    basicSyncing()
}

func basicSyncing() {
    done := make(chan struct{})

    go func() {
        for i := 0; i < 5; i++ {
            fmt.Printf("%s worker %d start\n", fmt.Sprint(time.Now()), i)
            time.Sleep(time.Duration(rand.Intn(5)) * time.Second)
        }
        close(done)
    }()

    <-done
    fmt.Println("exiting...")
}

我们做了一个done通道,负责阻断代码,直到goroutine完成。done可以是任何类型,但struct{}经常被用于这些类型的通道。它的目的不是为了传输结构,所以它的类型并不重要。

一旦工作完成,worker goroutine 将关闭 done。此时,我们可以从 done 中接收,它将是一个空结构。接收动作解除了代码的阻塞,使其可以退出。

这就是我们使用通道等待goroutine完成的方式。

总结

并发可能看起来是一个令人生畏的话题。我当然认为是这样的。然而,在了解了基础知识之后,我认为实现起来真的很美。希望你们能从这个教程中有所收获我们仅仅是触及了表面,Go为我们提供的东西还有很多。下一次我们将在更多的并发性教程中见面。再见!

到此这篇关于一文带你了解Golang中的并发性的文章就介绍到这了,更多相关Golang并发性内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

一文带你了解Golang中的并发性

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

一文带你了解Golang中的并发性

并发是一个很酷的话题,一旦你掌握了它,就会成为一笔巨大的财富。所以本文就来和大家一起来聊聊Golang中的并发性,感兴趣的可以了解一下
2023-03-15

一文带你了解Golang中的WaitGroups

WaitGroups是同步你的goroutines的一种有效方式。这篇文章主要来和大家聊聊Golang中WaitGroups的使用,感兴趣的小伙伴可以跟随小编一起了解一下
2023-03-14

一文带你了解Golang中的缓冲区Buffer

作为一种常见的数据结构,缓冲区(Buffer)在计算机科学中有着广泛的应用。这篇文章将详细介绍 Go 中 Buffer 的用法,从多个方面介绍其特性和应用场景,需要的可以参考一下
2023-05-18

一文带你了解Golang中interface的设计与实现

本文就来详细说说为什么说 接口本质是一种自定义类型,以及这种自定义类型是如何构建起 go 的 interface 系统的,感兴趣的小伙伴可以跟随小编一起学习一下
2023-01-04

一文带你了解MySQL中触发器的操作

目录概述介绍触发器的特性操作—创建触发器操作—new和old操作—查看触发器操作—删除触发器注意事项概述介绍触发器,就是一种特殊的存储过程。触发器和存储过程一样是一个能够完成特定功能、存储
2023-02-17

一文带你了解Go语言实现的并发神库conc

前几天逛github发现了一个有趣的并发库-conc,这篇文章将为大家详细介绍一下这个库的实现,文中的示例代码讲解详细,感兴趣的可以了解一下
2023-01-31

一文带你了解Golang中reflect反射的常见错误

go 反射的错误大多数都来自于调用了一个不适合当前类型的方法, 而且,这些错误通常是在运行时才会暴露出来,而不是在编译时,如果我们传递的类型在反射代码中没有被覆盖到那么很容易就会 panic。本文就介绍一下使用 go 反射时很大概率会出现的错误,需要的可以参考一下
2023-01-05

一文带你了解Vue灰度发布

Vue灰度发布是指在发布新版本的时候,只将新版本的一部分用户引流到新版本上,以便测试新版本的稳定性和功能是否正常。灰度发布是一种渐进式的发布策略,可以有效减少发布新版本带来的风险。在Vue中进行灰度发布可以通过以下步骤实现:1. 创建新版本
2023-09-22

一文带你了解MySQL中的事务

目录一.什么是事务二.事务操作演示小结三.事务的特性四.事务的隔离级别概述四种隔离级别脏读、不可重复读、幻读操作一.什么是事务在mysql中的事务(Transaction)是由存储引擎实现的,在MySQL中,只有InnoDB存储引擎才支持
2023-02-17

一文带你深入理解Golang中的RWMutex

这篇文章主要为大家详细介绍了Golang中RWMutex的相关知识,知其然,更要知其所以然。文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下
2023-05-14

一文带你了解promise并解决回调地狱

这篇文章主要介绍了Promise解决回调地狱问题,文中有详细的代码示例,具有一定的参考价值,需要的朋友可以阅读参考
2023-05-15

一文带你了解Golang中类型转换库cast的使用

你是否在使用Go的过程中因为类型转换的繁琐而苦恼过?你是否觉得Go语言中的类型断言可能会panic而对自己写的代码有那么一点点不放心?本文就为大家推荐一个用于类型转换的第三方库cast绝对是一个值得尝试的选择
2023-02-08

一文带你了解Python中的枚举(enum)

这篇文章一文带你了解Python中的枚举(enum),在Python中,枚举和我们在对象中定义的类变量时一样的,每一个类变量就是一个枚举项,需要的朋友可以参考下
2023-05-15

一文带你了解Node.js中的path模块

Node.js和Python技术类似, 都致力于能够实现跨平台的通用代码。 为此,针对路径的拼接, Node.js提供了path模块,本文就来讲讲path模块的使用
2023-03-21

一文带你了解C++中queue的使用

C++中的queue是一种容器,用于在FIFO(先进先出)原则下存储和管理元素。本篇文章将深入探讨C++中的queue,包括它的定义、使用、原理和示例,感兴趣的可以了解一下
2023-05-18

一文带你了解C++中deque的使用

C++中的deque是一种双端队列,可以在队列的前端和后端进行插入元素和删除操作,同时可以视作一个长度不定的数组,支持高效的插入和删除操作。本篇文章将深入探讨C++中的deque的使用,感兴趣的可以了解一下
2023-05-18

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录