日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

c语言定时器作用,Go语言定时器实现原理及作用

發(fā)布時(shí)間:2023/12/2 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 c语言定时器作用,Go语言定时器实现原理及作用 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

對(duì)于任何一個(gè)正在運(yùn)行的應(yīng)用,如何獲取準(zhǔn)確的絕對(duì)時(shí)間都非常重要,但是在一個(gè)分布式系統(tǒng)中我們很難保證各個(gè)節(jié)點(diǎn)上絕對(duì)時(shí)間的一致性,哪怕通過(guò) NTP 這種標(biāo)準(zhǔn)的對(duì)時(shí)協(xié)議也只能把時(shí)間的誤差控制在毫秒級(jí),所以相對(duì)時(shí)間在一個(gè)分布式系統(tǒng)中顯得更為重要,在接下來(lái)的講解中我們將會(huì)介紹一下Go語(yǔ)言中的定時(shí)器以及它在并發(fā)編程中起到什么樣的作用。

絕對(duì)時(shí)間一定不會(huì)是完全準(zhǔn)確的,它對(duì)于一個(gè)運(yùn)行中的分布式系統(tǒng)其實(shí)沒(méi)有太多指導(dǎo)意義,但是由于相對(duì)時(shí)間的計(jì)算不依賴于外部的系統(tǒng),所以它的計(jì)算可以做的比較準(zhǔn)確,首先介紹一下Go語(yǔ)言中用于計(jì)算相對(duì)時(shí)間的定時(shí)器的實(shí)現(xiàn)原理。

結(jié)構(gòu)

timer 就是Go語(yǔ)言定時(shí)器的內(nèi)部表示,每一個(gè) timer 其實(shí)都存儲(chǔ)在堆中,tb 就是用于存儲(chǔ)當(dāng)前定時(shí)器的桶,而 i 是當(dāng)前定時(shí)器在堆中的索引,我們可以通過(guò)這兩個(gè)變量找到當(dāng)前定時(shí)器在堆中的位置:

type timer struct {

tb *timersBucket

i? int

when?? int64

period int64

f????? func(interface{}, uintptr)

arg??? interface{}

seq??? uintptr

}

when 表示當(dāng)前定時(shí)器(Timer)被喚醒的時(shí)間,而 period 表示兩次被喚醒的間隔,每當(dāng)定時(shí)器被喚醒時(shí)都會(huì)調(diào)用 f(args, now) 函數(shù)并傳入 args 和當(dāng)前時(shí)間作為參數(shù)。

然而這里的 timer 作為一個(gè)私有結(jié)構(gòu)體其實(shí)只是定時(shí)器的運(yùn)行時(shí)表示,time 包對(duì)外暴露的定時(shí)器使用了如下所示的結(jié)構(gòu)體:

type Timer struct {

C

r runtimeTimer

}

Timer 定時(shí)器必須通過(guò) NewTimer 或者 AfterFunc 函數(shù)進(jìn)行創(chuàng)建,其中的 runtimeTimer 其實(shí)就是上面介紹的 timer 結(jié)構(gòu)體,當(dāng)定時(shí)器失效時(shí),失效的時(shí)間就會(huì)被發(fā)送給當(dāng)前定時(shí)器持有的 Channel C,訂閱管道中消息的 Goroutine 就會(huì)收到當(dāng)前定時(shí)器失效的時(shí)間。

在 time 包中,除了 timer 和 Timer 兩個(gè)分別用于表示運(yùn)行時(shí)定時(shí)器和對(duì)外暴露的 API 之外,timersBucket 這個(gè)用于存儲(chǔ)定時(shí)器的結(jié)構(gòu)體也非常重要,它會(huì)存儲(chǔ)一個(gè)處理器上的全部定時(shí)器,不過(guò)如果當(dāng)前機(jī)器的核數(shù)超過(guò)了 64 核,也就是機(jī)器上的處理器 P 的個(gè)數(shù)超過(guò)了 64 個(gè),多個(gè)處理器上的定時(shí)器就可能存儲(chǔ)在同一個(gè)桶中:

type timersBucket struct {

lock???????? mutex

gp?????????? *g

created????? bool

sleeping???? bool

rescheduling bool

sleepUntil?? int64

waitnote???? note

t??????????? []*timer

}

每一個(gè) timersBucket 中的 t 就是用于存儲(chǔ)定時(shí)器指針的切片,每一個(gè)運(yùn)行的Go語(yǔ)言程序都會(huì)在內(nèi)存中存儲(chǔ)著 64 個(gè)桶,這些桶中都存儲(chǔ)定時(shí)器的信息:

每一個(gè)桶持有的 timer 切片其實(shí)都是一個(gè)最小堆,這個(gè)最小堆會(huì)按照 timer 應(yīng)該觸發(fā)的時(shí)間對(duì)它們進(jìn)行排序,最小堆最上面的定時(shí)器就是最近需要被喚醒的 timer,下面來(lái)介紹下定時(shí)器的創(chuàng)建和觸發(fā)過(guò)程。

工作原理

既然我們已經(jīng)介紹了定時(shí)器的數(shù)據(jù)結(jié)構(gòu),接下來(lái)我們就可以開始分析它的常見(jiàn)操作以及工作原理了,在這一節(jié)中我們將介紹定時(shí)器的創(chuàng)建、觸發(fā)、time.Sleep 與定時(shí)器的關(guān)系以及計(jì)時(shí)器 Ticker 的實(shí)現(xiàn)原理。

創(chuàng)建

time 包對(duì)外提供了兩種創(chuàng)建定時(shí)器的方法,第一種方法就是 NewTimer 接口,這個(gè)接口會(huì)創(chuàng)建一個(gè)用于通知觸發(fā)時(shí)間的 Channel、調(diào)用 startTimer 方法并返回一個(gè)創(chuàng)建指向 Timer 結(jié)構(gòu)體的指針:

func NewTimer(d Duration) *Timer {

c := make(chan Time, 1)

t := &Timer{

C: c,

r: runtimeTimer{

when: when(d),

f: sendTime,

arg: c,

},

}

startTimer(&t.r)

return t

}

另一個(gè)用于創(chuàng)建 Timer 的方法 AfterFunc 其實(shí)也提供了非常相似的結(jié)構(gòu),與 NewTimer 方法不同的是該方法沒(méi)有創(chuàng)建一個(gè)用于通知觸發(fā)時(shí)間的 Channel,它只會(huì)在定時(shí)器到期時(shí)調(diào)用傳入的方法:

func AfterFunc(d Duration, f func()) *Timer {

t := &Timer{

r: runtimeTimer{

when: when(d),

f: goFunc,

arg: f,

},

}

startTimer(&t.r)

return t

}

startTimer 基本上就是創(chuàng)建定時(shí)器的入口了,所有定時(shí)器的創(chuàng)建和重啟基本上都需要調(diào)用該函數(shù):

func startTimer(t *timer) {

addtimer(t)

}

func addtimer(t *timer) {

tb := t.assignBucket()

tb.addtimerLocked(t)

}

它會(huì)調(diào)用 addtimer 函數(shù),這個(gè)函數(shù)總共做了兩件事情,首先通過(guò) assignBucket 方法為當(dāng)前定時(shí)器選擇一個(gè) timersBucket 桶,我們會(huì)根據(jù)當(dāng)前 Goroutine 所在處理器 P 的 id 選擇一個(gè)合適的桶,隨后調(diào)用 addtimerLocked 方法將當(dāng)前定時(shí)器加入桶中:

func (tb *timersBucket) addtimerLocked(t *timer) bool {

t.i = len(tb.t)

tb.t = append(tb.t, t)

if !siftupTimer(tb.t, t.i) {

return false

}

if t.i == 0 {

if tb.sleeping && tb.sleepUntil > t.when {

tb.sleeping = false

notewakeup(&tb.waitnote)

}

if tb.rescheduling {

tb.rescheduling = false

goready(tb.gp, 0)

}

if !tb.created {

tb.created = true

go timerproc(tb)

}

}

return true

}

addtimerLocked 會(huì)先將最新加入的定時(shí)器加到隊(duì)列的末尾,隨后調(diào)用 siftupTimer 將當(dāng)前定時(shí)器與四叉樹(或者四叉堆)中的父節(jié)點(diǎn)進(jìn)行比較,保證父節(jié)點(diǎn)的到期時(shí)間一定小于子節(jié)點(diǎn):

這個(gè)四叉樹只能保證父節(jié)點(diǎn)的到期時(shí)間大于子節(jié)點(diǎn),這對(duì)于我們來(lái)說(shuō)其實(shí)也足夠了,因?yàn)槲覀冎魂P(guān)心即將被觸發(fā)的計(jì)數(shù)器,如果當(dāng)前定時(shí)器是第一個(gè)被加入四叉樹的定時(shí)器,我們還會(huì)通過(guò) go timerproc(tb) 啟動(dòng)一個(gè) Goroutine 用于處理當(dāng)前樹中的定時(shí)器,這也是處理定時(shí)器的核心方法。

觸發(fā)

定時(shí)器的觸發(fā)都是由 timerproc 中的一個(gè)雙層 for 循環(huán)控制的,外層的 for 循環(huán)主要負(fù)責(zé)對(duì)當(dāng)前 Goroutine 進(jìn)行控制,它不僅會(huì)負(fù)責(zé)鎖的獲取和釋放,還會(huì)在合適的時(shí)機(jī)觸發(fā)當(dāng)前 Goroutine 的休眠:

func timerproc(tb *timersBucket) {

tb.gp = getg()

for {

tb.sleeping = false

now := nanotime()

delta := int64(-1)

// inner loop

if delta < 0 {

tb.rescheduling = true

goparkunlock(&tb.lock, waitReasonTimerGoroutineIdle, traceEvGoBlock, 1)

continue

}

tb.sleeping = true

tb.sleepUntil = now + delta

noteclear(&tb.waitnote)

notetsleepg(&tb.waitnote, delta)

}

}

如果距離下一個(gè)定時(shí)器被喚醒的時(shí)間小于 0,當(dāng)前的 timerproc 就會(huì)將 rescheduling 標(biāo)記設(shè)置成 true 并立刻陷入休眠,這其實(shí)也意味著當(dāng)前 timerproc 中不包含任何待處理的定時(shí)器,當(dāng)我們?cè)傧蛟?timerBucket 加入定時(shí)器時(shí)就會(huì)重新喚醒 timerproc Goroutine。

在其他情況下,也就是下一次計(jì)數(shù)器的響應(yīng)時(shí)間是 now + delta 時(shí),timerproc 中的外層循環(huán)會(huì)通過(guò) notesleepg 將當(dāng)前 Goroutine 陷入休眠。

func notetsleepg(n *note, ns int64) bool {

gp := getg()

if gp == gp.m.g0 {

throw("notetsleepg on g0")

}

semacreate(gp.m)

entersyscallblock()

ok := notetsleep_internal(n, ns, nil, 0)

exitsyscall()

return ok

}

該函數(shù)會(huì)先獲取當(dāng)前的 Goroutine 并在當(dāng)前的 CPU 上創(chuàng)建一個(gè)信號(hào)量,隨后在 entersyscallblock 和 exitsyscall 之間執(zhí)行系統(tǒng)調(diào)用讓當(dāng)前的 Goroutine 陷入休眠并在 ns 納秒后返回。

內(nèi)部循環(huán)的主要作用就是觸發(fā)已經(jīng)到期的定時(shí)器,在這個(gè)內(nèi)部循環(huán)中,我們會(huì)按照以下的流程對(duì)當(dāng)前桶中的定時(shí)器進(jìn)行處理:

如果桶中不包含任何定時(shí)器就會(huì)直接返回并陷入休眠等待定時(shí)器加入當(dāng)前桶;

如果四叉樹最上面的定時(shí)器還沒(méi)有到期會(huì)通過(guò) notetsleepg 方法陷入休眠等待最近定時(shí)器的到期;

如果四叉樹最上面的定時(shí)器已經(jīng)到期;

當(dāng)定時(shí)器的 period > 0 就會(huì)設(shè)置下一次會(huì)觸發(fā)定時(shí)器的時(shí)間并將當(dāng)前定時(shí)器向下移動(dòng)到對(duì)應(yīng)的位置;

當(dāng)定時(shí)器的 period <= 0 就會(huì)將當(dāng)前定時(shí)器從四叉樹中移除;

在每次循環(huán)的最后都會(huì)從定時(shí)器中取出定時(shí)器中的函數(shù)、參數(shù)和序列號(hào)并調(diào)用函數(shù)觸發(fā)該計(jì)數(shù)器;

for {

if len(tb.t) == 0 {

delta = -1

break

}

t := tb.t[0]

delta = t.when - now

if delta > 0 {

break

}

ok := true

if t.period > 0 {

t.when += t.period * (1 + -delta/t.period)

if !siftdownTimer(tb.t, 0) {

ok = false

}

} else {

last := len(tb.t) - 1

if last > 0 {

tb.t[0] = tb.t[last]

tb.t[0].i = 0

}

tb.t[last] = nil

tb.t = tb.t[:last]

if last > 0 {

if !siftdownTimer(tb.t, 0) {

ok = false

}

}

t.i = -1 // mark as removed

}

f := t.f

arg := t.arg

seq := t.seq

f(arg, seq)

}

使用 NewTimer 創(chuàng)建的定時(shí)器,傳入的函數(shù)時(shí) sendTime,它會(huì)將當(dāng)前時(shí)間發(fā)送到定時(shí)器持有的 Channel 中,而使用 AfterFunc 創(chuàng)建的定時(shí)器,在內(nèi)層循環(huán)中調(diào)用的函數(shù)就會(huì)是調(diào)用方傳入的函數(shù)了。

休眠

如果你使用過(guò)一段時(shí)間的Go語(yǔ)言,一定在項(xiàng)目中使用過(guò) time 包中的 Sleep 方法讓當(dāng)前的 Goroutine 陷入休眠以等待某些條件的完成或者觸發(fā)一些定時(shí)任務(wù),time.Sleep 就是通過(guò)如下所示的 timeSleep 方法完成的:

func timeSleep(ns int64) {

if ns <= 0 {

return

}

gp := getg()

t := gp.timer

if t == nil {

t = new(timer)

gp.timer = t

}

*t = timer{}

t.when = nanotime() + ns

t.f = goroutineReady

t.arg = gp

tb := t.assignBucket()

lock(&tb.lock)

if !tb.addtimerLocked(t) {

unlock(&tb.lock)

badTimer()

}

goparkunlock(&tb.lock, waitReasonSleep, traceEvGoSleep, 2)

}

timeSleep 會(huì)創(chuàng)建一個(gè)新的 timer 結(jié)構(gòu)體,在初始化的過(guò)程中我們會(huì)傳入當(dāng)前 Goroutine 應(yīng)該被喚醒的時(shí)間以及喚醒時(shí)需要調(diào)用的函數(shù) goroutineReady,隨后會(huì)調(diào)用 goparkunlock 將當(dāng)前 Goroutine 陷入休眠狀態(tài),當(dāng)定時(shí)器到期時(shí)也會(huì)調(diào)用 goroutineReady 方法喚醒當(dāng)前的 Goroutine:

func goroutineReady(arg interface{}, seq uintptr) {

goready(arg.(*g), 0)

}

time.Sleep 方法其實(shí)只是創(chuàng)建了一個(gè)會(huì)在到期時(shí)喚醒當(dāng)前 Goroutine 的定時(shí)器并通過(guò) goparkunlock 將當(dāng)前的協(xié)程陷入休眠狀態(tài)等待定時(shí)器觸發(fā)的喚醒。

Ticker

除了只用于一次的定時(shí)器(Timer)之外,Go語(yǔ)言的 time 包中還提供了用于多次通知的 Ticker 計(jì)時(shí)器,計(jì)時(shí)器中包含了一個(gè)用于接受通知的 Channel 和一個(gè)定時(shí)器,這兩個(gè)字段共同組成了用于連續(xù)多次觸發(fā)事件的計(jì)時(shí)器:

type Ticker struct {

C

r runtimeTimer

}

想要在Go語(yǔ)言中創(chuàng)建一個(gè)計(jì)時(shí)器只有兩種方法,一種是使用 NewTicker 方法顯示地創(chuàng)建 Ticker 計(jì)時(shí)器指針,另一種可以直接通過(guò) Tick 方法獲取一個(gè)會(huì)定期發(fā)送消息的 Channel:

func NewTicker(d Duration) *Ticker {

if d <= 0 {

panic(errors.New("non-positive interval for NewTicker"))

}

c := make(chan Time, 1)

t := &Ticker{

C: c,

r: runtimeTimer{

when: when(d),

period: int64(d),

f: sendTime,

arg: c,

},

}

startTimer(&t.r)

return t

}

func Tick(d Duration)

if d <= 0 {

return nil

}

return NewTicker(d).C

}

Tick 其實(shí)也只是對(duì) NewTicker 的簡(jiǎn)單封裝,從實(shí)現(xiàn)上我們就能看出來(lái)它其實(shí)就是調(diào)用了 NewTicker 獲取了計(jì)時(shí)器并返回了計(jì)時(shí)器中 Channel,兩個(gè)創(chuàng)建計(jì)時(shí)器的方法的實(shí)現(xiàn)都并不復(fù)雜而且費(fèi)容易理解,所以在這里也就不詳細(xì)展開介紹了。

需要注意的是每一個(gè) NewTicker 方法開啟的計(jì)時(shí)器都需要在不需要使用時(shí)調(diào)用 Stop 進(jìn)行關(guān)閉,如果不顯示調(diào)用 Stop 方法,創(chuàng)建的計(jì)時(shí)器就沒(méi)有辦法被垃圾回收,而通過(guò) Tick 創(chuàng)建的計(jì)時(shí)器由于只對(duì)外提供了 Channel,所以是一定沒(méi)有辦法關(guān)閉的,我們一定要謹(jǐn)慎使用這一接口創(chuàng)建計(jì)時(shí)器。

性能分析

定時(shí)器在內(nèi)部使用四叉樹的方式進(jìn)行實(shí)現(xiàn)和存儲(chǔ),當(dāng)我們?cè)谏a(chǎn)環(huán)境中使用定時(shí)器進(jìn)行毫秒級(jí)別的計(jì)時(shí)時(shí),在高并發(fā)的場(chǎng)景下會(huì)有比較明顯的性能問(wèn)題,我們可以通過(guò)實(shí)驗(yàn)測(cè)試一下定時(shí)器在高并發(fā)時(shí)的性能,假設(shè)我們有以下的代碼:

func runTimers(count int) {

durationCh := make(chan time.Duration, count)

wg := sync.WaitGroup{}

wg.Add(count)

for i := 0; i < count; i++ {

go func() {

startedAt := time.Now()

time.AfterFunc(10*time.Millisecond, func() {

defer wg.Done()

durationCh

})

}()

}

wg.Wait()

close(durationCh)

durations := []time.Duration{}

totalDuration := 0 * time.Millisecond

for duration := range durationCh {

durations = append(durations, duration)

totalDuration += duration

}

averageDuration := totalDuration / time.Duration(count)

sort.Slice(durations, func(i, j int) bool {

return durations[i] < durations[j]

})

fmt.Printf("run %v timers with average=%v, pct50=%v, pct99=%v\n", count, averageDuration, durations[count/2], durations[int(float64(count)*0.99)])

}

注意:由于機(jī)器和性能的不同,多次運(yùn)行測(cè)試可能會(huì)有不一樣的結(jié)果。

這段代碼開了 N 個(gè) Goroutine 并在每一個(gè) Goroutine 中運(yùn)行一個(gè)定時(shí)器,我們會(huì)在定時(shí)器到期時(shí)將開始計(jì)時(shí)到定時(shí)器到期所用的時(shí)間加入 Channel 并用于之后的統(tǒng)計(jì),在函數(shù)的最后我們會(huì)計(jì)算出 N 個(gè) Goroutine 中定時(shí)器到期時(shí)間的平均數(shù)、50 分位數(shù)和 99 分位數(shù):

$ go test ./... -v

=== RUN?? TestTimers

run 1000 timers with average=10.367111ms, pct50=10.234219ms, pct99=10.913219ms

run 2000 timers with average=10.431598ms, pct50=10.37367ms, pct99=11.025823ms

run 5000 timers with average=11.873773ms, pct50=11.986249ms, pct99=12.673725ms

run 10000 timers with average=11.954716ms, pct50=12.313613ms, pct99=13.507858ms

run 20000 timers with average=11.456237ms, pct50=10.625529ms, pct99=25.246254ms

run 50000 timers with average=21.223818ms, pct50=14.792982ms, pct99=34.250143ms

run 100000 timers with average=36.010924ms, pct50=31.794761ms, pct99=128.089527ms

run 500000 timers with average=176.676498ms, pct50=138.238588ms, pct99=676.967558ms

--- PASS: TestTimers (1.21s)

我們將上述代碼輸出的結(jié)果繪制成如下圖所示的折線圖,其中橫軸是并行定時(shí)器的個(gè)數(shù),縱軸表示定時(shí)器從開始到觸發(fā)時(shí)間的差值,三個(gè)不同的線分別表示時(shí)間的平均值、50 分位數(shù)和 99 分位數(shù):

雖然測(cè)試的數(shù)據(jù)可能有一些誤差,但是從圖中我們也能得出一些跟定時(shí)器性能和現(xiàn)象有關(guān)的結(jié)論:

定時(shí)器觸發(fā)的時(shí)間一定會(huì)晚于創(chuàng)建時(shí)傳入的時(shí)間,假設(shè)定時(shí)器需要等待 10ms 觸發(fā),那它觸發(fā)的時(shí)間一定是晚于 10ms 的;

當(dāng)并發(fā)的定時(shí)器數(shù)量達(dá)到 5000 時(shí),定時(shí)器的平均誤差達(dá)到了 ~18%,99 分位數(shù)上的誤差達(dá)到了 ~26%;

并發(fā)定時(shí)器的數(shù)量超過(guò) 5000 之后,定時(shí)器的誤差就變得非常明顯,不能有效、準(zhǔn)確地完成計(jì)時(shí)任務(wù);

這其實(shí)也是因?yàn)槎〞r(shí)器從開始到觸發(fā)的時(shí)間間隔非常短,當(dāng)我們將計(jì)時(shí)的時(shí)間改到 100ms 時(shí)就會(huì)發(fā)現(xiàn)性能問(wèn)題有比較明顯的改善:

哪怕并行運(yùn)行了 10w 個(gè)定時(shí)器,99 分位數(shù)的誤差也只有 ~12%,我們其實(shí)能夠發(fā)現(xiàn)Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)中的定時(shí)器在計(jì)時(shí)時(shí)間較短并且并發(fā)較高時(shí)有著非常明顯的問(wèn)題,所以在一些性能非常敏感的基礎(chǔ)服務(wù)中使用定時(shí)器一定要非常注意,它可能達(dá)不到我們預(yù)期的效果。

不過(guò)哪怕我們不主動(dòng)使用定時(shí)器,而是使用 context.WithDeadline 這種方法,由于它底層也會(huì)使用定時(shí)器實(shí)現(xiàn),所以仍然會(huì)受到影響。

總結(jié)

Go語(yǔ)言的定時(shí)器在并發(fā)編程起到了非常重要的作用,它能夠?yàn)槲覀兲峁┍容^準(zhǔn)確的相對(duì)時(shí)間,基于它的功能,標(biāo)準(zhǔn)庫(kù)中還提供了計(jì)時(shí)器、休眠等接口能夠幫助我們?cè)贕o語(yǔ)言程序中更好地處理過(guò)期和超時(shí)等問(wèn)題。

標(biāo)準(zhǔn)庫(kù)中的定時(shí)器在大多數(shù)情況下是能夠正常工作并且高效完成任務(wù)的,但是在遇到極端情況或者性能敏感場(chǎng)景時(shí),它可能沒(méi)有辦法勝任,而在 10ms 的這個(gè)粒度下,目前也沒(méi)有找到能夠使用的定時(shí)器實(shí)現(xiàn),一些使用時(shí)間輪算法的開源庫(kù)也不能很好地完成這個(gè)任務(wù)。

總結(jié)

以上是生活随笔為你收集整理的c语言定时器作用,Go语言定时器实现原理及作用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。