关于学习Go语言的并发编程

开始之前,介绍一下​最近很火的开源技术,低代码。

作为一种软件开发技术逐渐进入了人们的视角里,它利用自身独特的优势占领市场一角——让使用者可以通过可视化的方式,以更少的编码,更快速地构建和交付应用软件,极大程度地降低了软件的开发、配置、部署和培训成本。

应用地址:https://www.jnpfsoft.com 开发语言:Java/.net

这是一个基于 Java Boot/.Net Core 构建的简单、跨平台快速开发框架。前后端封装了上千个常用类,方便扩展;采用微服务、前后端分离架构,集成了代码生成器,支持前后端业务代码生成,满足快速开发;框架集成了表单、报表、图表、大屏等各种常用的 Demo 方便直接使用;后端框架支持 Vue2、Vue3,平台即可私有化部署,也支持 K8S 部署。

关于并发

Go 语言的创始人Rob Pike 曾说过:并行关乎执行,并发关乎结构。他认为:
• 并发是一种程序设计方法:将一个程序分解成多个小片段,每个小片段独立执行;并发程序的小片段之间可以通过通信相互协作。
• 并行是有关执行的,它表示同时进行一些计算任务。

程序小片段之间通讯不同语言实现不同,比如:传统语言Java使用共享内存方式达到线程之间通讯,而Go语言channel来进行通讯。

原生线程、Java线程、Goroutine

Java中的多线程,由 JVM 在 Java 堆中分配内存来存储线程的相关信息,包括线程栈、程序计数器等。当需要执行 Java 线程时,它会向操作系统请求分配一个或多个原生线程(例如 POSIX 线程或 Windows 线程),操作系统分配成功后,JVM 会将 Java 线程与这些原生线程进行映射,并建立关联,并在需要时将 Java 线程的状态同步到相应的原生线程中。

由此可以看出,Java线程和原生线程1:1对应,由操作系统(OS)调度算法执行,该并发以下特点:

  • 线程栈默认空间大且不支持动态伸缩,Java 默认最小都是1MB,Linux 默认 8MB;
  • 线程切换创建、销毁以及线程间上下文切换的代价都较大。
  • 线程通过共享内存进行通讯,

POSIX线程(Pthreads)是C函数、类型和常数的集合,用于创建和管理线程。它是POSIX标准的一个子集,提供在BeagleBone Black上使用C/C++应用程序实现线程所需的一切。

原生线程就是操作系统线程或叫系统线程。

Go语言引入用户层轻量级线程(Goroutine),它由Go运行时负责调度。Goroutine相比传统操作系统线程而言有如下优势。

  • 资源占用小,每个Goroutine的初始栈大小仅为2KB,且支持动态伸缩,避免内存浪费;
  • 由Go运行时而不是操作系统调度,goroutine上下文切换代价较小;
  • 内置channel作为goroutine间通信原语,为并发设计提供强大支撑。

了解Go调度原理

Go 语言实现了调度器(scheduler),它负责将 goroutine 分配到原生线程上执行。

G-P-M模型

Go 语言中的调度模型(G-P-M模型)它包含了三个重要组件:G(goroutine)、P(processor)、M(machine)。

GPM

  • G(goroutine):一个执行单元,这里也就是 goroutine,它包含了执行代码所需的信息,比如栈空间、程序计数器等。
  • P(processor):P 一个逻辑处理器,它负责执行 goroutine。每个 P 维护了一个 goroutine 队列,它可以将 goroutine 分配到 M(系统线程)上执行。P 的数量由 GOMAXPROCS 环境变量决定,默认值为 CPU 的逻辑核心数。
  • M(machine):一个系统线程(machine),它负责执行 goroutine 的真正计算工作。M 与操作系统的线程直接绑定,负责实际的计算任务,比如执行 goroutine 的函数、系统调用等。Go 语言的调度器会将多个 goroutine 映射到少量的系统线程上执行。

抢占式调度

在上面模型中,如果某个G处于死循环或长时间执行(比如:进行系统调用,IO操作),那么P队列里面的G就长时间得不到执行,为了解决此问题,需要使用抢占式调度。

Java 中有以下两种抢占式调度算法

  1. 优先级调度(Priority Scheduling)

    • 每个线程都有一个优先级,高优先级的线程会比低优先级的线程更容易获得CPU的执行权(注意:设置了优先级不是绝对优先执行,只是概率上高)。
    • 在Java中,线程的优先级范围是从Thread.MIN_PRIORITY(1)到Thread.MAX_PRIORITY(10),默认是Thread.NORM_PRIORITY(5)。
  2. 时间片轮转调度(Round Robin Scheduling)

    • 每个线程被分配一个固定的时间片,当该线程的时间片用完时,操作系统会暂停它的执行,将CPU控制权交给下一个线程。
    • 在Java中,时间片轮转调度通过yield()方法来实现。当线程调用yield()时,它就会主动放弃CPU的执行权,让其他线程有机会执行。

Go 语言与Java抢占调度不同,Java是实际上是操作系统时间片轮转调度,发生在内核层。Go 抢占调度是发生在用户层,由 Go 运行时管理,通过软件定时器和抢占点来实现抢占。

Go 程序启动时会创建一个线程(称为监控线程),该线程运行一个内部函数 sysmon ,用来进行系统监控任务,如垃圾回收、抢占调度、监视死锁等。这个函数在后台运行,确保 Go 程序的正常运行。

func main() {...if GOARCH != "wasm" { // 系统栈上的函数执行systemstack(func() {  newm(sysmon, nil, -1) // 用于创建新的 M(机器,代表一个操作系统线程)。})} ...
}

sysmon 每20us~10ms启动一次,大体工作:

  • 释放闲置超过5分钟的span物理内存;
  • 如果超过2分钟没有垃圾回收,强制执行;
  • 将长时间未处理的netpoll结果添加到任务队列;
  • 向长时间运行的G任务发出抢占调度;
  • 收回因syscall长时间阻塞的P。

具体来说,以下情况会触发抢占式调度:

  1. 系统调用:当一个 goroutine 执行系统调用时,调度器会将该 goroutine 暂停,并将处理器分配给其他可运行的 goroutine。一旦系统调用完成,被暂停的 goroutine 可以继续执行。
  2. 函数调用:当一个 goroutine 调用一个阻塞的函数(如通道的发送和接收操作、锁的加锁和解锁操作等)时,调度器会将该 goroutine 暂停,并将处理器分配给其他可运行的 goroutine。一旦被阻塞的函数可以继续执行,被暂停的 goroutine 可以继续执行。
  3. 时间片耗尽:每个 goroutine 在运行一段时间后都会消耗一个时间片。当时间片耗尽时,调度器会将当前正在运行的 goroutine 暂停,并将处理器分配给其他可运行的 goroutine。被暂停的 goroutine 将会被放入到就绪队列中,等待下一次调度。

GO并发模型

Go 使用 CSP(Communicating Sequential Processes,通信顺序进程)并发编程模型,该模型由计算机科学家 Tony Hoare 在 1978 年提出。

在Go中,针对CSP模型提供了三种并发原语:

  • goroutine:对应CSP模型中的P(原意是进程,在这里也就是goroutine),封装了数据的处理逻辑,是Go运行时调度的基本执行单元。
  • channel:对应CSP模型中的输入/输出原语,用于goroutine之间的通信和同步。
  • select:用于应对多路输入/输出,可以让goroutine同时协调处理多个channel操作。

Go 奉行“不要通过共享内存来通信,而应通过通信来共享内存。”,也就是推荐通过channel来传递值,让goroutine相互通讯协作。

channel 分为无缓冲和有缓冲,使用通道时遵循以下规范:

  1. 在无缓冲通道上,每一次发送操作都有对应匹配的接收操作。
  2. 对于从无缓冲通道进行的接收,发生在对该通道进行的发送完成之前。
  3. 对于带缓冲的通道(缓存大小为C),通道中的第K个接收完成操作发生在第K+C个发送操作完成之前。
  4. 如果将C=0就是无缓冲的通道,也就是第K个接收完成在第K个发送完成之前。
func sender(ch chan<- int, done chan<- bool) {fmt.Println("Sending...")ch <- 42 // 发送数据到无缓冲通道fmt.Println("Sent")done <- true // 发送完成信号
}func receiver(ch <-chan int, done <-chan bool) {<-done // 等待发送操作完成信号fmt.Println("Receiving...")val := <-ch // 从无缓冲通道接收数据fmt.Println("Received:", val)
}func main() {ch := make(chan int) // 创建无缓冲通道done := make(chan bool) // 用于发送操作完成信号go sender(ch, done)   // 启动发送goroutinego receiver(ch, done) // 启动接收goroutinetime.Sleep(2 * time.Second) // 等待一段时间以观察结果
}

有缓冲通道

func sender(ch chan<- int) {for i := 0; i < 5; i++ {fmt.Println("Sending:", i)ch <- i // 发送数据到通道fmt.Println("Sent:", i)}close(ch)
}func receiver(ch <-chan int) {for {val, ok := <-ch // 从通道接收数据if !ok {fmt.Println("Channel closed")return}fmt.Println("Received:", val)time.Sleep(1 * time.Second) // 模拟接收操作耗时}
}func main() {ch := make(chan int, 2) // 创建带缓冲大小为2的通道go sender(ch)   // 启动发送goroutinego receiver(ch) // 启动接收goroutinetime.Sleep(10 * time.Second) // 等待一段时间以观察结果
}

Go并发场景

并行计算

利用goroutine并发执行任务,加速计算过程。

// calculateSquare 是一个计算数字平方的函数,它模拟了一个耗时的计算过程。
func calculateSquare(num int, resultChan chan<- int) {time.Sleep(1 * time.Second) // 模拟耗时计算resultChan <- num * num
}func main() {nums := []int{1, 2, 3, 4, 5}resultChan := make(chan int)// 启动多个goroutine并发计算数字的平方for _, num := range nums {go calculateSquare(num, resultChan)}// 从通道中接收计算结果并打印for range nums {result := <-resultChanfmt.Println("Square:", result)}close(resultChan)
}

IO密集型任务

在处理IO密集型任务时,可以使用goroutine和channel实现并发读写操作,提高IO效率。

// fetchURL 函数用于获取指定URL的内容,并将结果发送到通道resultChan中。
func fetchURL(url string, resultChan chan<- string) {resp, err := http.Get(url)if err != nil {resultChan <- fmt.Sprintf("Error fetching %s: %s", url, err)return}defer resp.Body.Close()body, err := ioutil.ReadAll(resp.Body)if err != nil {resultChan <- fmt.Sprintf("Error reading response from %s: %s", url, err)return}resultChan <- string(body)
}func main() {urls := []string{"https://example.com", "https://example.org", "https://example.net"}resultChan := make(chan string)// 启动多个goroutine并发获取URL的内容for _, url := range urls {go fetchURL(url, resultChan)}// 从通道中接收结果并打印for range urls {result := <-resultChanfmt.Println("Response:", result)}close(resultChan)
}

并发数据处理

对于需要同时处理多个数据流的情况,可以使用goroutine和channel实现并发数据处理,例如数据流的合并、拆分、过滤等操作。

// processData 函数用于处理从dataStream中接收的数据,并将处理结果发送到resultChan中。
func processData(dataStream <-chan int, resultChan chan<- int) {for num := range dataStream {resultChan <- num * 2 // 假设处理数据是将数据乘以2}
}func main() {dataStream := make(chan int)resultChan := make(chan int)// 产生数据并发送到dataStream中go func() {for i := 1; i <= 5; i++ {dataStream <- i}close(dataStream)}()// 启动goroutine并发处理数据go processData(dataStream, resultChan)// 从通道中接收处理结果并打印for range dataStream {result := <-resultChanfmt.Println("Processed Data:", result)}close(resultChan)
}

并发网络编程

编写网络服务器或客户端时,可以利用goroutine处理每个连接,实现高并发的网络应用。

// handler 是一个HTTP请求处理函数,它会向客户端发送"Hello, World!"的响应。
func handler(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello, World!")
}func main() {// 注册HTTP请求处理函数http.HandleFunc("/", handler)// 启动HTTP服务器并监听端口8080go http.ListenAndServe(":8080", nil)fmt.Println("Server started on port 8080")// 使用select{}使主goroutine保持运行状态,以便HTTP服务器能够处理请求select {}
}

定时任务和周期性任务

// task 是一个需要定时执行的任务函数。
func task() {fmt.Println("Task executed at:", time.Now())
}func main() {ticker := time.NewTicker(1 * time.Second)defer ticker.Stop()// 循环等待定时器的触发并执行任务for {select {case <-ticker.C:task()}}
}

工作池

通过创建一组goroutine来处理任务池中的任务,可以有效地控制并发数量,适用于需要限制并发的情况。

// worker 是一个工作函数,它会从jobs通道中接收任务,并将处理结果发送到results通道中。
func worker(id int, jobs <-chan int, results chan<- int) {for job := range jobs {fmt.Printf("Worker %d started job %d\n", id, job)time.Sleep(1 * time.Second) // 模拟工作时间fmt.Printf("Worker %d finished job %d\n", id, job)results <- job * 2 // 假设工作的结果是输入的两倍}
}func main() {const numJobs = 10const numWorkers = 3jobs := make(chan int, numJobs)    // 缓冲channel用于发送任务results := make(chan int, numJobs) // 用于接收任务结果// 启动多个worker goroutinevar wg sync.WaitGroupfor i := 1; i <= numWorkers; i++ {wg.Add(1)go func(id int) {defer wg.Done()worker(id, jobs, results)}(i)}// 发送任务到jobs channelfor j := 1; j <= numJobs; j++ {jobs <- j}close(jobs) // 关闭jobs channel// 等待所有worker完成并收集结果go func() {wg.Wait()close(results)}()// 从通道中接收处理结果并打印for result := range results {fmt.Println("Result:", result)}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/17872.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【数据结构】直接选择排序详解!

文章目录 1.直接选择排序 1.直接选择排序 &#x1f427; begin 有可能就是 maxi &#xff0c;所以交换的时候&#xff0c;要及时更新 maxi &#x1f34e; 直接选择排序是不稳定的&#xff0c;例如&#xff1a; 9 [9] 5 [5]&#xff0c;排序后&#xff0c;因为直接选择排序是会…

Debug-012-el-popover 使用 doClose() 关闭窗口不生效的处理方案

前言&#xff1a; 今天上午碰见一个非常奇怪的情况&#xff1a;一样的方法实现的功能&#xff0c;效果却不一样。 两个页面都是使用的doClose()去关闭的el-popover&#xff0c;其中有一个就是不生效&#xff0c;找不同找了半天&#xff0c;始终不得其解。请看效果吧&#xff1…

Day 5:2785. 将字符串中的元音字母排序

Leetcode 2785. 将字符串中的元音字母排序 给你一个下标从 0 开始的字符串 s &#xff0c;将 s 中的元素重新 排列 得到新的字符串 t &#xff0c;它满足&#xff1a; 所有辅音字母都在原来的位置上。更正式的&#xff0c;如果满足 0 < i < s.length 的下标 i 处的 s[i] …

【第5章】SpringBoot整合Druid

文章目录 前言一、启动器二、配置1.JDBC 配置2.连接池配置3. 监控配置 三、配置多数据源1. 添加配置2. 创建数据源 四、配置 Filter1. 配置Filter2. 可配置的Filter 五、获取 Druid 的监控数据六、案例1. 问题2. 引入库3. 配置4. 配置类5. 测试类6. 测试结果 七、案例 ( 推荐 )…

移动端开发 笔记01

目录 01 移动端的概述 02 移动端的视口标签 03 开发中的二倍图 04 流式布局 05 弹性盒子布局 01 移动端的概述 移动端包括:手机 平板 便携式设备 目前主流的移动端开发: 安卓设备 IOS设备 只要移动端支持浏览器 那么就可以使用浏览器开发移动端项目 开发移动端 使用…

怎么看外国的短视频:四川鑫悦里文化传媒有限公司

怎么看外国的短视频&#xff1a;跨文化视角下的观察与思考 随着全球化进程的加速和网络技术的飞速发展&#xff0c;外国短视频逐渐走进了我们的视野。这些来自不同文化背景、语言体系和审美观念的短视频作品&#xff0c;为我们打开了一扇了解世界的窗口。然而&#xff0c;如何…

golang中的md5、sha256数据加密文件md5/sha256值计算步骤和运行内存图解

在go语言中对数据计算一个md5&#xff0c;或sha256和其他语言 如java, php中的使用方式稍有不同&#xff0c; 那就是要加密的数据必须通过流的形式写入到你创建的Hash对象中。 Hash数据加密步骤 1. 先使用对应的加密算法包中的New函数创建一个Hash对象&#xff0c;(这个也就是…

leetCode. 85. 最大矩形

leetCode. 85. 最大矩形 部分参考上一题链接 leetCode.84. 柱状图中最大的矩形 此题思路 代码 class Solution { public:int largestRectangleArea( vector<int>& h ) {int n h.size();vector<int> left( n ), right( n );stack<int> st;// 求每个矩形…

vue/uniapp 企业微信H5使用JS-SDK

企业微信H5需要我们使用一些SDK方法如获取外部联系人userid 获取当前外部联系人userid 使用SDK前提是如何通过config接口注入权限验证配置 使用说明 - 接口文档 - 企业微信开发者中心 当前项目是vue项目&#xff0c;不好直接使用 引入JS文件&#xff0c;但我们可以安装依赖…

使用nexus搭建的docker私库,定期清理无用的镜像,彻底释放磁盘空间

一、背景 我们使用nexus搭建了docker镜像&#xff0c;随着推送的镜像数量越来越多&#xff0c;导致nexus服务器的磁盘空间不够用了。于是&#xff0c;我们急需先手动删除一些过期的镜像&#xff0c;可发现磁盘空间并没有释放。 那么&#xff0c;如何才能彻底释放掉呢&#xff…

Golang | Leetcode Golang题解之第102题二叉树的层序遍历

题目&#xff1a; 题解&#xff1a; func levelOrder(root *TreeNode) [][]int {ret : [][]int{}if root nil {return ret}q : []*TreeNode{root}for i : 0; len(q) > 0; i {ret append(ret, []int{})p : []*TreeNode{}for j : 0; j < len(q); j {node : q[j]ret[i] …

磁盘分区和挂载

磁盘分区和挂载 一、磁盘 业务层面&#xff1a;满足一定的需求所是做的特定操作 硬盘是什么以及硬盘的作用 硬盘&#xff1a;计算器的存储设备&#xff0c;一个或者多个磁性的盘片做成&#xff0c;可以在盘片上进行数据的读写 连接方式&#xff1a;内部设备&#xff0c;外…

深度揭秘:蓝海创意云渲染农场的五大特色功能

在当今数字化时代&#xff0c;影视制作、效果图设计等领域对于高质量的渲染需求日益增长。在这个背景下&#xff0c;云渲染平台成为了行业中不可或缺的一部分&#xff0c;它为用户提供了高效、灵活的渲染解决方案。蓝海创意云渲染农场https://www.vsochina.com/cn/render蓝海创…

软件需求分析和软件原型开发是一会事情吗?

软件需求分析和软件原型开发是软件开发过程中的两个重要环节&#xff0c;它们各自承担着不同的任务&#xff0c;但又紧密相连&#xff0c;共同影响着软件项目的成功。下面将详细解释这两个环节的定义、目的以及它们之间的关系。 一、软件需求分析 定义&#xff1a;软件需求分析…

C++学习日记 | LAB 6 static library 静态库

资料来源&#xff1a;南科大 余仕琪 C/C Program Design LINK&#xff1a;CPP/week06 at main ShiqiYu/CPP GitHub 一、本节内容 本节主要介绍静态库和动态库。 1.1 静态库和动态库的概念 静态链接和静态库(也称为存档)是链接器将所有使用的库函数复制到可执行文件的结果。静…

小程序唯品会Authorization sign

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01;wx a15018601872 本文章未…

Python---Matplotlib(2万字总结)【从入门到掌握】

数据可视化 在完成了对数据的透视之后&#xff0c;可以将数据透视的结果通过可视化的方式呈现出来&#xff0c;简单的说&#xff0c;就是将数据变成漂亮的图表&#xff0c;因为人类对颜色和形状会更加敏感&#xff0c;然后再进一步解读数据背后隐藏的价值。在之前的文章中已经…

nacos安装与使用

1.nacos简介与安装 什么是注册中心&#xff08;服务治理&#xff09; 服务注册&#xff1a;服务提供者provider&#xff0c;启动的时候向注册中心上报自己的网络信息 服务发现&#xff1a;服务消费者consumer&#xff0c;启动的时候向注册中心上报自己的网络信息&#xff0c;拉…

centos7防火墙入站白名单配置

firewall-cmd --set-default-zonedropfirewall-cmd --get-active-zone记录下当前激活网卡firewall-cmd --permanent --change-interfaceens33 --zonedrop firewall-cmd --zonedrop --list-all 添加信任的源IP和开放端口 firewall-cmd --permanent --add-source192.168.254.1 -…

【OpenCV】图形绘制与填充

介绍了绘制、填充图像的API。也介绍了RNG类用来生成随机数。相关API&#xff1a; line() rectangle() circle() ellipse() putText() 代码&#xff1a; #include "iostream" #include "opencv2/opencv.hpp"using namespace std; using namespace cv…