衡阳派盒市场营销有限公司

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

解析Golang定時任務庫gron設計和原理

Linux愛好者 ? 來源:Linux愛好者 ? 作者:Linux愛好者 ? 2022-12-15 13:57 ? 次閱讀

從 cron 說起

在 Unix-like 操作系統中,有一個大家都很熟悉的 cli 工具,它能夠來處理定時任務,周期性任務,這就是: cron。 你只需要簡單的語法控制就能實現任意【定時】的語義。用法上可以參考一下這個 Crontab Guru Editor[1],做的非常精巧。

cb04c2ac-7c3c-11ed-8abf-dac502259ad0.jpg

簡單說,每一個位都代表了一個時間維度,* 代表全集,所以,上面的語義是:在每天早上的4點05分觸發任務。

但 cron 畢竟只是一個操作系統級別的工具,如果定時任務失敗了,或者壓根沒啟動,cron 是沒法提醒開發者這一點的。并且,cron 和 正則表達式都有一種魔力,不知道大家是否感同身受,這里引用同事的一句名言:

這世界上有些語言非常相似: shell腳本, es查詢的那個dsl語言, 定時任務的crontab, 正則表達式. 他們相似就相似在每次要寫的時候基本都得重新現學一遍。

正巧,最近看到了 gron 這個開源項目,它是用 Golang 實現一個并發安全的定時任務庫。實現非常簡單精巧,代碼量也不多。今天我們就來一起結合源碼看一下,怎樣基于 Golang 的能力做出來一個【定時任務庫】。

gron

Gron provides a clear syntax for writing and deploying cron jobs.

gron[2] 是一個泰國小哥在 2016 年開源的作品,它的特點就在于非常簡單和清晰的語義來定義【定時任務】,你不用再去記 cron 的語法。我們來看下作為使用者怎樣上手。

首先,我們還是一個 go get 安裝依賴:

$gogetgithub.com/roylee0704/gron

假設我們期望在【時機】到了以后,要做的工作是打印一個字符串,每一個小時執行一次,我們就可以這樣:

packagemain

import(
"fmt"
"time"
"github.com/roylee0704/gron"
)

funcmain(){
c:=gron.New()
c.AddFunc(gron.Every(1*time.Hour),func(){
fmt.Println("runseveryhour.")
})
c.Start()
}

非常簡單,而且即便是在 c.Start 之后我們依然可以添加新的定時任務進去。支持了很好的擴展性。

定時參數

注意到我們調用 gron.New().AddFunc() 時傳入了一個 gron.Every(1*time.Hour)

這里其實你可以傳入任何一個 time.Duration,從而把調度間隔從 1 小時調整到 1 分鐘甚至 1 秒。

除此之外,gron 還很貼心地封裝了一個 xtime 包用來把常見的 time.Duration 封裝起來,這里我們開箱即用。

import"github.com/roylee0704/gron/xtime"

gron.Every(1*xtime.Day)
gron.Every(1*xtime.Week)

很多時候我們不僅僅某個任務在當天運行,還希望是我們指定的時刻,而不是依賴程序啟動時間,機械地加 24 hour。gron 對此也做了很好的支持:

gron.Every(30*xtime.Day).At("00:00")
gron.Every(1*xtime.Week).At("23:59")

我們只需指定 At("hh:mm") 就可以實現在指定時間執行。

源碼解析

這一節我們來看看 gron 的實現原理。

所謂定時任務,其實包含兩個層面:

  1. 觸發器。即我們希望這個任務在什么時間點,什么周期被觸發;

  2. 任務。即我們在觸發之后,希望執行的任務,類比到我們上面示例的 fmt.Println。

對這兩個概念的封裝和擴展是一個定時任務庫必須考慮的。

而同時,我們是在 Golang 的協程上跑程序的,意味著這會是一個長期運行的協程,否則你即便指定了【一個月后干XXX】這個任務,程序兩天后掛了,也就無法實現你的訴求了。

所以,我們還希望有一個 manager 的角色,來管理我們的一組【定時任務】,如何調度,什么時候啟動,怎么停止,啟動了以后還想加新任務是否支持。

Cron

在 gron 的體系里,Cron 對象(我們上面通過 gron.New 創建出來的)就是我們的 manager,而底層的一個個【定時任務】則對應到 Cron 對象中的一個個 Entry:

//Cronprovidesaconvenientinterfaceforschedulingjobsuchastoclean-up
//databaseentryeverymonth.
//
//Cronkeepstrackofanynumberofentries,invokingtheassociatedfuncas
//specifiedbytheschedule.Itmayalsobestarted,stoppedandtheentries
//maybeinspected.
typeCronstruct{
entries[]*Entry
runningbool
addchan*Entry
stopchanstruct{}
}

//NewinstantiatesnewCroninstantc.
funcNew()*Cron{
return&Cron{
stop:make(chanstruct{}),
add:make(chan*Entry),
}
}
  • entries 就是定時任務的核心能力,它記錄了一組【定時任務】;

  • running 用來標識這個 Cron 是否已經啟動;

  • add 是一個channel,用來支持在 Cron 啟動后,新增的【定時任務】;

  • stop 同樣是個channel,注意到是空結構體,用來控制 Cron 的停止。這個其實是經典寫法了,對日常開發也有借鑒意義,我們待會兒會好好看一下。

我們觀察到,當調用 gron.New() 方法后,得到的是一個指向 Cron 對象的指針。此時只是初始化了 stop 和 add 兩個 channel,沒有啟動調度。

Entry

重頭戲來了,Cron 里面的 []* Entry 其實就代表了一組【定時任務】,每個【定時任務】可以簡化理解為 <觸發器,任務> 組成的一個 tuple。

//Entryconsistsofascheduleandthejobtobeexecutedonthatschedule.
typeEntrystruct{
ScheduleSchedule
JobJob

//thenexttimethejobwillrun.ThisiszerotimeifCronhasnotbeen
//startedorinvalidschedule.
Nexttime.Time

//thelasttimethejobwasrun.Thisiszerotimeifthejobhasnotbeen
//run.
Prevtime.Time
}

//ScheduleistheinterfacethatwrapsthebasicNextmethod.
//
//Nextdeducesnextoccurringtimebasedontandunderlyingstates.
typeScheduleinterface{
Next(ttime.Time)time.Time
}

//JobistheinterfacethatwrapsthebasicRunmethod.
//
//Runexecutestheunderlyingfunc.
typeJobinterface{
Run()
}
  • Schedule 代表了一個【觸發器】,或者說一個定時策略。它只包含一個 Next 方法,接受一個時間點,業務要返回下一次觸發調動的時間點。

  • Job 則是對【任務】的抽象,只需要實現一個 Run 方法,沒有入參出參。

除了這兩個核心依賴外,Entry 結構還包含了【前一次執行時間點】和【下一次執行時間點】,這個目前可以忽略,只是為了輔助代碼用。

按照時間排序

//byTimeisahandywrappertochronologicallysortentries.
typebyTime[]*Entry

func(bbyTime)Len()int{returnlen(b)}
func(bbyTime)Swap(i,jint){b[i],b[j]=b[j],b[i]}

//Lessreports`earliest`timeishouldsortbeforej.
//zerotimeisnot`earliest`time.
func(bbyTime)Less(i,jint)bool{

ifb[i].Next.IsZero(){
returnfalse
}
ifb[j].Next.IsZero(){
returntrue
}

returnb[i].Next.Before(b[j].Next)
}

這里是對 Entry 列表的簡單封裝,因為我們可能同時有多個 Entry 需要調度,處理的順序很重要。這里實現了 sort 的接口, 有了 Len(), Swap(), Less() 我們就可以用 sort.Sort() 來排序了。

此處的排序策略是按照時間大小。

新增定時任務

我們在示例里面出現過調用 AddFunc() 來加入一個 gron.Every(xxx) 這樣一個【定時任務】。其實這是給用戶提供的簡單封裝。

//JobFuncisanadaptertoallowtheuseofordinaryfunctionsasgron.Job
//Iffisafunctionwiththeappropriatesignature,JobFunc(f)isahandler
//thatcallsf.
//
//todo:possiblyfuncwithparams?maybenotneeded.
typeJobFuncfunc()

//Runcallsj()
func(jJobFunc)Run(){
j()
}


//AddFuncregisterstheJobfunctionforthegivenSchedule.
func(c*Cron)AddFunc(sSchedule,jfunc()){
c.Add(s,JobFunc(j))
}

//Addappendsschedule,jobtoentries.
//
//ifcroninstantisnotrunning,addingtoentriesistrivial.
//otherwise,topreventdata-race,addsthroughchannel.
func(c*Cron)Add(sSchedule,jJob){

entry:=&Entry{
Schedule:s,
Job:j,
}

if!c.running{
c.entries=append(c.entries,entry)
return
}
c.add<-?entry
}

JobFunc 實現了我們上一節提到的 Job 接口,基于此,我們就可以讓用戶直接傳入一個 func() 就ok,內部轉成 JobFunc,再利用通用的 Add 方法將其加入到 Cron 中即可。

注意,這里的 Add 方法就是新增定時任務的核心能力了,我們需要觸發器 Schedule,任務 Job。并以此來構造出一個定時任務 Entry。

若 Cron 實例還沒啟動,加入到 Cron 的 entries 列表里就ok,隨后啟動的時候會處理。但如果已經啟動了,就直接往 add 這個 channel 中塞,走額外的新增調度路徑。

啟動和停止

//Startsignalscroninstantctogetupandrunning.
func(c*Cron)Start(){
c.running=true
goc.run()
}


//Stophaltscroninstantcfromrunning.
func(c*Cron)Stop(){

if!c.running{
return
}
c.running=false
c.stop<-?struct{}{}
}

我們先 high level 地看一下一個 Cron 的啟動和停止。

  • Start 方法執行的時候會先將 running 變量置為 true,用來標識實例已經啟動(啟動前后加入的定時任務 Entry 處理策略是不同的,所以這里需要標識),然后啟動一個 goroutine 來實際跑啟動的邏輯。

  • Stop 方法則會將 running 置為 false,然后直接往 stop channel 塞一個空結構體即可。

ok,有了這個心里預期,我們來看看 c.run() 里面干了什么事:

varafter=time.After


//runthescheduler...
//
//Itneedstobeprivateasit'sresponsibleofsynchronizingacritical
//sharedstate:`running`.
func(c*Cron)run(){

vareffectivetime.Time
now:=time.Now().Local()

//tofigurenexttrigtimeforentries,referencedfromnow
for_,e:=rangec.entries{
e.Next=e.Schedule.Next(now)
}

for{
sort.Sort(byTime(c.entries))
iflen(c.entries)>0{
effective=c.entries[0].Next
}else{
effective=now.AddDate(15,0,0)//topreventphantomjobs.
}

select{
casenow=<-after(effective.Sub(now)):
???//entrieswithsametimegetsrun.
for_,entry:=rangec.entries{
ifentry.Next!=effective{
break
}
entry.Prev=now
entry.Next=entry.Schedule.Next(now)
goentry.Job.Run()
}
casee:=<-c.add:
???e.Next?=?e.Schedule.Next(time.Now())
???c.entries?=?append(c.entries,e)
case<-c.stop:
???return//terminatego-routine.
}
}
}

重點來了,看看我們是如何把上面 Cron, Entry, Schedule, Job 串起來的。

  • 首先拿到 local 的時間 now;
  • 遍歷所有 Entry,調用 Next 方法拿到各個【定時任務】下一次運行的時間點;
  • 對所有 Entry 按照時間排序(我們上面提過的 byTime);
  • 拿到第一個要到期的時間點,在 select 里面通過 time.After 來監聽。到點了就起動新的 goroutine 跑對應 entry 里的 Job,并回到 for 循環,繼續重新 sort,再走同樣的流程;
  • 若 add channel 里有新的 Entry 被加進來,就加入到 Cron 的 entries 里,觸發新的 sort;
  • 若 stop channel 收到了信號,就直接 return,結束執行。

整體實現還是非常簡潔的,大家可以感受一下。

Schedule

前面其實我們暫時將觸發器的復雜性封裝在 Schedule 接口中了,但怎么樣實現一個 Schedule 呢?

尤其是注意,我們還支持 At 操作,也就是指定 Day,和具體的小時,分鐘。回憶一下:

gron.Every(30*xtime.Day).At("00:00")
gron.Every(1*xtime.Week).At("23:59")

這一節我們就來看看,gron.Every 干了什么事,又是如何支持 At 方法的。

//EveryreturnsaSchedulereoccurseveryperiodp,pmustbeatleast
//time.Second.
funcEvery(ptime.Duration)AtSchedule{

ifp//truncatesuptoseconds

return&periodicSchedule{
period:p,
}
}

gron 的 Every 函數接受一個 time.Duration,返回了一個 AtSchedule 接口。我待會兒會看,這里注意,Every 里面是會把【秒】級以下給截掉。

我們先來看下,最后返回的這個 periodicSchedule 是什么:

typeperiodicSchedulestruct{
periodtime.Duration
}

//Nextaddstimettounderlyingperiod,truncatesuptounitofseconds.
func(psperiodicSchedule)Next(ttime.Time)time.Time{
returnt.Truncate(time.Second).Add(ps.period)
}

//Atreturnsaschedulewhichreoccurseveryperiodp,attimet(hh:ss).
//
//Note:Atpanicswhenperiodpislessthanxtime.Day,anderrorhh:ssformat.
func(psperiodicSchedule)At(tstring)Schedule{
ifps.periodpanic("periodmustbeatleastindays")
}

//parsetnaively
h,m,err:=parse(t)

iferr!=nil{
panic(err.Error())
}

return&atSchedule{
period:ps.period,
hh:h,
mm:m,
}
}

//parsenaivelytokeniseshoursandminutes.
//
//returnserrorwheninputformatwasincorrect.
funcparse(hhmmstring)(hhint,mmint,errerror){

hh=int(hhmm[0]-'0')*10+int(hhmm[1]-'0')
mm=int(hhmm[3]-'0')*10+int(hhmm[4]-'0')

ifhh0||hh>24{
hh,mm=0,0
err=errors.New("invalidhhformat")
}
ifmm0||mm>59{
hh,mm=0,0
err=errors.New("invalidmmformat")
}

return
}

可以看到,所謂 periodicSchedule 就是一個【周期性觸發器】,只維護一個 time.Duration 作為【周期】。

periodicSchedule 實現 Next 的方式也很簡單,把秒以下的截掉之后,直接 Add(period),把周期加到當前的 time.Time 上,返回新的時間點。這個大家都能想到。

重點在于,對 At 能力的支持。我們來關注下 func (ps periodicSchedule) At(t string) Schedule 這個方法

  • 若周期連 1 天都不到,不支持 At 能力,因為 At 本質是在選定的一天內,指定小時,分鐘,作為輔助。連一天都不到的周期,是要精準處理的;

  • 將用戶輸入的形如 "23:59" 時間字符串解析出來【小時】和【分鐘】;

  • 構建出一個 atSchedule 對象,包含了【周期時長】,【小時】,【分鐘】。

ok,這一步只是拿到了材料,那具體怎樣處理呢?這個還是得繼續往下走,看看 atSchedule 結構干了什么:

typeatSchedulestruct{
periodtime.Duration
hhint
mmint
}

//resetreturnsnewDatebasedontimeinstantt,andreconfigureitshh:ss
//accordingtoatSchedule'shh:ss.
func(asatSchedule)reset(ttime.Time)time.Time{
returntime.Date(t.Year(),t.Month(),t.Day(),as.hh,as.mm,0,0,time.UTC)
}

//Nextreturns**next**time.
//iftpasseditssupposedschedule:reset(t),returnsreset(t)+period,
//elsereturnsreset(t).
func(asatSchedule)Next(ttime.Time)time.Time{
next:=as.reset(t)
ift.After(next){
returnnext.Add(as.period)
}
returnnext
}

其實只看這個 Next 的實現即可。我們從 periodSchedule 那里獲取了三個屬性。

在調用 Next 方法時,先做 reset,根據原有 time.Time 的年,月,日,以及用戶輸入的 At 中的小時,分鐘,來構建出來一個 time.Time 作為新的時間點。

此后判斷是在哪個周期,如果當前周期已經過了,那就按照下個周期的時間點返回。

到這里,一切就都清楚了,如果我們不用 At 能力,直接 gron.Every(xxx),那么直接就會調用

t.Truncate(time.Second).Add(ps.period)

拿到一個新的時間點返回。

而如果我們要用 At 能力,指定當天的小時,分鐘。那就會走到 periodicSchedule.At 這里,解析出【小時】和【分鐘】,最后走 Next 返回 reset 之后的時間點。

這個和 gron.Every 方法返回的 AtSchedule 接口其實是完全對應的:

//AtScheduleextendsSchedulebyenablingperiodic-interval&time-specificsetup
typeAtScheduleinterface{
At(tstring)Schedule
Schedule
}

直接就有一個 Schedule 可以用,但如果你想針對天級以上的 duration 指定時間,也可以走 At 方法,也會返回一個 Schedule 供我們使用。

擴展性

gron 里面對于所有的依賴也都做成了【依賴接口而不是實現】。Cron 的 Add 函數的入參也是兩個接口,這里可以隨意替換:func (c *Cron) Add(s Schedule, j Job)

最核心的兩個實體依賴 Schedule, Job 都可以用你自定義的實現來替換掉。

如實現一個新的 Job:

typeReminderstruct{
Msgstring
}

func(rReminder)Run(){
fmt.Println(r.Msg)
}

事實上,我們上面提到的 periodicSchedule 以及 atSchedule 就是 Schedule 接口的具體實現。我們也完全可以不用 gron.Every,而是自己寫一套新的 Schedule 實現。只要實現 Next(p time.Duration) time.Time 即可。

我們來看一個完整用法案例:

packagemain

import(
"fmt"
"github.com/roylee0704/gron"
"github.com/roylee0704/gron/xtime"
)

typePrintJobstruct{Msgstring}

func(pPrintJob)Run(){
fmt.Println(p.Msg)
}

funcmain(){

var(
//schedules
daily=gron.Every(1*xtime.Day)
weekly=gron.Every(1*xtime.Week)
monthly=gron.Every(30*xtime.Day)
yearly=gron.Every(365*xtime.Day)

//contrivedjobs
purgeTask=func(){fmt.Println("purgeagedrecords")}
printFoo=printJob{"Foo"}
printBar=printJob{"Bar"}
)

c:=gron.New()

c.Add(daily.At("12:30"),printFoo)
c.AddFunc(weekly,func(){fmt.Println("Everyweek")})
c.Start()

//JobsmayalsobeaddedtoarunningGron
c.Add(monthly,printBar)
c.AddFunc(yearly,purgeTask)

//StopGron(runningjobsarenothalted).
c.Stop()
}

經典寫法-控制退出

這里我們還是要聊一下 Cron 里控制退出的經典寫法。我們把其他不相關的部分清理掉,只留下核心代碼:

typeCronstruct{
stopchanstruct{}
}

func(c*Cron)Stop(){
c.stop<-?struct{}{}
}

func(c*Cron)run(){

for{
select{
case<-c.stop:
???return//terminatego-routine.
}
}
}

空結構體能夠最大限度節省內存,畢竟我們只是需要一個信號。核心邏輯用 for + select 的配合,這樣當我們需要結束時可以立刻響應。非常經典,建議大家日常有需要的時候采用。

結語

gron 整體代碼其實只在 cron.go 和 schedule.go 兩個文件,合起來代碼不過 300 行,非常精巧,基本沒有冗余,擴展性很好,是非常好的入門材料。

不過,作為一個 cron 的替代品,其實 gron 還是有自己的問題的。簡單講就是,如果我重啟了一個EC2實例,那么我的 cron job 其實也還會繼續執行,這是落盤的,操作系統級別的支持。

但如果我執行 gron 的進程掛掉了,不好意思,那就完全涼了。你只有重啟,然后再把所有任務加回來才行。而我們既然要用 gron,是很有可能定一個幾天后,幾個星期后,幾個月后這樣的觸發器的。誰能保證進程一直活著呢?連機子本身都可能重啟。

所以,我們需要一定的機制來保證 gron 任務的可恢復性,將任務落盤,持久化狀態信息,算是個思考題,這里大家可以考慮一下怎么做。

審核編輯 :李倩



聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 操作系統
    +關注

    關注

    37

    文章

    6896

    瀏覽量

    123755
  • 代碼
    +關注

    關注

    30

    文章

    4828

    瀏覽量

    69060

原文標題:解析 Golang 定時任務庫 gron 設計和原理

文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    Linux計劃任務介紹

    定時備份數據。比如:11點開啟網站搶購接口,12點關閉網站搶購接口。 3.計劃任務主要分為以下兩種使用情況: 1.系統級別的定時任務: 臨時文件清理、系統信息采集、日志文件切割?2.用戶級別的
    的頭像 發表于 11-24 15:49 ?353次閱讀

    Golang配置代理方法

    由于一些客觀原因的存在,我們開發 Golang 項目的過程總會碰到無法下載某些依賴包的問題。這不是一個小問題,因為你的工作會被打斷,即便你使用各種神通解決了問題,很可能這時你的線程已經切換到其他的事情上了(痛恨思路被打斷!)。所以最好是一開始我們就重視這個問題,并一勞永逸的解決它。
    的頭像 發表于 11-11 11:17 ?403次閱讀
    <b class='flag-5'>Golang</b>配置代理方法

    定時器技術:Air780E如何革新定時任務管理?

    今天講的是關于Air780E如何革新定時任務管理的內容,希望大家有所收獲。
    的頭像 發表于 11-07 13:50 ?315次閱讀
    <b class='flag-5'>定時</b>器技術:Air780E如何革新<b class='flag-5'>定時任務</b>管理?

    mysql定時備份任務

    在生產環境上,為了避免數據的丟失,通常情況下都會定時的對數據進行備份。而Linux的crontab指令則可以幫助我們實現對數據定時進行備份。首先我們來簡單了解crontab指令,如
    的頭像 發表于 10-31 10:07 ?215次閱讀

    Python解析:通過實現代理請求與數據抓取

    在Python中,有多個可以幫助你實現代理請求和數據抓取。這些提供了豐富的功能和靈活的API,使得你可以輕松地發送HTTP請求、處理響應、解析HTML/XML/JSON數據,以及進行復雜的網絡操作。
    的頭像 發表于 10-24 07:54 ?231次閱讀

    陀螺儀LSM6DSOW開發(5)----MotionFX解析空間坐標

    和配置MotionFX,使用FIFO讀取傳感器數據,FIFO可以作為數據緩沖區,存儲傳感器的臨時數據。這樣可以防止數據丟失,特別是在處理器忙于其他任務時,并利用這些數據進行空間坐標的解析。本章案例使用上節的demo進行修改。
    的頭像 發表于 08-15 18:13 ?1773次閱讀
    陀螺儀LSM6DSOW開發(5)----MotionFX<b class='flag-5'>庫</b><b class='flag-5'>解析</b>空間坐標

    linux定時任務的用法總結

    習慣了使用 windows 的計劃任務,使用 linux 中的 crontab 管理定時任務時很不適應。
    的頭像 發表于 08-14 18:16 ?904次閱讀
    linux<b class='flag-5'>定時任務</b>的用法總結

    ESP8266如何實現時間小于3us的定時任務

    想實現一個穩定的軟串口,現有的軟串口程序是通過中斷實現的,但中斷好像會被其他中斷打斷,導致數據丟失,定時器按文檔上的說法,只能大于50us,能不能實現時間小于3us的定時任務或者提高GPIO中斷的優先級呢?或者還有其他什么辦法?
    發表于 07-19 06:13

    陀螺儀LSM6DSV16X與AI集成(8)----MotionFX解析空間坐標

    和配置MotionFX,使用FIFO讀取傳感器數據,FIFO可以作為數據緩沖區,存儲傳感器的臨時數據。這樣可以防止數據丟失,特別是在處理器忙于其他任務時,并利用這些數據進行空間坐標的解析。本章案例使用上節的demo進行修改。
    的頭像 發表于 07-18 10:43 ?1283次閱讀
    陀螺儀LSM6DSV16X與AI集成(8)----MotionFX<b class='flag-5'>庫</b><b class='flag-5'>解析</b>空間坐標

    智能插座“云”時代:定時任務與事件驅動的創新管理

    用戶可以通過云端界面,在任何時間任何地點對插座進行配置和監控,同時收集數據和洞察分析,以促進能效最優化。無論是確保家中的咖啡機在你醒來之前準備好早晨的咖啡,還是遠程調整辦公室的溫度設置以節約能源,智能插座配合云管理打開了便捷與高效的大門。
    的頭像 發表于 07-15 18:16 ?1067次閱讀
    智能插座“云”時代:<b class='flag-5'>定時任務</b>與事件驅動的創新管理

    定時器的工作方式介紹

    或實現周期性事件的硬件模塊。它可以用于實現各種定時任務,如定時中斷、PWM(脈沖寬度調制)輸出、頻率測量等。定時器通常由一個計數器、一個時鐘源和一個控制寄存器組成。 1.1 定時器的分
    的頭像 發表于 07-12 10:29 ?1128次閱讀

    長持續時間定時器電路圖 時間定時器的工作原理和功能

    的處理,都離不開定時器的精確控制。時間定時器通常由硬件和軟件兩部分組成,硬件部分通過計時器芯片或計數器來實現時間的度量和計算,而軟件部分則是通過編程語言提供的函數或類來設置和處理定時任務
    的頭像 發表于 06-24 17:34 ?2367次閱讀
    長持續時間<b class='flag-5'>定時</b>器電路圖 時間<b class='flag-5'>定時</b>器的工作原理和功能

    在物通博聯工業智能網關的本地配置界面(WEB)直接配置定時控制任務

    開關,可實現全年定時任務自動執行。在多個任務日期重疊時,可選執行高等級的還是并行執行。設有一個遠程和本地的控制點,默認為本地狀態,網關自己執行設置的任務,當用戶將改控制點切換為遠程時可進行遠程一鍵開關或者指定
    的頭像 發表于 04-24 17:21 ?593次閱讀
    在物通博聯工業智能網關的本地配置界面(WEB)直接配置<b class='flag-5'>定時</b>控制<b class='flag-5'>任務</b>

    【米爾-全志T113-i開發板試用】3、使用golang獲取系統信息

    獲取系統信息,我們可以使用Linux的Proc文件系統,解析其中的文件來取得相應的信息,但是那樣做太麻煩了,需要找很多資料,寫挺多的代碼。 我們可以使用現成的工具gopsutil。gopsutil
    發表于 02-22 09:39

    ArkTS語言基礎類-解析

    多線程并發,支持Worker線程和宿主線程之間進行通信,開發者需要主動創建和關閉Worker線程。 提供常見的[容器類增、刪、改、查]的能力。 提供XML、URL、URI構造和解析的能力。 XML
    發表于 02-20 16:44
    百家乐官网押注方法| 大发888优惠红利代码| 百家乐网络视频游戏| 八卦24山| 游戏机百家乐官网的玩法技巧和规则 | 太阳城金旭园| 大发888作弊| 大发888任务| 一起pk棋牌游戏| 大发888注册58| 沙龙娱乐| 伟德亚洲娱乐城| 百家乐官网系统分析器| 百家乐官网机器手怎么做弊| 伽师县| 百家乐官网的玩法和技巧| 网上百家乐官网打牌| 网上百家乐官网娱乐场开户注册 | 太阳城百家乐官网娱乐官方网| 金银岛百家乐官网的玩法技巧和规则 | 百家乐平台送彩金| 大发888二十一点| 新澳门娱乐城官网| 色中色最新网址| 视频百家乐官网游戏| 做生意什么花风水好| 发中发百家乐的玩法技巧和规则| 星河娱乐城| 百家乐官网游戏机技| 马尼拉百家乐官网的玩法技巧和规则 | 澳门百家乐官网真人斗地主| 网络百家乐官网会输钱的多吗| 百家乐怎么做弊| 株洲县| 稳赢的百家乐官网投注方法| 百家乐加牌规则| 大发888真人娱乐场| 筠连县| 百家乐网上赌场| 百家乐官网波音平台有假吗| 百家乐伴侣|