Go语言:两种常见的并发模型

在并发编程中,须要精确地控制对共享资源的访问,Go语言将共享的值通过通道传递

并发版"Hello World"

使用goroutine来打印"Hello World"

package main

import "fmt"

func main() {
done := make(chan int, 10) // 缓冲通道 for i := 0; i < cap(done); i++ {
go func() {
fmt.Println("Hello World")
done <- 1
}()
} for i := 0; i < cap(done); i++ {
<-done
}
}

上述代码使用了一个大小为10的缓冲通道,使用一个循环启用了10个goroutine来打印一句"Hello World",利用了通道的特性,当goroutine没有全部完成时,势必会有一个<-done被阻塞,于是基于这一点来等待这10个goroutine的结束。

利用WaitGroup可以达成同样的目的:

package main

import (
"fmt"
"sync"
) func main() {
var wg sync.WaitGroup for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
fmt.Println("Hello World")
wg.Done()
}()
}
wg.Wait()
}

必须确保在后台线程启动之前执行wg.Add(1),用于增加等待事件的个数。当后台线程完成打印工作之后,调用wg.Done表示完成一个事件,wg.Wait等待全部时间完成。

生产者/消费者模型

生产者生产一些数据,然后放到产品队列中,同时消费者从产品队列中取得这些数。生产和消费是两个异步的过程,当产品队列中没有数据时,消费者就进入饥饿等待中,当产品队列中数据已满时,生产者则面临因产品积压导致CPU被剥夺的问题。

package main

import (
"fmt"
"time"
) func producer(factor int, out chan<- int) {
for i := 0; ; i++ {
out <- factor * i
}
} func consumer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
} func main() {
ch := make(chan int, 64) go producer(3, ch)
go producer(5, ch)
go consumer(ch) time.Sleep(5 * time.Second)
}

上述代码模拟了这一过程,定义了一个producer函数作为生产者,定义了一个consumer函数作为消费者,在主函数中,创建了一个64个int大小的队列,用于存放producer生产好的"产品",producer会不断向该队列发送数据,consumer会迭代这个队列,打印出队列中的数据。

上述程序中是采用sleep让主线程沉睡来让producer线程和consumer线程运行一段时间,可以考虑利用信号来退出

func main() {
ch := make(chan int, 64) go producer(3, ch)
go producer(5, ch)
go consumer(ch) sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) // 挂接信号
fmt.Printf("quit (%v)\n", <-sig)
}

按下Ctrl+C就会产生一个SIGINT中断信号发送到sig通道中,使该通道不再阻塞,使得程序正常退出。

发布/订阅模型

pub/sub模型,在该模型中,消息生产者为发布者,消息消费者为订阅者,生产者和消费者是M:N关系。在上述的生产者/消费者模型中,是将消息发送到一个队列中,而发布/订阅模型则是将消息发布给一个主题。

具体看代码:

package main

import (
"fmt"
"strings"
"sync"
"time"
) type (
subscriber chan interface{} // 订阅者为一个通道
topicFunc func(v interface{}) bool // 主题为一个过滤器
) type Publisher struct {
mutex sync.RWMutex // 读写锁
buffer int // 订阅队列的缓存大小
timeout time.Duration // 发布超时时间
subscribers map[subscriber]topicFunc // 订阅者信息
} // NewPublisher 构建一个发布者对象 可以设置发布超时时间和缓存队列长度
func NewPublisher(publishTimeout time.Duration, buffer int) *Publisher {
return &Publisher{
buffer: buffer,
timeout: publishTimeout,
subscribers: make(map[subscriber]topicFunc),
}
} // SubscribeTopic 添加一个新的订阅者,订阅过滤器筛选后的主题
func (p *Publisher) SubscribeTopic(topic topicFunc) chan interface{} {
ch := make(chan interface{}, p.buffer)
p.mutex.Lock()
p.subscribers[ch] = topic
p.mutex.Unlock()
return ch
} // Subscribe 添加一个新的订阅者,订阅全部主题
func (p *Publisher) Subscribe() chan interface{} {
return p.SubscribeTopic(nil)
} // Evict 退出订阅
func (p *Publisher) Evict(sub chan interface{}) {
p.mutex.Lock()
defer p.mutex.Unlock()
delete(p.subscribers, sub)
close(sub)
} // Publish 发布一个主题
func (p *Publisher) Publish(v interface{}) {
p.mutex.RLock()
defer p.mutex.RUnlock() var wg sync.WaitGroup
for sub, topic := range p.subscribers {
wg.Add(1)
go p.sendTopic(sub, topic, v, &wg)
}
wg.Wait()
} // Close 关闭发布者对象,同时关闭所有的订阅者通道
func (p *Publisher) Close() {
p.mutex.Lock()
defer p.mutex.Unlock() for sub := range p.subscribers {
delete(p.subscribers, sub)
close(sub)
}
} // sendTopic 发送主题,可以容忍一定的时限
func (p *Publisher) sendTopic(sub subscriber, topic topicFunc, v interface{}, wg *sync.WaitGroup) {
defer wg.Done()
if topic != nil && !topic(v) {
return
}
select {
case sub <- v:
case <-time.After(p.timeout):
}
} func main() {
p := NewPublisher(100*time.Microsecond, 10)
defer p.Close() all := p.Subscribe() // 订阅所有主题
// 订阅golang主题
golang := p.SubscribeTopic(func(v interface{}) bool {
if s, ok := v.(string); ok {
return strings.Contains(s, "golang")
}
return false
}) p.Publish("Hello world")
p.Publish("Hello golang") go func() {
for msg := range all {
fmt.Println("all:", msg)
}
}() go func() {
for msg := range golang {
fmt.Println("golang:", msg)
}
}() // 运行一段时间后退出
time.Sleep(3 * time.Second)
}

程序中定义了Publisher结构体,为其定义两种订阅方法,对于SubscribeTopic函数要传入一个函数作为过滤器,该过滤器遇到未订阅的主题消息时会返回false,对于订阅的主题消息则返回true。sendTopic函数的作用是向订阅者的通道发送主题消息,并且调用过滤器判断消息是否需要,如果不需要(false),则丢弃数据,返回该函数。sendTopic函数会被放置在Publish函数中,对每个订阅者都调用sendTopic函数来发送主题消息。在main函数中创建了两个订阅者(通道),分别订阅所有主题和订阅一个主题,之后创建了两个线程,不断打印两个订阅队列的消息。

在上述pub/sub模型中,每条消息都会传送给多个订阅者。发布者通常不会知道,也不关心哪一个订阅者正在接收主题消息。订阅者和发布者可以在运行时动态添加,它们之间是一条松散的耦合关系,这使得系统的复杂性随时间的推移而增长。

安全退出

通知线程停止任务,特别是当它工作在错误的方向上时。

package main

import (
"fmt"
"sync"
"time"
) func worker(wg *sync.WaitGroup, cancel chan bool) {
defer wg.Done() for {
select {
default:
fmt.Println("working")
case <-cancel: // 退出信号
return
}
}
} func main() {
// 创建通道 传递退出信号
cancel := make(chan bool)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go worker(&wg, cancel)
}
time.Sleep(time.Second)
close(cancel)
wg.Wait()
}

这里利用了select关键字,当select有多个分支时会随机选择一个可用的通道分支,如果没有可用的通道分支,则选择default分支,否则会一直阻塞。

在上述代码中,当关闭通道时,也起到了一个广播的作用,所有线程函数中的select中的cancel通道不再阻塞,将线程退出。

context包

利用context包可以达成更理想的效果,以此来实现线程的安全退出,基于此将上述代码修改:

package main

import (
"context"
"fmt"
"sync"
"time"
) func worker(ctx context.Context, wg *sync.WaitGroup) error {
defer wg.Done() for {
select {
default:
fmt.Println("Hello")
case <-ctx.Done():
return ctx.Err()
}
}
} func main() {
// 设置超时时间为10秒
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go worker(ctx, &wg)
}
time.Sleep(time.Second)
cancel() // 将所有线程退出
wg.Wait()
}

Go语言:两种常见的并发模型的更多相关文章

  1. Golang 入门系列(十七)几个常见的并发模型——生产者消费者模型

    前面已经讲过很多Golang系列知识,包括并发,锁等内容,感兴趣的可以看看以前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863.ht ...

  2. 移动Web开发图片自适应两种常见情况解决方案

    本文主要说的是Web中图片根据手机屏幕大小自适应居中显示,图片自适应两种常见情况解决方案.开始吧 在做配合手机客户端的Web wap页面时,发现文章对图片显示的需求有两种特别重要的情况,一是对于图集, ...

  3. jQuery ajax调用后台aspx后台文件的两种常见方法(不是ashx)

    在asp.net webForm开发中,用Jquery ajax调用aspx页面的方法常用的有两种:下面我来简单介绍一下. [WebMethod] public static string SayHe ...

  4. R语言两种方式求指定日期所在月的天数

                 R语言两种方式求指定日期所在月的天数 days_monthday<-function(date){ m<-format(date,format="%m& ...

  5. 移动站Web开发图片自适应两种常见情况解决方案

    本文主要说的是Web中图片根据手机屏幕大小自适应居中显示,图片自适应两种常见情况解决方案.开始吧 在做配合手机客户端的Web wap页面时,发现文章对图片显示的需求有两种特别重要的情况,一是对于图集, ...

  6. 两种RBAC权限控制模型详解

    序言 由于最近一直卡在权限控制这个坎上,原来设计的比较简单的权限控制思路已经无法满足比较复杂一些的场景,因此一直在探索一种在大部分场景下比较通用的权限模型. 首先,这里说明一下两种RBAC权限模型分别 ...

  7. SQL SERVER中的两种常见死锁及解决思路

    在sql server中,死锁都与一种锁有关,那就是排它锁(x锁).由于在同一时间对同一个数据库资源只能有一个数据库进程可以拥有排它锁.因此,一旦多个进程都需要获取某个或者同一个数据库资源的排它访问权 ...

  8. Azure登陆的两种常见方式(user 和 service principal登陆)

    通过Powershell 登陆Azure(Azure MoonCake为例)一般常见的有两种方式 1. 用户交互式登陆 前提条件:有一个AAD account 此种登陆方式会弹出一个登陆框,让你输入一 ...

  9. MES实施会有哪些情况?为你介绍两种常见的类型

    MES项目实施顾问是一份极具挑战的工作,需具备大量的专业知识,以及丰富的实施经验.今天,小编为大家介绍最常见的两种MES实施顾问类型,希望对大家有所启发. 保姆型实施顾问 是指以实施顾问为主导,只要是 ...

  10. Java服务端两个常见的并发错误

    理想情况来讲,开发在开始编写代码之前就应该讲并发情况考虑进去,但是大多数实际情况确是,开发压根不会考虑高并发情况下的业务问题.主要原因还是因为业务极难遇到高并发的情况. 下面列举两个比较常见的后端编码 ...

随机推荐

  1. PHP如何在两个大文件中找出相同的记录?

    1.引言 给定a,b两个文件, 分别有x,y行数据, 其中(x, y均大于10亿), 机器内存限制100M,该如何找出其中相同的记录? 2.思路 处理该问题的困难主要是无法将这海量数据一次性读进内存中 ...

  2. TCP 为什么是 三次 握手 不是两次 不是四次

    ​ 为什么不是两次 (1) 防止 历史 旧数据 连接 客户端连续发送多次 SYN 建⽴连接的报⽂,在⽹络拥堵等情况下: ● ⼀个「旧 SYN 报⽂」⽐「最新的 SYN 」 报⽂早到达了服务端: ● 那 ...

  3. vite + vue安装 注意事项

    一.要求node版本必须>12.0.0 1.node 如何升级 · 执行npm cache clean -f 清除缓冲 · npm install -g n 安装 n 模块  n模块用于管理 n ...

  4. (五).JavaScript的数组

    1. 数组 1.1 数组的基础 数组:同种或不同数据类型数据的有序集合 功能:同时存储多个数据 数据:常量 变量 表达式 数组 函数 对象 定义方式:字面量定义或者构造函数定义 字面量定义数组(本质上 ...

  5. ggplot axis text 拐弯

    scale_y_discrete(position = "left",labels=function(x) str_wrap(x, width=48)) +

  6. Python数据可视化-条形图渐变颜色

    import pandas as pd from pyecharts.charts import Bar from pyecharts.commons.utils import JsCode data ...

  7. Java中的方法增强

    A:在不影响业务情况下,增强一个方法有几种方法呢? B:3种! A:哪三种呀? 一.继承类来重写方法: 1.要可以获取这个类的构造: class Man{ public void run(){ Sys ...

  8. layui 手册

    https://layui.yii666.com/doc/modules/layer.html

  9. 关与python面向对象的认识

    面向对象编程 类:从一堆对象中以抽象的方式把相同的特征归类得到. 抽象类 类 实列 子类抽象为父类,子类继承父类特征. 类实例化为实例,实例抽象为类. class Human(object): cen ...

  10. Oracle-账户被锁:The account is locked

    Oracle-账户被锁:The account is locked cmd-->sqlplus--> 管理员登录:system@ORCL 密码:1 然后输入:alter user kjb( ...