【问题标题】:how to efficiently call a func after X hours?X小时后如何有效地调用函数?
【发布时间】:2018-06-10 08:26:18
【问题描述】:

我知道我可以这样做:

func randomFunc() {
    // do stuff
    go destroyObjectAfterXHours(4, "idofobject")
    // do other stuff
}

func destroyObjectAfterXHours(hours int, id string) {
    time.Sleep(hours * time.Hour)
    destroyObject(id)
}

但如果我们想象destroyObjectAfterXHours 在几分钟内被调用一百万次,这个解决方案将非常糟糕。

我希望有人可以分享一个更有效的解决方案来解决这个问题。

我一直在考虑一种潜在的解决方案,将销毁时间和对象 ID 存储在某个地方,然后会有一个函数每隔 X 分钟遍历一次列表,销毁必须销毁的对象并删除它们存储该信息的任何位置的 ID 和时间信息。这会是一个好的解决方案吗?

我担心这也是一个糟糕的解决方案,因为您将不得不一直遍历包含数百万个项目的列表,然后必须有效地删除一些项目等。

【问题讨论】:

  • 您能再描述一下您的用例吗?我问是因为可能有更优雅的解决方案。例如,MongoDB 允许根据时间戳上的 TTL 索引自动删除文档。

标签: go


【解决方案1】:

time.AfterFunc 函数是为此用例设计的:

func randomFunc() {
    // do stuff
    time.AfterFunc(4*time.Hour, func() { destroyObject("idofobject") })
    // do other stuff
}

time.AfterFunc 高效且易于使用。

如文档所述,该函数在持续时间过去后在 goroutine 中调用。 goroutine 没有像问题中那样预先创建。

【讨论】:

  • 据我了解,这将产生自己的 goroutine,这意味着一百万个请求仍会导致创建一百万个 goroutine?
  • 是的,运行时会创建一个 goroutine 来在计时器到期时运行该函数。就定时器到期而言,运行时实现非常高效(运行时维护定时器的分桶堆)。
  • 只有当定时器到期?意思是// do other stuff 将在destoryObject 发生之前很久就被调用?如果destroyObject 只需要一毫秒,那么我就不必担心同时运行一百万个 goroutine 了?如果是这样的话,听起来好得令人难以置信
  • 定时器超时后创建goroutine。 do other stuff 将在 destroyOjbect 被调用之前很久执行。如果destroyObject 很快并且过期时间随时间分布,您无需担心数百万个 goroutine。
  • 这真的让我大吃一惊.. 不记得曾经看到过如此复杂的事情变得如此简单.. 将尝试对其进行测试.. 再次感谢:)!
【解决方案2】:

所以我同意你的解决方案 #2 而不是数字 1。

遍历一百万个数字的列表比拥有一百万个单独的 Go 例程要容易得多

Go 例程很昂贵(与循环相比)并且占用内存和处理时间。例如。一百万个 Go Routine 大约需要 4GB 的 RAM。

另一方面,遍历一个列表只占用很少的空间,并且在 O(n) 时间内完成。

这个确切功能的一个很好的例子是 Go Cache,它在定期运行的 Go 例程中删除其过期元素

https://github.com/patrickmn/go-cache/blob/master/cache.go#L931

这是他们如何做到的更详细的示例:

type Item struct {
    Object     interface{}
    Expiration int64
}


func (item Item) Expired() bool {
    if item.Expiration == 0 {
        return false
    }
   return time.Now().UnixNano() > item.Expiration
}

func RemoveItem(s []Item, index int) []int {
     return append(s[:index], s[index+1:]...)
}

func deleteExpired(items []Item){ 
    var deletedItems  []int
    for k, v := range items {
        if v.Expired(){
            deletedItems = append(deletedItems, k)
        }
    }
    for key, deletedIndex := range deleteditems{ 
        items = RemoveItem(items, deletedIndex)
    }
}

上面的实现肯定可以用链表而不是数组来改进,但这是一般的想法

【讨论】:

【解决方案3】:

这是一个有趣的问题。我提出了一个解决方案,它使用堆来维护要销毁的项目队列并准确休眠,直到下一个项目被销毁。我认为它更有效,但在某些情况下收益可能很小。尽管如此,您可以在此处查看代码:

package main
import (
    "container/heap"
    "fmt"
    "time"
)

type Item struct {
    Expiration time.Time
    Object     interface{} // It would make more sence to be *interface{}, but not as convinient
}

//MINIT is the minimal interval for delete to run. In most cases, it is better to be set as 0
const MININT = 1 * time.Second

func deleteExpired(addCh chan Item) (quitCh chan bool) {
    quitCh = make(chan bool)
    go func() {
        h := make(ExpHeap, 0)
        var t *time.Timer

        item := <-addCh
        heap.Push(&h, &item)
        t = time.NewTimer(time.Until(h[0].Expiration))

        for {
            //Check unfinished incoming first
            for incoming := true; incoming; {
                select {
                case item := <-addCh:
                    heap.Push(&h, &item)
                default:
                    incoming = false
                }
            }
            if delta := time.Until(h[0].Expiration); delta >= MININT {
                t.Reset(delta)
            } else {
                t.Reset(MININT)
            }

            select {
            case <-quitCh:
                return
            //New Item incoming, break the timer
            case item := <-addCh:
                heap.Push(&h, &item)
                if item.Expiration.After(h[0].Expiration) {
                    continue
                }
                if delta := time.Until(item.Expiration); delta >= MININT {
                    t.Reset(delta)
                } else {
                    t.Reset(MININT)
                }
            //Wait until next item to be deleted
            case <-t.C:
                for !h[0].Expiration.After(time.Now()) {
                    item := heap.Pop(&h).(*Item)
                    destroy(item.Object)
                }
                if delta := time.Until(h[0].Expiration); delta >= MININT {
                    t.Reset(delta)
                } else {
                    t.Reset(MININT)
                }
            }
        }
    }()
    return quitCh
}

type ExpHeap []*Item

func (h ExpHeap) Len() int {
    return len(h)
}

func (h ExpHeap) Swap(i, j int) {
    h[i], h[j] = h[j], h[i]
}

func (h ExpHeap) Less(i, j int) bool {
    return h[i].Expiration.Before(h[j].Expiration)
}

func (h *ExpHeap) Push(x interface{}) {
    item := x.(*Item)
    *h = append(*h, item)
}

func (h *ExpHeap) Pop() interface{} {
    old, n := *h, len(*h)
    item := old[n-1]
    *h = old[:n-1]
    return item
}

//Auctural destroy code.
func destroy(x interface{}) {
    fmt.Printf("%v @ %v\n", x, time.Now())
}

func main() {
    addCh := make(chan Item)
    quitCh := deleteExpired(addCh)

    for i := 30; i > 0; i-- {
        t := time.Now().Add(time.Duration(i) * time.Second / 2)
        addCh <- Item{t, t}
    }
    time.Sleep(7 * time.Second)
    quitCh <- true
}

游乐场:https://play.golang.org/p/JNV_6VJ_yfK

顺便说一句,有像cron这样的包用于工作管理,但我不熟悉它们,所以我不能说它们的效率。

编辑: 仍然我没有足够的声誉来发表评论:( 关于性能:此代码基本上具有较少的 CPU 使用率,因为它仅在必要时自行唤醒,并且仅遍历要销毁的项目而不是整个列表。根据个人(实际是ACM经验),大概现代CPU可以在1.2秒左右处理一个10^9的循环,也就是说在10^6的规模上,遍历整个列表大约需要1毫秒以上,不包括实际销毁代码AND 数据复制(在 100 毫秒左右的范围内,运行数千次,平均成本会很高)。我的代码的方法是 O(lg N),它在 10^6 的规模上至少快一千倍(考虑到常数)。请再次注意,所有这些计算都是基于经验而不是基准(有但我无法提供)。

编辑 2: 再想一想,我认为简单的解决方案可以使用简单的优化:

func deleteExpired(items []Item){ 
    tail = len(items)
    for index, v := range items { //better naming
        if v.Expired(){
            tail--
            items[tail],items[index] = v,items[tail]
        }
    }
    deleteditems := items[tail:]
    items:=items[:tail]
}

通过此更改,它不再低效地复制数据并且不会分配额外的空间。

编辑 3: 从here更改代码 我测试了afterfunc的memoryuse。在我的笔记本电脑上,每次调用是 250 字节,而在 palyground 上是 69(我很好奇原因)。使用我的代码,指针 + 时间。时间是 28 字节。在百万的规模上,差异很小。使用 After Func 是一个更好的选择。

【讨论】:

  • 真的很感激,但对我来说看起来有点太复杂了:p.. 我会尝试更仔细地研究它。您认为性能会比 cjds 答案中的性能更好吗?
  • 非常感谢 time.AfterFunc没有按我希望的方式工作,我会尝试集成代码(:
  • 堆确实是解决问题的好办法,但为什么不使用运行时的定时器堆呢?
  • 好吧,不知何故,我神奇地获得了评论的能力:)。 time.AfterFunc 每次都会产生一个 gorountine 它被调用,并且只有在计时器停止后才释放 gorountine。所以基本上它与您的原始解决方案相同。 profermance 分析已编辑到答案中。
  • @leafbebop time.AfterFunc not 在调用时生成一个 goroutine。 The goroutine is started in goFunc。函数goFunccalled when the time elapses
【解决方案4】:

如果是一次性的,这可以很容易地实现

// Make the destruction cancelable
cancel := make(chan bool)

go func(t time.Duration, id int){
 expired := time.NewTimer(t).C
 select {
   // destroy the object when the timer is expired
   case <-expired:
     destroyObject(id)
   // or cancel the destruction in case we get a cancel signal
   // before it is destroyed
   case <-cancel:
     fmt.Println("Cancelled destruction of",id)
     return
 }
}(time.Hours * 4, id)

if weather == weather.SUNNY {
  cancel <- true
}

如果你想每 4 小时做一次:

// Same as above, though since id may have already been destroyed
// once, I name the channel different
done := make(chan bool)

go func(t time.Duration,id int){

  // Sends to the channel every t
  tick := time.NewTicker(t).C

  // Wrap, otherwise select will only execute the first tick
  for{
    select {
      // t has passed, so id can be destroyed
      case <-tick:
        destroyObject(id)
      // We are finished destroying stuff
      case <-done:
        fmt.Println("Ok, ok, I quit destroying...")
        return
    }
  }
}()

if weather == weather.RAINY {
  done <- true
}

其背后的想法是为每个销毁作业运行一个可以取消的 goroutine。比如说,你有一个会话并且用户做了一些事情,所以你想让会话保持活动状态。由于 goroutines 非常便宜,你可以简单地启动另一个 goroutine。

【讨论】:

    猜你喜欢
    • 2014-01-24
    • 2017-03-29
    • 1970-01-01
    • 1970-01-01
    • 2020-08-16
    • 2012-01-03
    • 2014-09-14
    • 2018-06-23
    • 2012-02-04
    相关资源
    最近更新 更多