英文:
Golang map len() reports > 0 when map is empty
问题
简短故事:
我遇到了一个问题,之前有数据的地图现在应该是空的,但是报告的len()
大于0,尽管它看起来是空的,我不知道为什么。
较长故事:
我需要同时处理一些“设备”。每个“设备”可以有多条消息。Go的并发性似乎是一个明显的起点,所以我编写了一些代码来处理它,看起来一切都很好。然而...
我为每个“设备”启动了一个goroutine。在main()
函数中,我有一个包含每个“设备”的地图。当收到一条消息时,我检查该“设备”是否已经存在,如果不存在,则创建它,将其存储在地图中,然后将消息传递到设备的接收缓冲通道中。
这很好,每个设备都被很好地处理。然而,当设备在预设的时间内没有收到任何消息时,我需要设备(及其goroutine)终止。我通过在goroutine本身中检查自上次收到消息以来经过了多长时间来实现这一点,如果goroutine被认为是“过时”的,那么接收通道将被关闭。但是如何从地图中删除?
所以我传递了一个指向地图的指针,并且在返回之前,goroutine删除了地图中的设备并关闭了接收通道。然而,问题在于最后我发现len()
函数返回一个大于0的值,但是当我输出地图本身时,我看到它是空的。
我写了一个玩具示例来尝试复制故障,确实看到len()
在地图明显为空时报告大于0。上次我尝试时,我看到了10。再之前一次是14。再之前一次是53。
所以我可以复制这个故障,但我不确定故障是出在我身上还是出在Go上。当里面明显没有项目时,len()
如何报告大于0的值?
这是一个我能够复制的示例。我正在使用Go v1.5.1 windows/amd64。
这里有两件事,就我而言:
- 我是否正确管理了goroutine(可能没有),
- 当其中没有任何项目时,为什么
len(m)
报告大于0的值?
谢谢大家
示例代码:
package main
import (
"log"
"os"
"time"
)
const (
chBuffSize = 100 // 通道缓冲区的大小
thingIdleLifetime = time.Second * 5 // 当空闲时,事物可以存活多长时间
thingsToMake = 1000 // 要创建的事物和相关的goroutine数量
thingMessageCount = 10 // 发送到事物的消息数量
)
// 我们将传递给goroutine进行处理的事物-----------------
type thing struct {
id string
ch chan bool
}
// Go go gadget map test -------------------------------------------------------
func main() {
// 创建所有的事物!
things := make(map[string]thing)
for i := 0; i < thingsToMake; i++ {
t := thing{
id: string(i),
ch: make(chan bool, chBuffSize),
}
things[t.id] = t
// 将事物传递给它自己的goroutine
go doSomething(t, &things)
// 向事物发送(thingMessageCount)条消息
go func(t thing) {
for x := 0; x < thingMessageCount; x++ {
t.ch <- true
}
}(t)
}
// 检查事物的地图以查看是否为空
size := 0
for {
if size == len(things) && size != thingsToMake {
log.Println("与上次地图中的项目数量相同")
log.Println(things)
os.Exit(1)
}
size = len(things)
log.Printf("地图大小:%d\n", size)
time.Sleep(time.Second)
}
}
// 每个goroutine运行的函数----------------------------------------------
//
// 接受两个参数:
// 1)它正在处理的事物
// 2)指向事物地图的指针
//
// 当这个goroutine准备终止时,它应该从事物地图中删除相关的事物,以清理自己
func doSomething(t thing, things *map[string]thing) {
lastAccessed := time.Now()
for {
select {
case <-t.ch:
// 收到消息,所以延长lastAccessed时间
lastAccessed = time.Now()
default:
// 没有收到消息,所以检查是否允许继续
n := time.Now()
d := n.Sub(lastAccessed)
if d > thingIdleLifetime {
// 运行时间超过thingIdleLifetime,所以关闭通道,从地图中删除相关的事物,然后返回,终止goroutine
close(t.ch)
delete(*things, string(t.id))
return
}
}
// 在每个循环中休眠一秒钟,以防止CPU被占用
time.Sleep(time.Second)
}
}
补充一下:在我的原始代码中,这个循环是无限循环的。该程序设计为监听TCP连接并接收和处理数据,因此检查地图计数的函数在自己的goroutine中运行。然而,即使这个示例中的地图len()
检查在main()
函数中,并且它被设计为处理初始数据突发,然后退出循环,它仍然具有相同的症状。
更新2015/11/23 15:56 UTC
我重构了下面的示例。我不确定我是否误解了@RobNapier的意思,但这个效果要好得多。然而,如果我将thingsToMake
更改为一个更大的数字,比如100000,那么我会得到很多这样的错误:
goroutine 199734 [select]:
main.doSomething(0xc0d62e7680, 0x4, 0xc0d64efba0, 0xc082016240)
C:/Users/anttheknee/go/src/maptest/maptest.go:83 +0x144
created by main.main
C:/Users/anttheknee/go/src/maptest/maptest.go:46 +0x463
我不确定问题是我要求Go做太多,还是我对解决方案的理解出了问题。有什么想法吗?
package main
import (
"log"
"os"
"time"
)
const (
chBuffSize = 100 // 通道缓冲区的大小
thingIdleLifetime = time.Second * 5 // 当空闲时,事物可以存活多长时间
thingsToMake = 10000 // 要创建的事物和相关的goroutine数量
thingMessageCount = 10 // 发送到事物的消息数量
)
// 我们将传递给goroutine进行处理的事物-----------------
type thing struct {
id string
ch chan bool
done chan string
}
// Go go gadget map test -------------------------------------------------------
func main() {
// 创建所有的事物!
things := make(map[string]thing)
// 创建一个通道来接收完成通知
doneCh := make(chan string, chBuffSize)
log.Printf("创建 %d 个事物\n", thingsToMake)
for i := 0; i < thingsToMake; i++ {
t := thing{
id: string(i),
ch: make(chan bool, chBuffSize),
done: doneCh,
}
things[t.id] = t
// 将事物传递给它自己的goroutine
go doSomething(t)
// 向事物发送(thingMessageCount)条消息
go func(t thing) {
for x := 0; x < thingMessageCount; x++ {
t.ch <- true
time.Sleep(time.Millisecond * 10)
}
}(t)
}
log.Printf("所有 %d 个事物都已创建\n", thingsToMake)
// 当goroutine完成并清理地图时,在doneCh上接收完成通知
for {
id := <-doneCh
close(things[id].ch)
delete(things, id)
if len(things) == 0 {
log.Printf("地图:%v", things)
log.Println("全部完成。退出")
os.Exit(0)
}
}
}
// 每个goroutine运行的函数----------------------------------------------
//
// 接受两个参数:
// 1)它正在处理的事物
// 2)用于报告完成的通道
//
// 当这个goroutine准备终止时,它应该通过t.done响应通知调用者它已经完成并且可以进行清理。它将等待`thingIdleLifetime`直到超时并自行终止
func doSomething(t thing) {
timer := time.NewTimer(thingIdleLifetime)
for {
select {
case <-t.ch:
// 收到消息,所以重置计时器
timer.Reset(thingIdleLifetime)
case <-timer.C:
// 计时器到期,所以我们需要立即退出
t.done <- t.id
return
}
}
}
更新2015/11/23 16:41 UTC
完成的代码,似乎工作正常。如果有任何可以改进的地方,请随时告诉我,但这个代码是有效的(休眠是故意的,以便看到进展,否则太快了!)
package main
import (
"log"
"os"
"strconv"
"time"
)
const (
chBuffSize = 100 // 通道缓冲区的大小
thingIdleLifetime = time.Second * 5 // 当空闲时,事物可以存活多长时间
thingsToMake = 100000 // 要创建的事物和相关的goroutine数量
thingMessageCount = 10 // 发送到事物的消息数量
)
// 我们将传递给goroutine进行处理的事物-----------------
type thing struct {
id string
receiver chan bool
done chan string
}
// Go go gadget map test -------------------------------------------------------
func main() {
// 创建所有的事物!
things := make(map[string]thing)
// 创建一个通道来接收完成通知
doneCh := make(chan string, chBuffSize)
log.Printf("创建 %d 个事物\n", thingsToMake)
for i := 0; i < thingsToMake; i++ {
t := thing{
id: strconv.Itoa(i),
receiver: make(chan bool, chBuffSize),
done: doneCh,
}
things[t.id] = t
// 将事物传递给它自己的goroutine
go doSomething(t)
// 向事物发送(thingMessageCount)条消息
go func(t thing) {
for x := 0; x < thingMessageCount; x++ {
t.receiver <- true
time.Sleep(time.Millisecond * 100)
}
}(t)
}
log.Printf("所有 %d 个事物都已创建\n", thingsToMake)
// 每秒检查一次`len()`的事物,并在为空时退出
go func() {
for {
time.Sleep(time.Second)
m := things
log.Printf("地图长度:%v", len(m))
if len(m) == 0 {
log.Printf("确认空地图:%v", things)
log.Println("全部完成。退出")
os.Exit(0)
}
}
}()
// 当goroutine完成并清理地图时,在doneCh上接收完成通知
for {
id := <-doneCh
close(things[id].receiver)
delete(things, id)
}
}
// 每个goroutine运行的函数----------------------------------------------
//
// 当这个goroutine准备终止时,它应该通过t.done响应通知调用者它已经完成并且可以进行清理。它将等待`thingIdleLifetime`直到超时并自行终止
func doSomething(t thing) {
timer := time.NewTimer(thingIdleLifetime)
for {
select {
case <-t.receiver:
// 收到消息,所以重置计时器
timer.Reset(thingIdleLifetime)
case <-timer.C:
// 计时器到期,所以我们需要立即退出
t.done <- t.id
return
}
}
}
英文:
Short story:
I'm having an issue where a map that previously had data but should now be empty is reporting a len()
of > 0 even though it appears to be empty, and I have no idea why.
Longer story:
I need to process a number of devices at a time. Each device can have a number of messages. The concurrency of Go seemed like an obvious place to begin, so I wrote up some code to handle it and it seems to be going mostly very well. However...
I started a single goroutine for each device. In the main()
function I have a map
that contains each of the devices. When a message comes in I check to see whether the device already exists and if not I create it, store it in the map, and then pass the message into the device's receiving buffered channel.
This works great, and each device is being processed nicely. However, I need the device (and its goroutine) to terminate when it doesn't receive any messages for a preset amount of time. I've done this by checking in the goroutine itself how much time has passed since the last message was received, and if the goroutine is considered stale then the receiving channel is closed. But how to remove from the map?
So I passed in a pointer to the map, and I have the goroutine delete the device from the map and close the receiving channel before returning. The problem though is that at the end I'm finding that the len()
function returns a value > 0, but when I output the map itself I see that it's empty.
I've written up a toy example to try to replicate the fault, and indeed I'm seeing that len()
is reporting > 0 when the map is apparently empty. The last time I tried it I saw 10. The time before that 14. The time before that one, 53.
So I can replicate the fault, but I'm not sure whether the fault is with me or with Go. How is len()
reporting > 0 when there are apparently no items in it?
Here's an example of how I've been able to replicate. I'm using Go v1.5.1 windows/amd64
There are two things here, as far as I'm concerned:
- Am I managing the goroutines properly (probably not) and
- Why does
len(m)
report > 0 when there are no items in it?
Thanks all
Example Code:
package main
import (
"log"
"os"
"time"
)
const (
chBuffSize = 100 // How large the thing's channel buffer should be
thingIdleLifetime = time.Second * 5 // How long things can live for when idle
thingsToMake = 1000 // How many things and associated goroutines to make
thingMessageCount = 10 // How many messages to send to the thing
)
// The thing that we'll be passing into a goroutine to process -----------------
type thing struct {
id string
ch chan bool
}
// Go go gadget map test -------------------------------------------------------
func main() {
// Make all of the things!
things := make(map[string]thing)
for i := 0; i < thingsToMake; i++ {
t := thing{
id: string(i),
ch: make(chan bool, chBuffSize),
}
things[t.id] = t
// Pass the thing into it's own goroutine
go doSomething(t, &things)
// Send (thingMessageCount) messages to the thing
go func(t thing) {
for x := 0; x < thingMessageCount; x++ {
t.ch <- true
}
}(t)
}
// Check the map of things to see whether we're empty or not
size := 0
for {
if size == len(things) && size != thingsToMake {
log.Println("Same number of items in map as last time")
log.Println(things)
os.Exit(1)
}
size = len(things)
log.Printf("Map size: %d\n", size)
time.Sleep(time.Second)
}
}
// Func for each goroutine to run ----------------------------------------------
//
// Takes two arguments:
// 1) the thing that it is working with
// 2) a pointer to the map of things
//
// When this goroutine is ready to terminate, it should remove the associated
// thing from the map of things to clean up after itself
func doSomething(t thing, things *map[string]thing) {
lastAccessed := time.Now()
for {
select {
case <-t.ch:
// We received a message, so extend the lastAccessed time
lastAccessed = time.Now()
default:
// We haven't received a message, so check if we're allowed to continue
n := time.Now()
d := n.Sub(lastAccessed)
if d > thingIdleLifetime {
// We've run for >thingIdleLifetime, so close the channel, delete the
// associated thing from the map and return, terminating the goroutine
close(t.ch)
delete(*things, string(t.id))
return
}
}
// Just sleep for a second in each loop to prevent the CPU being eaten up
time.Sleep(time.Second)
}
}
Just to add; in my original code this is looping forever. The program is designed to listen for TCP connections and receive and process the data, so the function that is checking the map count is running in it's own goroutine. However, this example has exactly the same symptom even though the map len()
check is in the main()
function and it is designed to handle an initial burst of data and then break out of the loop.
UPDATE 2015/11/23 15:56 UTC
I've refactored my example below. I'm not sure if I've misunderstood @RobNapier or not but this works much better. However, if I change thingsToMake
to a larger number, say 100000, then I get lots of errors like this:
goroutine 199734 [select]:
main.doSomething(0xc0d62e7680, 0x4, 0xc0d64efba0, 0xc082016240)
C:/Users/anttheknee/go/src/maptest/maptest.go:83 +0x144
created by main.main
C:/Users/anttheknee/go/src/maptest/maptest.go:46 +0x463
I'm not sure if the problem is that I'm asking Go to do too much, or if I've made a hash of understanding the solution. Any thoughts?
package main
import (
"log"
"os"
"time"
)
const (
chBuffSize = 100 // How large the thing's channel buffer should be
thingIdleLifetime = time.Second * 5 // How long things can live for when idle
thingsToMake = 10000 // How many things and associated goroutines to make
thingMessageCount = 10 // How many messages to send to the thing
)
// The thing that we'll be passing into a goroutine to process -----------------
type thing struct {
id string
ch chan bool
done chan string
}
// Go go gadget map test -------------------------------------------------------
func main() {
// Make all of the things!
things := make(map[string]thing)
// Make a channel to receive completion notification on
doneCh := make(chan string, chBuffSize)
log.Printf("Making %d things\n", thingsToMake)
for i := 0; i < thingsToMake; i++ {
t := thing{
id: string(i),
ch: make(chan bool, chBuffSize),
done: doneCh,
}
things[t.id] = t
// Pass the thing into it's own goroutine
go doSomething(t)
// Send (thingMessageCount) messages to the thing
go func(t thing) {
for x := 0; x < thingMessageCount; x++ {
t.ch <- true
time.Sleep(time.Millisecond * 10)
}
}(t)
}
log.Printf("All %d things made\n", thingsToMake)
// Receive on doneCh when the goroutine is complete and clean the map up
for {
id := <-doneCh
close(things[id].ch)
delete(things, id)
if len(things) == 0 {
log.Printf("Map: %v", things)
log.Println("All done. Exiting")
os.Exit(0)
}
}
}
// Func for each goroutine to run ----------------------------------------------
//
// Takes two arguments:
// 1) the thing that it is working with
// 2) the channel to report that we're done through
//
// When this goroutine is ready to terminate, it should remove the associated
// thing from the map of things to clean up after itself
func doSomething(t thing) {
timer := time.NewTimer(thingIdleLifetime)
for {
select {
case <-t.ch:
// We received a message, so extend the timer
timer.Reset(thingIdleLifetime)
case <-timer.C:
// Timer returned so we need to exit now
t.done <- t.id
return
}
}
}
UPDATE 2015/11/23 16:41 UTC
The completed code that appears to be working properly. Do feel free to let me know if there are any improvements that could be made, but this works (sleeps are deliberate to see progress as it's otherwise too fast!)
package main
import (
"log"
"os"
"strconv"
"time"
)
const (
chBuffSize = 100 // How large the thing's channel buffer should be
thingIdleLifetime = time.Second * 5 // How long things can live for when idle
thingsToMake = 100000 // How many things and associated goroutines to make
thingMessageCount = 10 // How many messages to send to the thing
)
// The thing that we'll be passing into a goroutine to process -----------------
type thing struct {
id string
receiver chan bool
done chan string
}
// Go go gadget map test -------------------------------------------------------
func main() {
// Make all of the things!
things := make(map[string]thing)
// Make a channel to receive completion notification on
doneCh := make(chan string, chBuffSize)
log.Printf("Making %d things\n", thingsToMake)
for i := 0; i < thingsToMake; i++ {
t := thing{
id: strconv.Itoa(i),
receiver: make(chan bool, chBuffSize),
done: doneCh,
}
things[t.id] = t
// Pass the thing into it's own goroutine
go doSomething(t)
// Send (thingMessageCount) messages to the thing
go func(t thing) {
for x := 0; x < thingMessageCount; x++ {
t.receiver <- true
time.Sleep(time.Millisecond * 100)
}
}(t)
}
log.Printf("All %d things made\n", thingsToMake)
// Check the `len()` of things every second and exit when empty
go func() {
for {
time.Sleep(time.Second)
m := things
log.Printf("Map length: %v", len(m))
if len(m) == 0 {
log.Printf("Confirming empty map: %v", things)
log.Println("All done. Exiting")
os.Exit(0)
}
}
}()
// Receive on doneCh when the goroutine is complete and clean the map up
for {
id := <-doneCh
close(things[id].receiver)
delete(things, id)
}
}
// Func for each goroutine to run ----------------------------------------------
//
// When this goroutine is ready to terminate it should respond through t.done to
// notify the caller that it has finished and can be cleaned up. It will wait
// for `thingIdleLifetime` until it times out and terminates on it's own
func doSomething(t thing) {
timer := time.NewTimer(thingIdleLifetime)
for {
select {
case <-t.receiver:
// We received a message, so extend the timer
timer.Reset(thingIdleLifetime)
case <-timer.C:
// Timer expired so we need to exit now
t.done <- t.id
return
}
}
}
答案1
得分: 6
map
不是线程安全的。你不能在多个 goroutine 上安全地访问一个 map
。你可能会破坏这个 map,就像你在这个例子中看到的那样。
与其让 goroutine 修改 map,不如让 goroutine 在返回之前将它们的标识符写入一个 channel。主循环应该监听该 channel,当收到一个标识符时,应该从 map 中删除该元素。
你可能需要了解一下 Go 并发模式。特别是,你可能想看一下 Fan-out/Fan-in。查看底部的链接。Go 博客上有很多关于并发的信息。
注意,你的 goroutine 正在忙等待检查超时。没有理由这样做。你使用 sleep(1 second)
应该是一个错误的提示。相反,你可以使用 time.Timer
,它会在一段时间后接收一个值,你可以重置它。
你的问题在于你如何将数字转换为字符串:
id: string(i),
这样会使用 i
作为 rune (int32
) 创建一个字符串。例如,string(65)
是 A
。一些不相等的 rune 会解析为相等的字符串。你会得到一个冲突,并且两次关闭同一个 channel。参考 http://play.golang.org/p/__KpnfQc1V
你应该这样写:
id: strconv.Itoa(i),
英文:
map
is not thread-safe. You cannot access a map
on multiple goroutines safely. You can corrupt the map, as you're seeing in this case.
Rather than allow the goroutine to modify the map, the goroutine should write their identifier to a channel before returning. The main loop should watch that channel, and when an identifier comes back, should remove that element from the map.
You'll probably want to read up on Go concurrency patterns. In particular, you may want to look at Fan-out/Fan-in. Look at the links at the bottom. The Go blog has a lot of information on concurrency.
Note that your goroutine is busy waiting to check for timeout. There's no reason for that. The fact that you "sleep(1 second)") should be a clue that there's a mistake. Instead, look at time.Timer
which will give you a chan that will receive a value after some time, which you can reset.
Your problem is how you're converting numbers to strings:
id: string(i),
That creates a string using i
as a rune (int32
). For example string(65)
is A
. Some unequal Runes resolve to equal strings. You get a collision and close the same channel twice. See http://play.golang.org/p/__KpnfQc1V
You meant this:
id: strconv.Itoa(i),
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论