草庐IT

支持首次触发的 Go Ticker

CODE 杂谈 2023-03-28 原文

促使我写这篇文章主要是在写一个关于虚拟货币账户监控的项目时使用 Ticker 的问题。

Ticker 的问题

如果用过 Ticker 的朋友会知道,创建 Ticker 后并不会马上执行,而是会等待一个时间 d,这就是创建时的间隔时间。如果间隔时间很短这基本上不会有太大问题,但是如果对首次执行时间有要求,就会很麻烦。例如以下这个案例:


package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	ts := time.NewTicker(5 * time.Second)
	fmt.Println("start_time#", time.Now().Unix())
	chanClose := make(chan struct{})
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		for {
			select {
			case <-chanClose:
				return
			case <-ts.C:
				fmt.Println("run_time#", time.Now().Unix())
			}
		}
	}()

	go func() {
		time.Sleep(10 * time.Second)
		chanClose <- struct{}{}
		ts.Stop()
	}()
	wg.Wait()
}

它将返回以下内容:

start_time# 1656860176
run_time# 1656860181
run_time# 1656860186

为了方便演示我们在事例中设了一个很短的时间,我们可以看到从代码启动到真正定时器触发,代码等待了5秒,就是time.NewTicker 创建时我们传的参数时间。但如果我们把这个时间改成1个小时,我们需要等待1个小时才会真正开始执行。

寻找解决方案

在我的项目中需要定时器马上执行,所以我通过搜索搜到了 Go 官方仓库 Issues 中提到过这个问题的解决方案 “time: create ticker with instant first tick”。我们可以看一下这个事例:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	ts := time.NewTicker(5 * time.Second)
	fmt.Println("start_time#", time.Now().Unix())
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		for ; true; <-ts.C {
			fmt.Println("run_time#", time.Now().Unix())
		}
	}()

	go func() {
		time.Sleep(10 * time.Second)
		ts.Stop()
		wg.Done()
	}()
	wg.Wait()
}

上述的执行后返回内容:

start_time# 1656860889
run_time# 1656860889
run_time# 1656860894

我们可以看到首次定时器触发任务的时间变成了程序执行的开始时间!在我们的例子中,这种方式没有问题,但是我们需要关注退出条件,在这里是 main goroutine 直接退出。第一个 goroutine 其实直到 main 退出前一直是堵塞状态。如果你的项目中多次使用这种形式的定时器,每一个都会有一个堵塞的 goroutine,虽然不会对你程序造成 panic,但我还是感觉不是很好。

我的版本

先上代码:

package ticktock

import (
	"time"
)
// 这个结构体内容是为了兼容 Ticker 的使用方式
type tickerStart struct {
	C      chan time.Time
	ticker *time.Ticker
	close  chan struct{}
}

func NewTickerStart(d time.Duration) *tickerStart {
    // 这里我们创建的 channel 设了一个 buffer,原因是我们需要
    // 在下面 Start 方法中及时推送当前时间而不至于堵塞。
    // 
	c := make(chan time.Time, 1) 
	return &tickerStart{ticker: time.NewTicker(d), C: c, close: make(chan struct{})}
}
// 这是我们核心的方法
func (ts *tickerStart) Start() {
	ts.C <- time.Now() // 首次触发关键
	go func() {
		for {
			select {
			case _, ok := <-ts.close: // 用于关闭这个 goroutine
				if !ok {
					return
				}
			case t := <-ts.ticker.C: 
			// 把go原生定时器 push 的时间推送到我们定义的 time channel 中
				ts.C <- t
			}

		}
	}()
}
// 兼容 ticker
func (ts *tickerStart) Reset(d time.Duration) {
	ts.ticker.Reset(d)
}
// 兼容 ticker
func (ts *tickerStart) Stop() {
	ts.ticker.Stop()
	close(ts.close)
}

使用代码如下:

package main

import (
	"fmt"
	"sync"
	"time"
	"ticktock"
)

func main() {
	fmt.Println("start_time#", time.Now().Unix())
	chanClose := make(chan struct{})
	tts := ticktock.NewTickerStart(5 * time.Second)
	tts.Start()
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		for {
			select {
			case <-chanClose:
				return
			case <-tts.C:
				fmt.Println("run_time#", time.Now().Unix())
			}
		}
	}()

	go func() {
		time.Sleep(10 * time.Second)
		chanClose <- struct{}{}
		tts.Stop()
	}()
	wg.Wait()
}

执行返回内容如下:

start_time# 1656861872
run_time# 1656861872
run_time# 1656861877

可以看到,和我们想要的一致。但和官方给出的不同我们不会堵塞 goroutine 。

最后

这是我在写虚拟货币账户监控项目中碰到的其中一个问题,我也会在后续的文章中写一写我碰到的其他问题。当然这个项目会开源,可以关注我的 github GanymedeNil's github

最后的最后

顺便给自己宣传一下,如果你对我感兴趣或者想和我聊聊可以加我微信 ganymede-nil和下载我的简历,找工作中,注明加我的理由,防止被我当作营销人员?。

有关支持首次触发的 Go Ticker的更多相关文章

  1. ruby - 触发器 ruby​​ 中 3 点范围运算符和 2 点范围运算符的区别 - 2

    请帮助我理解范围运算符...和..之间的区别,作为Ruby中使用的“触发器”。这是PragmaticProgrammersguidetoRuby中的一个示例:a=(11..20).collect{|i|(i%4==0)..(i%3==0)?i:nil}返回:[nil,12,nil,nil,nil,16,17,18,nil,20]还有:a=(11..20).collect{|i|(i%4==0)...(i%3==0)?i:nil}返回:[nil,12,13,14,15,16,17,18,nil,20] 最佳答案 触发器(又名f/f)是

  2. ruby-on-rails - Rails - 乐观锁定总是触发 StaleObjectError 异常 - 2

    我正在学习Rails,并阅读了关于乐观锁的内容。我已将类型为integer的lock_version列添加到我的articles表中。但现在每当我第一次尝试更新记录时,我都会收到StaleObjectError异常。这是我的迁移:classAddLockVersionToArticle当我尝试通过Rails控制台更新文章时:article=Article.first=>#我这样做:article.title="newtitle"article.save我明白了:(0.3ms)begintransaction(0.3ms)UPDATE"articles"SET"title"='dwdwd

  3. ruby-on-rails - 如何在 Rails Controller Action 上触发 Facebook 像素 - 2

    我有一个ruby​​onrails应用程序。我按照facebook的说明添加了一个像素。但是,要跟踪转化,Facebook要求您将页面置于达到预期结果时出现的转化中。即,如果我想显示客户已注册,我会将您注册后转到的页面作为成功对象进行跟踪。我的问题是,当客户注册时,在我的应用程序中没有登陆页面。该应用程序将用户带回主页。它在主页上显示了一条消息,所以我想看看是否有一种方法可以跟踪来自Controller操作而不是实际页面的转化。我需要计数的Action没有页面,它们是ControllerAction。是否有任何人都知道的关于如何执行此操作的gem、文档或最佳实践?这是进入布局文件的像素

  4. ruby - Faye WebSocket,关闭处理程序被触发后重新连接到套接字 - 2

    我有一个super简单的脚本,它几乎包含了FayeWebSocketGitHub页面上用于处理关闭连接的内容:ws=Faye::WebSocket::Client.new(url,nil,:headers=>headers)ws.on:opendo|event|p[:open]#sendpingcommand#sendtestcommand#ws.send({command:'test'}.to_json)endws.on:messagedo|event|#hereistheentrypointfordatacomingfromtheserver.pJSON.parse(event.d

  5. ruby-on-rails - Ruby method_added 回调不触发包括模块 - 2

    我想写一点“Deprecate-It”库并经常使用“method_added”回调。但是现在我注意到在包含模块时不会触发此回调。是否有任何回调或变通方法,以便在某些内容包含到自身时通知类“Foobar”?用于演示的小Demo:#IncludingModulswon'ttriggermethod_addedcallbackmoduleInvisibleMethoddefinvisible"Youwon'tgetacallbackfromme"endendclassFoobardefself.method_added(m)puts"InstanceMethod:'#{m}'addedto'

  6. ruby - 如何使用 readline 支持重新安装 ruby​​? - 2

    我已经按照https://github.com/wayneeseguin/rvm#installation上的说明通过RVM安装了Ruby.有关信息,我有所有文件(readline-5.2.tar.gz、readline-6.2.tar.gz、ruby-1.9.3-p327.tar.bz2、rubygems-1.8.24.tgz、wayneeseguin-rvm-stable.tgz和yaml-0.1.4.tar.gz)在~/.rvm/archives目录中,我不想在任何目录中重新下载它们方式。当我这样做时:sudo/usr/bin/apt-getinstallbuild-essent

  7. ruby-on-rails - "undefined method ` stub_request '"访问 RSpec 支持文件中的方法时 - 2

    我的Ruby-on-Rails项目中有以下文件结构,用于规范:/spec/msd/serviceservice_spec.rb/support/my_modulerequests_stubs.rb我的request_stubs.rb有:moduleMyModule::RequestsStubsmodule_functiondeflist_clientsurl="dummysite.com/clients"stub_request(:get,url).to_return(status:200,body:"clientsbody")endend在我的service_spec.rb我有:re

  8. ruby - Ruby 是否支持逐字字符串? - 2

    Ruby是否支持(找不到更好的词)非转义(逐字)字符串?就像在C#中一样:@"c:\ProgramFiles\"...或者在Tcl中:{c:\ProgramFiles\} 最佳答案 是的,您需要在字符串前加上%前缀,然后是描述其类型的单个字符。你想要的是%q{c:\programfiles\}。镐书很好地涵盖了这一点here,部分是通用分隔输入。 关于ruby-Ruby是否支持逐字字符串?,我们在StackOverflow上找到一个类似的问题: https:/

  9. ruby - 在 Ruby 1.8 中支持 Ruby 1.9 的哈希语法 - 2

    我正在编写一个Rubygem,在我的代码中使用{key:'value'}哈希语法。我的测试都在1.9.x中通过,但我(可以理解)在1.8.7中得到syntaxerror,unexpected':',expecting')'。是否有支持1.8.x的最佳实践?我是否需要使用我们的老friend=>重写代码,还是有更好的策略? 最佳答案 我认为你运气不好,如果你想支持1.8,那么你必须使用=>。像往常一样,我会提到在1.9的某些情况下您必须使用=>:如果键不是一个符号。请记住,任何对象(符号、字符串、类、float……)都可以是Ruby哈

  10. javascript - jQuery 的 jquery-1.10.2.min.map 正在触发 404(未找到) - 2

    我看到有关未找到文件min.map的错误消息:GETjQuery'sjquery-1.10.2.min.mapistriggeringa404(NotFound)截图这是从哪里来的? 最佳答案 如果ChromeDevTools报告.map文件的404(可能是jquery-1.10.2.min.map、jquery.min.map或jquery-2.0.3.min.map,但任何事情都可能发生)首先要知道的是,这仅在使用DevTools时才会请求。您的用户不会遇到此404。现在您可以修复此问题或禁用sourcemap功能。修复:获取文

随机推荐