Go语言:两种常见的并发模型
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语言:两种常见的并发模型的更多相关文章
- Golang 入门系列(十七)几个常见的并发模型——生产者消费者模型
前面已经讲过很多Golang系列知识,包括并发,锁等内容,感兴趣的可以看看以前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863.ht ...
- 移动Web开发图片自适应两种常见情况解决方案
本文主要说的是Web中图片根据手机屏幕大小自适应居中显示,图片自适应两种常见情况解决方案.开始吧 在做配合手机客户端的Web wap页面时,发现文章对图片显示的需求有两种特别重要的情况,一是对于图集, ...
- jQuery ajax调用后台aspx后台文件的两种常见方法(不是ashx)
在asp.net webForm开发中,用Jquery ajax调用aspx页面的方法常用的有两种:下面我来简单介绍一下. [WebMethod] public static string SayHe ...
- R语言两种方式求指定日期所在月的天数
R语言两种方式求指定日期所在月的天数 days_monthday<-function(date){ m<-format(date,format="%m& ...
- 移动站Web开发图片自适应两种常见情况解决方案
本文主要说的是Web中图片根据手机屏幕大小自适应居中显示,图片自适应两种常见情况解决方案.开始吧 在做配合手机客户端的Web wap页面时,发现文章对图片显示的需求有两种特别重要的情况,一是对于图集, ...
- 两种RBAC权限控制模型详解
序言 由于最近一直卡在权限控制这个坎上,原来设计的比较简单的权限控制思路已经无法满足比较复杂一些的场景,因此一直在探索一种在大部分场景下比较通用的权限模型. 首先,这里说明一下两种RBAC权限模型分别 ...
- SQL SERVER中的两种常见死锁及解决思路
在sql server中,死锁都与一种锁有关,那就是排它锁(x锁).由于在同一时间对同一个数据库资源只能有一个数据库进程可以拥有排它锁.因此,一旦多个进程都需要获取某个或者同一个数据库资源的排它访问权 ...
- Azure登陆的两种常见方式(user 和 service principal登陆)
通过Powershell 登陆Azure(Azure MoonCake为例)一般常见的有两种方式 1. 用户交互式登陆 前提条件:有一个AAD account 此种登陆方式会弹出一个登陆框,让你输入一 ...
- MES实施会有哪些情况?为你介绍两种常见的类型
MES项目实施顾问是一份极具挑战的工作,需具备大量的专业知识,以及丰富的实施经验.今天,小编为大家介绍最常见的两种MES实施顾问类型,希望对大家有所启发. 保姆型实施顾问 是指以实施顾问为主导,只要是 ...
- Java服务端两个常见的并发错误
理想情况来讲,开发在开始编写代码之前就应该讲并发情况考虑进去,但是大多数实际情况确是,开发压根不会考虑高并发情况下的业务问题.主要原因还是因为业务极难遇到高并发的情况. 下面列举两个比较常见的后端编码 ...
随机推荐
- localtime函数
localtime函数: 将时间数值变换成本地时间,考虑到本地时区和夏令时标志; 原型: struct tm *localtime(const time_t * calptr); 头文件 <ti ...
- springmvc接口访问流程排查
首先找到webapp下面的web.xml文件: 检查前端控制器: 并注意contextConfigLocation配置的springmvc的配置文件路径: 接着找到springmvc配置文件路径,如果 ...
- 02题解-洛谷 P2395 BBCode转换Markdown 题解
洛谷 P2395 BBCode转换Markdown 题解 题目传送门: here. 一道毒瘤的大模拟,给了你一部分的 BBCode 和 Markdown 语法,叫你转换.如下表: BBCode Mar ...
- C++ vector的emplace_back函数
C++ STL的vector相信大家一定都知道,它是一个一般用来当做可变长度列表的类.在C++11之前,一般给vector插入新元素用得都是push_back函数,比如下面这样: std::vecto ...
- [cisco][LAB]OSPF in NBMA
NBMA為一種沒有廣播類型的的網路連接,這會使得OSPF建立需要手動設定 拓樸如下: R1# ! interface Loopback0 ip address 172.16.1.1 255.255.2 ...
- Safari 浏览器下打印PDF, 打印预览显示为空白
重现代码 const iframe = document.createElement('iframe'); iframe.onload = () => { iframe.focus(); ifr ...
- supervisor 使用中遇到的问题
supervisor 配置完毕,使用supervisorctl reload 和supervisorctl update 启动时候报错 解决方法使用下面命令启动 /usr/bin/python2 /u ...
- 微信小程序中注册页面设计
.wxml <text>姓名</text> <input placeholder="请输入姓名" bindinput="getname&qu ...
- MySQL学习(十一)为什么不推荐使用uuid和雪花id
参考博客:https://www.cnblogs.com/wyq178/p/12548864.html 自增的主键的值是顺序的,所以Innodb把每一条记录都存储在一条记录的后面.当达到页面的最大填充 ...
- 详解ResNet 网络,如何让网络变得更“深”了
摘要:残差网络(ResNet)的提出是为了解决深度神经网络的"退化"(优化)问题.ResNet 通过设计残差块结构,调整模型结构,让更深的模型能够有效训练更训练. 本文分享自华为云 ...