我知道我可以这样做:
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分钟会有一个func遍历列表,破坏必须被销毁的对象从存储信息的任何位置删除他们的ID和时间信息。这会是一个很好的解决方案吗?
我担心它也会是一个糟糕的解决方案,因为你必须一直遍历一个包含数百万个项目的列表,然后必须有效地删除一些项目,等等。
答案 0 :(得分:3)
所以我同意你的解决方案#2而不是数字1.
遍历一百万个数字的列表比拥有一百万个单独的Go Routines
要容易得多 Go例程很昂贵(与循环相比)并占用内存和处理时间。例如。一百万个Go Routines占用大约4GB的RAM。另一方面遍历列表占用的空间很小,并且在O(n)时间内完成。
这个确切功能的一个很好的例子是Go Cache,它会定期运行Go Go Routine中的过期元素
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)
}
}
使用链表而不是数组肯定可以改进上面的实现,但这是一般的想法
答案 1 :(得分:3)
time.AfterFunc函数是针对此用例设计的:
func randomFunc() {
// do stuff
time.AfterFunc(4*time.Hour, func() { destroyObject("idofobject") })
// do other stuff
}
time.AfterFunc
效率高,使用简单。
正如文档所述,在持续时间过去之后,在goroutine中调用该函数。 goroutine不是像问题那样预先创建的。
答案 2 :(得分:1)
这是一个有趣的问题。我找到了一个解决方案,它使用堆来维护要销毁的项目队列,并准确地睡眠,直到下一个项目被销毁为止。我认为它更有效率,但在某些情况下增益可能会很小。尽管如此,您可以在此处看到代码:
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
}
playground:https://play.golang.org/p/JNV_6VJ_yfK
顺便说一句,有cron
这样的包用于工作管理,但我不熟悉它们所以我不能说它们的效率。
编辑: 我仍然没有足够的声誉评论:( 关于性能:这个代码基本上具有较少的CPU使用率,因为它只在必要时唤醒它自己,并且只遍历破坏而不是整个列表的项目。基于个人(实际上是ACM经验),大致现代的CPU可以在1.2秒左右处理10 ^ 9的循环,这意味着在10 ^ 6的范围内,遍历整个列表大约需要超过1毫秒,不包括伪造破坏代码和数据复制(在数千次运行中平均花费很多,达到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个(我很好奇的原因)。使用我的代码,A指针+时间。时间是28字节。在一百万的规模,差异很小。使用After Func是一个更好的选择。
答案 3 :(得分:1)
如果是一次性,可以通过
轻松实现// 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。