การลบคีย์ที่เลือกออกจากแผนที่ภายในวงช่วงระยะปลอดภัยหรือไม่?


136

จะลบคีย์ที่เลือกออกจากแผนที่ได้อย่างไร? ปลอดภัยไหมที่จะรวมdelete()กับช่วงดังในโค้ดด้านล่าง

package main

import "fmt"

type Info struct {
    value string
}

func main() {
    table := make(map[string]*Info)

    for i := 0; i < 10; i++ {
        str := fmt.Sprintf("%v", i)
        table[str] = &Info{str}
    }

    for key, value := range table {
        fmt.Printf("deleting %v=>%v\n", key, value.value)
        delete(table, key)
    }
}

https://play.golang.org/p/u1vufvEjSw

คำตอบ:


175

ปลอดภัย! คุณยังสามารถค้นหาตัวอย่างที่คล้ายกันได้ในEffective Go :

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

และข้อกำหนดภาษา :

ไม่ได้ระบุลำดับการวนซ้ำบนแผนที่และไม่รับประกันว่าจะเหมือนกันจากการทำซ้ำครั้งหนึ่งไปอีกครั้ง หากรายการแผนที่ที่ยังไม่ถึงถูกลบออกในระหว่างการทำซ้ำจะไม่มีการสร้างค่าการวนซ้ำที่เกี่ยวข้อง หากรายการแผนที่ถูกสร้างขึ้นระหว่างการทำซ้ำรายการนั้นอาจถูกสร้างขึ้นในระหว่างการทำซ้ำหรืออาจถูกข้ามไป ตัวเลือกอาจแตกต่างกันไปสำหรับแต่ละรายการที่สร้างขึ้นและจากการทำซ้ำครั้งหนึ่งไปอีกครั้ง หากแผนที่เป็นศูนย์จำนวนการวนซ้ำคือ 0


key.expired ไม่ได้กำหนด (สตริงประเภทไม่มีฟิลด์หรือเมธอดหมดอายุ)

4
@kristen - ในตัวอย่างที่อธิบายไว้ข้างต้นคีย์ไม่ควรเป็นสตริง แต่เป็นประเภทกำหนดเองบางประเภทที่ใช้func (a T) expired() boolอินเทอร์เฟซ สำหรับจุดประสงค์ของตัวอย่างนี้คุณสามารถลอง: m := make(map[int]int) /* populate m here somehow */ for key := range (m) { if key % 2 == 0 { /* this is just some condition, such as calling expired */ delete(m, key); } }
abanana

สับสนมาก
g10guang

150

คำตอบที่เซบาสเตียนมีความถูกต้อง แต่ผมอยากจะรู้ว่าทำไมมันปลอดภัยดังนั้นฉันทำบางขุดเข้าไปในรหัสที่มาแผนที่ ดูเหมือนว่าในการโทรไปdelete(k, v)นั้นโดยพื้นฐานแล้วเพียงแค่ตั้งค่าสถานะ (เช่นเดียวกับการเปลี่ยนค่าการนับ) แทนที่จะลบค่าจริง:

b->tophash[i] = Empty;

(ค่าว่างคือค่าคงที่0)

สิ่งที่แผนที่ดูเหมือนจะทำจริงคือการจัดสรรจำนวนที่เก็บข้อมูลที่กำหนดขึ้นอยู่กับขนาดของแผนที่ซึ่งจะเพิ่มขึ้นเมื่อคุณทำการแทรกในอัตรา2^B(จากซอร์สโค้ดนี้ ):

byte    *buckets;     // array of 2^B Buckets. may be nil if count==0.

ดังนั้นจึงมีการจัดสรรที่เก็บข้อมูลมากกว่าที่คุณใช้อยู่เกือบตลอดเวลาและเมื่อคุณทำrangeบนแผนที่ระบบจะตรวจสอบtophashค่าของที่เก็บข้อมูลแต่ละรายการ2^Bเพื่อดูว่าสามารถข้ามไปได้หรือไม่

สรุปได้ว่าdeleteภายใน a rangeปลอดภัยเนื่องจากข้อมูลยังคงอยู่ในทางเทคนิค แต่เมื่อตรวจสอบtophashพบว่าสามารถข้ามไปได้และไม่รวมไว้ในrangeการดำเนินการใด ๆ ที่คุณกำลังดำเนินการอยู่ ซอร์สโค้ดยังรวมถึงTODO:

 // TODO: consolidate buckets if they are mostly empty
 // can only consolidate if there are no live iterators at this size.

สิ่งนี้อธิบายว่าเหตุใดการใช้delete(k,v)ฟังก์ชันนี้จึงไม่ทำให้หน่วยความจำว่างเพียงแค่ลบออกจากรายการที่เก็บข้อมูลที่คุณได้รับอนุญาตให้เข้าถึง หากคุณต้องการเพิ่มหน่วยความจำจริงคุณจะต้องทำให้แผนที่ทั้งหมดไม่สามารถเข้าถึงได้เพื่อที่การเก็บขยะจะเข้ามาคุณสามารถทำได้โดยใช้บรรทัดเช่น

map = nil

2
ดูเหมือนว่าคุณกำลังบอกว่ามันปลอดภัยที่จะลบค่าใด ๆ ออกจากแผนที่ไม่ใช่แค่ค่า 'ปัจจุบัน' ถูกต้อง? และเมื่อถึงเวลาต้องประเมินแฮชที่ฉันได้ลบไปก่อนหน้านี้โดยพลการมันจะข้ามไปอย่างปลอดภัยหรือไม่?
Flimzy

@Flimzy ที่ถูกต้องตามที่คุณสามารถดูจากสนามเด็กเล่นนี้play.golang.org/p/FwbsghzrsO โปรดทราบว่าหากดัชนีที่คุณลบเป็นดัชนีแรกในช่วงดัชนีจะยังคงแสดงว่าดัชนีนั้นถูกเขียนเป็น k แล้ว แต่ถ้าคุณตั้งค่าดัชนีเป็นค่าใด ๆ นอกเหนือจากดัชนีแรกที่พบช่วงจะแสดงเพียงสองคีย์ / คู่ค่าแทนที่จะเป็นสามและไม่ต้องตกใจ
Verran

1
"ไม่ได้เพิ่มหน่วยความจำจริง" ยังเกี่ยวข้องอยู่หรือไม่ ฉันพยายามค้นหาในแหล่งที่มาที่มีความคิดเห็น แต่ไม่พบ
Tony

11
หมายเหตุสำคัญ:โปรดจำไว้ว่านี่เป็นเพียงการนำไปใช้งานในปัจจุบันและอาจมีการเปลี่ยนแปลงในอนาคตดังนั้นคุณต้องไม่พึ่งพาคุณสมบัติเพิ่มเติมใด ๆ ที่อาจดูเหมือนว่า "รองรับ" เพียงการค้ำประกันคุณได้เป็นผู้ให้โดยสเปคตามที่อธิบายไว้ในคำตอบของเซบาสเตียน (ที่กล่าวว่าการสำรวจและอธิบาย Go internals นั้นน่าสนใจให้ความรู้และยอดเยี่ยมโดยทั่วไป!)
akavel

4

ฉันสงสัยว่าหน่วยความจำรั่วอาจเกิดขึ้นได้ ดังนั้นฉันจึงเขียนโปรแกรมทดสอบ:

package main

import (
    log "github.com/Sirupsen/logrus"
    "os/signal"
    "os"
    "math/rand"
    "time"
)

func main() {
    log.Info("=== START ===")
    defer func() { log.Info("=== DONE ===") }()

    go func() {
        m := make(map[string]string)
        for {
            k := GenerateRandStr(1024)
            m[k] = GenerateRandStr(1024*1024)

            for k2, _ := range m {
                delete(m, k2)
                break
            }
        }
    }()

    osSignals := make(chan os.Signal, 1)
    signal.Notify(osSignals, os.Interrupt)
    for {
        select {
        case <-osSignals:
            log.Info("Recieved ^C command. Exit")
            return
        }
    }
}

func GenerateRandStr(n int) string {
    rand.Seed(time.Now().UnixNano())
    const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    b := make([]byte, n)
    for i := range b {
        b[i] = letterBytes[rand.Int63() % int64(len(letterBytes))]
    }
    return string(b)
}

ดูเหมือนว่า GC จะปลดปล่อยหน่วยความจำ งั้นก็ไม่เป็นไร


0

ในระยะสั้นใช่ ดูคำตอบก่อนหน้านี้

และสิ่งนี้จากที่นี่ :

ianlancetaylor แสดงความคิดเห็นเมื่อวันที่ 18 กุมภาพันธ์ 2015
ฉันคิดว่ากุญแจสำคัญในการทำความเข้าใจเรื่องนี้คือการตระหนักว่าในขณะที่ดำเนินการเนื้อหาของคำสั่ง for / range จะไม่มีการทำซ้ำในปัจจุบัน มีชุดของค่าที่ได้เห็นและชุดของค่าที่ไม่ได้เห็น ในขณะที่ดำเนินการเนื้อหาหนึ่งในคู่คีย์ / ค่าที่เห็น - คู่ล่าสุด - ถูกกำหนดให้กับตัวแปรของคำสั่ง range ไม่มีอะไรพิเศษเกี่ยวกับคู่คีย์ / ค่านั้นเป็นเพียงหนึ่งในคู่ที่เห็นแล้วในระหว่างการทำซ้ำ

คำถามที่เขาตอบคือเกี่ยวกับการปรับเปลี่ยนองค์ประกอบของแผนที่ในระหว่างการrangeดำเนินการซึ่งเป็นเหตุผลที่เขากล่าวถึง "การวนซ้ำปัจจุบัน" แต่ก็มีความเกี่ยวข้องเช่นกันที่นี่คุณสามารถลบคีย์ระหว่างช่วงและนั่นหมายความว่าคุณจะไม่เห็นคีย์เหล่านี้ในช่วงต่อไป (และถ้าคุณเห็นแล้วก็ไม่เป็นไร)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.