golang随机数生成以及time包使用

正确使用Go的Timer

我们总是会使用Timer去执行一些定时任务,最近在Go语言的定时器使用上面不小心踩到一点问题,这里记录一下。

go demo(input)

 

func demo(input chan interface{}) {

    for {

        select {

        case msg <- input:

            println(msg)

 

        case <-time.After(time.Second * 5):

            println("5s timer")

 

        case <-time.After(time.Second * 10):

            println("10s timer")

        }

    }

}

写出上面这段程序的目的是从 input channel 持续接收消息加以处理,同时希望每过5秒钟和每过10秒钟就分别执行一个定时任务。但是当你执行这段程序的时候,只要 input channel 中的消息来得足够快,永不间断,你会发现启动的两个定时任务都永远不会执行;即使没有消息到来,第二个10s的定时器也是永远不会执行的。原因就是 select 每次执行都会重新执行 case 条件语句,并重新注册到 select 中,因此这两个定时任务在每次执行 select 的时候,都是启动了一个新的从头开始计时的 Timer 对象,所以这两个定时任务永远不会执行。

其实,

select {

case msg <- input:

 

case <-time.After(time.Second)

}

这个利用 time.After() 启动 Timer 的编程手法主要是用来解决 channel 操作的 Timeout 问题,而不是执行定时任务。Go 语言采用这种方式来实现 channel 的 Timeout 究竟怎么样?这个话题暂时不在这里分析。

如何正确使用 Timer 来完成上面提到的定时任务?

func demo(input chan interface{}) {

    t1 := time.NewTimer(time.Second * 5)

    t2 := time.NewTimer(time.Second * 10)

 

    for {

        select {

        case msg <- input:

            println(msg)

 

        case <-t1.C:

            println("5s timer")

            t1.Reset(time.Second * 5)

 

        case <-t2.C:

            println("10s timer")

            t2.Reset(time.Second * 10)

        }

    }

}

改正后的程序,原理上是自定义两个全局的 Timer,每次执行 select 都重复使用这两个 Timer,而不是每次都生成全新的。这样才可以真正做到在接收消息的同时,还能够定时的执行相应的任务。

 

深入讨论channel timeout

 

Go 语言的 channel 本身是不支持 timeout 的,所以一般实现 channel 的读写超时都采用 select,如下:

select {

case <-c:

case <-time.After(time.Second):

}

这两天在写码的过程中突然对这样实现 channel 超时产生了怀疑,这种方式真的好吗?于是我写了这样一个测试程序:

package main

 

import (

    "os"

    "time"

)

 

func main() {

    c := make(chan int, 100)

 

    go func() {

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

            c <- 1

            time.Sleep(time.Second)

        }

 

        os.Exit(0)

    }()

 

    for {

        select {

        case n := <-c:

            println(n)

        case <-timeAfter(time.Second * 2):

        }

    }

}

 

func timeAfter(d time.Duration) chan int {

    q := make(chan int, 1)

 

    time.AfterFunc(d, func() {

        q <- 1

        println("run") // 重点在这里

    })

 

    return q

}

这个程序很简单,你会发现运行结果将会输出 10 次 “run”,也就是每一遍执行 select 注册的 timer 最终都执行了,虽然这里读 channel 都没有超时。原因其实很简单,每次执行 select 语句,都会将 case 条件语句给执行一遍,于是 timeAfter 的执行结果就是会创建一个定时器,并注册到 runtime 中,select 语句执行完成后,这个定时器本身并没有撤销,还继续保留在 runtime 的小顶堆中,所以这些 timer 一超时就会执行挂载的函数。

当然,用 time.After() 函数来做 channel 的读写超时,在应用层根本感受不到底层的定时器还保留着、继续执行;问题是,如果这里的 select 语句在循环中执行得非常快,也就是 channel 中的消息来得非常频繁,会出现的问题就是 runtime 中会有大量的定时器存在,timeout 的时间设置得越长,底层维护的定时器就会越多。原因就是每次 select 都会注册一个新的 timer,并且 timer 只有在它超时后才会被删除。

想想,自己的 channel 每秒钟将传输成千上万的消息,将会有多少 timer 对象存在底层 runtime 中。大量的临时对象会不会影响内存?大量的 timer 会不会影响其他定时器的准确度?

最后,我觉得正确的 channel timeout 也许应该这么做:

to := time.NewTimer(time.Second)

for {

    to.Reset(time.Second)

    select {

    case <-c:

    case <-to.C:

    }

}

这样做就是为了维护一个全局单一的定时器,每次操作前调整一下定时器的超时时间,从而避免每次循环都生成新的定时器对象。

简单测试了一下两种 channel 超时实现方式,在全力收发数据的情况的内存对象和 gc 情况。 
 
* 蓝线是采用 time.After(),并设置4s 超时的堆内存对象分配的数量 * 绿线是采用 time.After(),并设置2s 超时的堆内存对象分配的数量 * 黄线是采用全局 timer,并设置4s 超时的堆内存对象分配的数量

这个现象其实是预料之中的,重点可以注意设置的超时时间越长,time.After() 的表现将越糟糕。

 

这三条线和上图的三条线描述的对象是一样的,图中的 gc 时间是平均每次 gc 的时间。

针对这个 channel timeout,我没有去测试是否会影响其他定时器的准确性,但我认为这是必然的,随着定时器的增多。

最后,我始终觉得 channel 本身应该支持超时机制,而不是利用 select 来实现。


timer in Go's runtime

12 September 2013

by skoo

我们总是使用sleep()类函数来让线程暂停一段时间,在Go语言里,也是使用Sleep()来暂停goroutine。 那么Go语言的sleep究竟是如何现实的呢?当然你翻看标准库中的time包里面的sleep.go源码时, 你可能会觉得看不明白,因为支持sleep功能的真正实现是在runtime里面。不难想到sleep功能是根据定时器来实现的, 因此接下来看看runtime中的timer究竟长什么样子。

timer的实现主要位于runtime/time.goc文件中。

####主要数据结构

struct  Timers

{

    Lock;

    G       *timerproc;

    bool    sleeping;

    bool    rescheduling;

    Note    waitnote;

    Timer   **t;

    int32   len;

    int32   cap;

};

 

struct  Timer

{

    int32   i;      // heap index

 

    // Timer wakes up at when, and then at when+period, ... (period > 0 only)

    // each time calling f(now, arg) in the timer goroutine, so f must be

    // a well-behaved function and not block.

    int64   when;

    int64   period;

    FuncVal *fv;

    Eface   arg;

};

这两个结构是定义在runtime.h文件中。

调用一次sleep其实就是生成一个Timer,然后添加到Timers中。可以看出来Timers就是维护所有Timer的一个集合。除了可以向Timers中添加Timer外,还要从Timers中删除超时的Timer。所以,Timers采用小顶堆来维护,小顶堆是常用来管理定时器的结构,有的地方也使用红黑树。

Timers

  • Timers结构中有一个Lock, 大概猜测一下就知道是用来保护添加/删除Timer的,实际上也是干这件事的。
  • timerproc指针维护的是一个goroutine,这个goroutine的主要功能就是检查小顶堆中的Timer是否超时。当然,超时就是删除Timer,并且执行Timer对应的动作。
  • t显然就是存储所有Timer的堆了。

省略几个字段放到下文再介绍。

Timer

  • when就是定时器超时的时间
  • fvarg挂载的是Timer超时后需要执行的方法。

到此,Go语言的定时器大概模型就能想象出来了。其实,所有定时器的实现都大同小异,长得都差不多。

####timerproc goroutine 上文提到timerproc维护的是一个goroutine,这个goroutine就做一件事情——不断的循环检查堆,删除掉那些超时的Timer,并执行Timer。下面精简一下代码,看个大概主干就足够明白了。

static void

timerproc(void)

{

    for(;;) {

        for(;;) {

            // 判断Timer是否超时

            t = timers.t[0];

            delta = t->when - now;

            if(delta > 0)

                break;

 

            // TODO: 删除Timer, 代码被删除

 

            // 这里的f调用就是执行Timer了

            f(now, arg);

        }

 

        // 这个过程是,堆中没有任何Timer的时候,就把这个goroutine给挂起,不运行。

        // 添加Timer的时候才会让它ready。

        if(delta < 0) {

            // No timers left - put goroutine to sleep.

            timers.rescheduling = true;

            runtime·park(runtime·unlock, &timers, "timer goroutine (idle)");

            continue;

        }

 

        // 这里干的时候就让这个goroutine也sleep, 等待最近的Timer超时,再开始执行上面的循环检查。当然,这里的sleep不是用本文的定时器来实现的,而是futex锁实现。

        // At least one timer pending.  Sleep until then.

        timers.sleeping = true;

        runtime·notetsleep(&timers.waitnote, delta);

        }

    }

}

这里一定要记住,timerproc是在一个独立的goroutine中执行的。梳理一下上面代码的过程:

  1. 判断堆中是否有Timer? 如果没有就将Timersrescheduling设置为true的状态,true就代表timerproc goroutine被挂起,需要重新调度。这个重新调度的时刻就是在添加一个Timer进来的时候,会ready这个goroutine。这里挂起goroutine使用的是runtime·park()函数。
  2. 如果堆中有Timer存在,就取出堆顶的一个Timer,判断是否超时。超时后,就删除Timer,执行Timer中挂载的方法。这一步是循环检查堆,直到堆中没有Timer或者没有超时的Timer为止。
  3. 在堆中的Timer还没超时之前,这个goroutine将处于sleep状态,也就是设置Timerssleeping为true状态。这个地方是通过runtime·notesleep()函数来完成的,其实现是依赖futex锁。这里,goroutine将sleep多久呢?它将sleep到最近一个Timer超时的时候,就开始执行。

维护Timers超时的goroutine干的所有事情也就这么一点,这里除了堆的维护外,就是goroutine的调度了。

####添加一个定时器

另外一个重要的过程就是如何完成一个Timer的添加? 同样精简掉代码,最好是对照完整的源码看。

static void

addtimer(Timer *t)

{

    if(timers.len >= timers.cap) {

        // TODO 这里是堆没有剩余的空间了,需要分配一个更大的堆来完成添加Timer。

    }

 

    // 这里添加Timer到堆中.

    t->i = timers.len++;

    timers.t[t->i] = t;

    siftup(t->i);

 

    // 这个地方比较重要,这是发生在添加的Timer直接位于堆顶的时候,堆顶位置就代表最近的一个超时Timer.

    if(t->i == 0) {

        // siftup moved to top: new earliest deadline.

        if(timers.sleeping) {

            timers.sleeping = false;

            runtime·notewakeup(&timers.waitnote);

        }

        if(timers.rescheduling) {

            timers.rescheduling = false;

            runtime·ready(timers.timerproc);

        }

    }

}

从代码可以看到新添加的Timer如果是堆顶的话,会检查Timers的sleeping和rescheduling两个状态。上文已经提过了,这两个状态代表timeproc goroutine的状态,如果处于sleeping,那就wakeup它; 如果是rescheduling就ready它。这么做的原因就是通知那个wait的goroutine——”堆中有一个Timer了”或者”堆顶的Timer易主了”,你赶紧来检查一下它是否超时。

添加一个Timer的过程实在太简单了,关键之处就是最后的Timers状态检查逻辑。

####Sleep()的实现 上面的内容阐述了runtime的定时器是如何运行的,那么Go语言又是如何在定时器的基础上实现Sleep()呢?

Go程序中调用time.Sleep()后将进入runtime,执行下面的代码:

void

runtime·tsleep(int64 ns, int8 *reason)

{

    Timer t;

 

    if(ns <= 0)

        return;

 

    t.when = runtime·nanotime() + ns;

    t.period = 0;

    t.fv = &readyv;

    t.arg.data = g;

    runtime·lock(&timers);

    addtimer(&t);

    runtime·park(runtime·unlock, &timers, reason);

}

sleep原来就是创建一个Timer,添加到Timers中去,最后调用runtime·park()将当前调用Sleep()的goroutine给挂起就完事了。

关键是,goroutine被挂起后,如何在超时后被唤醒继续运行呢?这里就是Timer中fvarg两个字段挂载的东西来完成的了。此处,fv挂载了&readyv,看一下readyv的定义:

static void

ready(int64 now, Eface e)

{

    USED(now);

 

    runtime·ready(e.data);

}

 

static FuncVal readyv = {(void(*)(void))ready};

readyv其实就是指向了ready函数,这个ready函数就是在Timer超时的时候将会被执行,它将ready被挂起的goroutine。t.arg.data = g; 这行代码就是在保存当前goroutine了。

Sleep()实现总结起来就三大步:

  1. 创建一个Timer添加到Timers中
  2. 挂起当前goroutine
  3. Timer超时ready当前goroutine

Go语言的定时器实现还是比较清晰的,没有什么繁琐的逻辑。相比,其他地方(如:Nginx)的实现来说,这里可能就是多了goroutine的调度逻辑。

看一个东西的实现,重要的是知道作者为何要这样做。只有弄明白了why, how才有价值。


func main(){
    fmt.Println("----方式一:每次执行结果一样----------")
    for i:=0; i<10; i++ {
        fmt.Print(rand.Intn(100),"/")
    }
    fmt.Println("------方式2-----------")
    r := rand.New(rand.NewSource(time.Now().UnixNano()))
    for i:=0; i<10; i++ {
        fmt.Print(r.Intn(100),"/")
    }
    fmt.Println("------方式2--------")
    rand.Seed(time.Now().UnixNano())
    for i := 0; i < 10; i++ {
        x := rand.Intn(100)
        fmt.Print(x,"/")
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值