go当中的channel 无缓冲channel和缓冲channel的适用场景、结合select的使用

Channel

Go channel就像Go并发模型中的“胶水”,它将诸多并发执行单元连接起来,或者正是因为有channel的存在,Go并发模型才能迸发出强大的表达能力。

无缓冲channel

无缓冲channel兼具通信和同步特性,在并发程序中应用颇为广泛。

可以通过不带有capacity参数的内置make函数创建一个可用的无缓冲channel:

c := make(chan T)

由于无缓冲channel的运行时层实现不带有缓冲区,因此对无缓冲channel的接收和发送操作是同步的。

一个无缓冲的channel动作发生和完成的时序如下:

  • 发送动作一定发生在接收动作完成之前;
  • 接收动作一定发生在发送动作完成之前。

这与Go官方“Go内存模型”一文中对channel通信的描述是一致的。正因如此,下面的代码可以保证main输出的变量a的值为"hello, world":

var c = make(chan int)
var a stringfunc f() {
    a = "hello, world"
    <-c
}

func main() {
    go f()
    c <- 5
    println(a)
}

因为函数f中的channel接收动作发生在主goroutine对channel发送动作完成之前,而a = “hello, world"语句又发生在channel接收动作之前,因此主goroutine在channel发送操作完成后看到的变量a的值一定是"hello, world”,而不是空字符串。

用作信号传递

1)一对一通知信号

无缓冲channel常被用于在两个goroutine之间一对一地传递通知信号,

(2)一对多通知信号

无缓冲channel还被用来实现一对多的信号通知机制

关闭一个无缓冲channel会让所有阻塞在该channel上的接收操作返回,从而实现一种一对多的广播机制。该一对多的信号通知机制还常用于通知一组worker goroutine退出:

// 通知其他goroutine工作线程已完成
type signal struct{}

func worker(i int) {
	fmt.Printf("worker %v is working\n", i)
	time.Sleep(5 * time.Second)
	fmt.Printf("worker %v : works done\n", i)
}

// spawnGroup 是一个用于生成一组工作线程的函数
// 参数:
//   - f: 工作函数,每个工作线程都会执行该函数
//   - num: 工作线程的数量
//   - groupSignal: 用于控制工作线程启动和停止的信号通道
//
// 返回值:
//   - <-chan signal: 用于接收所有工作线程完成的信号通道
func spawnGroup(f func(i int), num int, groupSignal <-chan signal) <-chan signal {
	c := make(chan signal)
	var wg sync.WaitGroup
	for i := 0; i < num; i++ {
		wg.Add(1)
		go func(i int) {
			<-groupSignal //阻塞,等待启动信号
			fmt.Printf("worker %v: start to work\n", i)
			f(i)
			wg.Done() //工作完成,减少WaitGroup的计数
		}(i + 1)
	}

	go func() {
		wg.Wait()
		c <- signal(struct{}{}) //发送信号通知
	}()
	return c
}

func main() {
	fmt.Println("start a group of workers...")
	groupSignal := make(chan signal)
	c := spawnGroup(worker, 5, groupSignal)
	time.Sleep(10 * time.Second)
	fmt.Println("the group of workers start to work...")
	// 关闭工作组信号通道,通知所有工作线程开始工作
	close(groupSignal)
	<-c
	fmt.Println("the group of workers work done!")
}

最后结果:

Untitled

用于替代锁机制

由于无缓冲channel具有同步特性,因此可以在某些场合替代锁,让程序更加清晰,可读性增强;以下给出基于共享内存+锁模式的goroutine安全的计数器:

type counter struct {
	c chan int
	i int
}

var cter counter

func InitCounter() {
	cter = counter{
		c: make(chan int),
	}
	// 增加计数器的动作相当于一次接收动作
	go func() {
		for {
			cter.i++
			cter.c <- cter.i
		}
	}()
	fmt.Println("counter init ok")
}

func Increase() int {
	return <-cter.c
}

func init() {
	InitCounter()
}

func main() {
	for i := 0; i < 10; i++ {
		go func(i int) {
			v := Increase()
			fmt.Printf("goroutine-%d: current counter value is %d\n", i, v)
		}(i)
	}
	time.Sleep(5 * time.Second)
}

此代码通过无缓冲channel的同步阻塞特性实现计数器的控制

也符合“不要通过共享内存来通信,而应该通过通信来共享内存”的原则

缓冲channel

带缓冲channel可以通过带有capacity参数的内置make函数创建

c := make(chan T, capacity)

接收操作在缓冲区非空的情况下是异步的(发送或接收无须阻塞等待)

用作消息队列

channel的原生特性与我们认知中的消息队列十分相似,包括goroutine安全、有fifo(first-in, first out)保证等。异步收发的带缓冲channel更适合用作消息队列,并且带缓冲channel在数据收发性能上要明显好于无缓冲channel

用作计数信号量 counting semaphore

带缓冲channel当前数据个数代表的是同时处于活跃状态的goroutine数量,capacity则代表同时处于活跃状态的最大数量。以下是一个例子:

// 同一时间最多3个活跃状态
var active = make(chan struct{}, 3)
var jobs = make(chan int, 10)

func main() {
	go func() {
		for i := 0; i < 8; i++ {
			jobs <- i + 1
		}
		close(jobs)
	}()
	var wg sync.WaitGroup
	for j := range jobs {
		wg.Add(1)
		go func(j int) {
			active <- struct{}{}
			log.Printf("handle job: %v\n", j)
			time.Sleep(2 * time.Second)
			<-active
			wg.Done()
		}(j)
	}
	wg.Wait()
}

结果:

Untitled

可以发现同一时间处于处理状态的job最多为3个

len(channel)的应用

如果s是chan T类型,那么len(s)针对channel的类型不同,有如下两种语义:

  • 当s为无缓冲channel时,len(s)总是返回0;
  • 当s为带缓冲channel时,len(s)返回当前channel s中尚未被读取的元素个数。

但是单纯依靠if语句来判断channel元素状态并不可靠,因为在并发状态下不能保证后续对channel进行收发时channel状态不变:

Untitled

oroutine1在使用len(channel)判空后,便尝试从channel中接收数据。但在其真正从channel中读数据前,goroutine2已经将数据读了出去,goroutine1后面的读取将阻塞在channel上,导致后面逻辑失效。因此,为了不阻塞在channel上,常见的方法是将判空与读取放在一个事务中,将判满与写入放在一个事务中,而这类事务我们可以通过select实现。来看下面的示例:

func producer(c chan<- int) {
	var i int = 1
	for {
		time.Sleep(2 * time.Second)
		ok := trySend(c, i)
		if ok {
			fmt.Printf("[producer]: send [%d] to channel\n", i)
			i++
			continue
		}
		fmt.Printf("[producer]: try send [%d], but channel is full\n", i)
	}
}

func tryRecv(c <-chan int) (int, bool) {
	select {
	case i := <-c:
		return i, true
	default:
		return 0, false
	}
}

func trySend(c chan<- int, i int) bool {
	select {
	case c <- i:
		return true
	default:
		return false
	}
}

func consumer(c <-chan int) {
	for {
		i, ok := tryRecv(c)
		if !ok {
			fmt.Println("[consumer]: try to recv from channel, but the channel is empty")
			time.Sleep(1 * time.Second)
			continue
		}
		fmt.Printf("[consumer]: recv [%d] from channel\n", i)
		if i >= 3 {
			fmt.Println("[consumer]: exit")
			return
		}
	}
}

func main() {
	c := make(chan int, 3)
	go producer(c)
	go consumer(c)

	select {} // 仅用于演示,临时用来阻塞主goroutine
}

结果:

Untitled

这种方法的缺点就在于改变了channel的状态

想在不改变channel状态的前提下单纯地侦测channel的状态,又不会因channel满或空阻塞在channel上。但很遗憾,目前没有一种方法既可以实现这样的功能又适用于所有场合。在特定的场景下,可以用len(channel)来实现。比如图34-2中的这两种场景。

在图34-2中,a是一个多发送单接收的场景,即有多个发送者,但有且只有一个接收者。在这样的场景下,我们可以在接收者goroutine中根据len(channel)是否大于0来判断channel中是否有数据需要接收。

b是一个多接收单发送的场景,即有多个接收者,但有且只有一个发送者。在这样的场景下,我们可以在发送goroutine中根据len(channel)是否小于cap(channel)来判断是否可以执行向channel的发送操作。

Untitled

nil channel的妙用

没有初始化的channel(nil channel)进行读写操作将会发生阻塞

func main() {
	var c chan int
	<-c
}

结果:

Untitled

main goroutine被阻塞在channel上,导致Go运行时认为出现deadlock状态并抛出panic。

但nil channel并非一无是处。来看一个例子:

func main() {
    c1, c2 := make(chan int), make(chan int)
    go func() {
        time.Sleep(time.Second * 5)
        c1 <- 5
        close(c1)
    }()
    
    go func() {
        time.Sleep(time.Second * 7)
        c2 <- 7
        close(c2)
    }()
    
    for {
        select {
        case x, ok := <-c1:
	// 对于一个nil channel执行获取操作,该操作会被堵塞,因此可以显示设置
            if !ok {
                c1 = nil
            } else {
                fmt.Println(x)
            }
        case x, ok := <-c2:
                if !ok {
                    c2 = nil
                } else {
                    fmt.Println(x)
                }
        }
        if c1 == nil && c2 == nil {
            break
        }
    }
    fmt.Println("program end")
}

与select结合

避免阻塞

default的使用通常是在没得选的情况下,因此也有一种可以避免堵塞的特性

func sendTime(c interface{}, seq uintptr) {
    // 无阻塞地向c发送当前时间
    // ...
    select {
        case c.(chan Time) <- Now():
        default:
    }
}

实现超时机制

通过超时事件,既可以避免陷入无尽的等待也可以做一些异常处理工作:

func worker() {
	select {
	case <-c:
		//...
	case <-time.After(30*time.Second):
		return
	}
}

timer实质上是由go运行时自动维护的,而不是操作系统的定时器资源:

Untitled

go通过名为timerproc的函数,维护了一个“最小堆”。该goroutine会被定期唤醒并读取堆顶的timer对象,执行该timer对象对应的函数(向timer.C中发送一条数据,触发定时器),执行完毕后就会从最小堆中移除该timer对象。

所以我们在使用timer的时候应该即使调用timer的Stop方法从最小堆中删除尚未到达过期时间的timer对象。

实现心跳机制

这种机制可以使我们在监听的同时执行一些周期性任务,比如下面这段代码:

func worker() {
    heartbeat := time.NewTicker(30 * time.Second)
    defer heartbeat.Stop()
    for {
        select {
        case <-c:
            // ... 处理业务逻辑
        case <- heartbeat.C: //记得调用方法停止运作
            //... 处理心跳
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

电脑投屏到电视的软件,Mac,Linux,Win均可使用

电脑投屏到电视的软件&#xff0c;Mac&#xff0c;Linux&#xff0c;Win均可使用 AirDroid Cast的TV版&#xff0c;可以上笔记本电脑或台式电脑直接投屏到各种安卓电视上。 无线投屏可以实现本地投屏及远程投屏&#xff0c;AirPlay协议可以实现本地投屏&#xff0c;大家可以按需…

1panel在应用商店里面安装jenkins

文章目录 目录 文章目录 前言 一、使用步骤 1.1 填写安装参数 1.2 在界面中进入容器拿到自动生成的jenkins密码 前言 一、使用步骤 1.1 填写安装参数 在应用商店里面搜索jenkins,然后点击安装 填写参数 1.2 在界面中进入容器拿到自动生成的jenkins密码 命令 cat /var/jenki…

【腾讯云 HAI域探秘】基于高性能应用服务器HAI部署的 ChatGLM2-6B模型,我开发了AI办公助手,公司行政小姐姐用了都说好!

目录 前言 一、腾讯云HAI介绍&#xff1a; 1、即插即用 轻松上手 2、横向对比 青出于蓝 3、多种高性能应用部署场景 二、腾讯云HAI一键部署并使用ChatGLM2-6B快速实现开发者所需的相关API服务 1、登录 高性能应用服务 HAI 控制台 2、点击 新建 选择 AI模型&#xff0c;…

Flutter | TextField长按时选项菜单复制、粘贴显示为英文问题解决

Flutter | TextField长按时选项菜单复制、粘贴显示为英文问题解决 问题描述&#xff1a; 长按TextField后&#xff0c;显示剪切、复制等选项为英文&#xff0c;如下图所示&#xff0c;这是因为问未设置语言本地化&#xff0c;我们需要进行设置。 首先在pubspec.yaml加入以下依赖…

如何快速搭建一个大模型?简单的UI实现

&#x1f525;博客主页&#xff1a;真的睡不醒 &#x1f680;系列专栏&#xff1a;深度学习环境搭建、环境配置问题解决、自然语言处理、语音信号处理、项目开发 &#x1f498;每日语录&#xff1a;相信自己&#xff0c;一路风景一路歌&#xff0c;人生之美&#xff0c;正在于…

NX二次开发UF_CURVE_ask_offset_parms 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan UF_CURVE_ask_offset_parms Defined in: uf_curve.h int UF_CURVE_ask_offset_parms(tag_t offset_curve_object, UF_CURVE_offset_data_p_t offset_data_pointer ) overview 概述 …

手把手教会你--github的学习--持续更新

有什么问题&#xff0c;请尽情问博主&#xff0c;QQ群796141573 前言1.1 使用过程(1) 进入某个项目(2) 点击某个文件(3) 在源码区域下面(4) 源码区的头顶上 1.2 作者的其他项目1.3 搜索1.4 复制别人的代码(即项目)到自己的空间内1.5 上传自己的Bugs(bushi1.6 在线修改文件1.7 评…

基于OPC UA 的运动控制读书笔记(1)

最近一段时间集中研究OPCUA 在机器人控制应用中应用的可能性。这个话题自然离不开运动控制。 笔者对运动控制不是十分了解。于是恶补EtherCAT 驱动&#xff0c;PLCopen 运动控制的知识&#xff0c;下面是自己的读书笔记和实现OPCUA /IEC61499 运动控制器的实现方案设想。 为什么…

【Web】攻防世界Web_php_wrong_nginx_config

这题考察了绕过登录、目录浏览、后门利用 进来先是一个登录框&#xff0c;随便怎么输前端都直接弹窗 禁用js后再输入后登录 查看源码&#xff0c;好家伙&#xff0c;不管输什么都进不去 直接扫目录 访问/robots.txt 访问/hint.php 访问/Hack.php 抓包看一下 cookie里isLogin0…

kafka2.x常用命令:创建topic,查看topic列表、分区、副本详情,删除topic,测试topic发送与消费

原创/朱季谦 接触kafka开发已经两年多&#xff0c;也看过关于kafka的一些书&#xff0c;但一直没有怎么对它做总结&#xff0c;借着最近正好在看《Apache Kafka实战》一书&#xff0c;同时自己又搭建了三台kafka服务器&#xff0c;正好可以做一些总结记录。 本文主要是记录如…

【matlab程序】matlab画台风符号和实例应用

【matlab程序】matlab画台风符号和实例应用 没有看文献&#xff0c;不知道文献中的符号什么样子&#xff0c;据我理解为这样子的&#xff1a; 因此&#xff0c;按照自己的理解做了这期。 结果浏览&#xff1a; 台风符号一切可改&#xff0c;可细细改。可是我不发论文&#xf…

【Amazon】安装Cloudwatch代理监控EC2

文章目录 一、实验概要二、实验操作步骤2.1 创建 CloudWatch 代理运行角色2.2 安装 CloudWatch 代理软件包2.3 使用 CloudWatch代理收集指标2.4 CloudWatch指标收集确认 三、参考链接 一、实验概要 使用 CloudWatch 代理从 Amazon EC2 实例和本地服务器中收集指标、日志和跟踪信…

树套树 (线段树+splay)

树套树&#xff0c;就是线段树、平衡树、树状数组等数据结构的嵌套。 最简单的是线段树套set&#xff0c;可以解决一些比较简单的问题&#xff0c;而且代码根线段树是一样的只是一些细节不太一样。 本题中用的是线段树套splay&#xff0c;代码较长。 树套树中的splay和单一的…

jenkins流水线(pipline)实例

1、pipline 语法介绍 声明式的pipeline语法格式 1. 所有的声明都必须包含在pipeline{}中 2. 块只能有节段&#xff0c;指令&#xff0c;步骤或者赋值语句组成 3. 阶段&#xff1a;agent&#xff0c;stages&#xff0c;post&#xff0c;steps 4. 指令&#xff1a;environment&a…

2017年五一杯数学建模B题自媒体时代的消息传播问题解题全过程文档及程序

2017年五一杯数学建模 B题 自媒体时代的消息传播问题 原题再现 电视剧《人民的名义》中人物侯亮平说&#xff1a;“现在是自媒体时代&#xff0c;任何突发性事件几分钟就传播到全世界。”相对于传统媒体&#xff0c;以互联网技术为基础的自媒体以其信息传播的即时性、交往方式…

C#,数值计算——插值和外推,RBF_fn 与 RBF_gauss 的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { public interface RBF_fn { double rbf(double r); } } ---------------------------------------------- using System; namespace Legalsoft.Truffer { public class RBF_gauss : RBF…

如何通过nginx进行服务的负载均衡

简单介绍 随着互联网的发展&#xff0c;业务流量越来越大并且业务逻辑也越来越复杂&#xff0c;单台服务器的性能及单点故障问题就凸显出来了&#xff0c;因此需要多台服务器组成应用集群&#xff0c;进行性能的水平扩展以及避免单点故障的出现。应用集群是将同一应用部署到多台…

上海亚商投顾:北证50指数大涨 逾百只北交所个股涨超10%

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 沪指11月24日震荡调整&#xff0c;深成指、创业板指盘中跌超1%。北证50指数大涨超6%&#xff0c;北交所个股持…

虚拟化逻辑架构: LBR 网桥基础管理

目录 一、理论 1.Linux Bridge 二、实验 1.LBR 网桥管理 三、问题 1.Linux虚拟交换机如何增删 一、理论 1.Linux Bridge Linux Bridge&#xff08;网桥&#xff09;是用纯软件实现的虚拟交换机&#xff0c;有着和物理交换机相同的功能&#xff0c;例如二层交换&#…

redis key

不管是&#xff1a;规则&#xff0c;还是其他规则&#xff0c;定义好就可以了。其实没有太多要求的。 1&#xff09;冒号分割类似那种yaml在客户端显示树结构 2&#xff09;其他分割类似那种properties在客户端显示列表结构