Czy usuwanie wybranych kluczy z mapy w pętli zakresu jest bezpieczne?

144

Jak usunąć wybrane klucze z mapy? Czy łączenie delete()z zakresem jest bezpieczne , jak w kodzie poniżej?

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

Everton
źródło

Odpowiedzi:

184

To jest bezpieczne! Podobną próbkę znajdziesz również w Effective Go :

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

I specyfikacja języka :

Kolejność iteracji na mapach nie jest określona i nie ma gwarancji, że będzie taka sama od jednej iteracji do następnej. Jeśli wpisy mapy, które nie zostały jeszcze osiągnięte, zostaną usunięte podczas iteracji , odpowiadające im wartości iteracji nie zostaną utworzone. Jeśli wpisy mapy są tworzone podczas iteracji , wpis ten może powstać podczas iteracji lub może zostać pominięty. Wybór może się różnić dla każdego utworzonego wpisu i od jednej iteracji do następnej. Jeśli mapa jest zerowa, liczba iteracji wynosi 0.

Sebastian
źródło
key.expired niezdefiniowany (typ string ma pola lub metody wygasł)
5
@kristen - w przykładzie opisanym powyżej klucz nie powinien być ciągiem znaków, ale raczej niestandardowym typem implementującym func (a T) expired() boolinterfejs. Na potrzeby tego przykładu możesz spróbować: 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
Bardzo mylące.
g10guang
156

Odpowiedź Sebastiana jest dokładna, ale chciałem wiedzieć, dlaczego jest to bezpieczne, więc zagłębiłem się w kod źródłowy Mapy . Wygląda na to delete(k, v), że w wywołaniu do , w zasadzie po prostu ustawia flagę (a także zmienia wartość zliczania) zamiast faktycznie usuwać wartość:

b->tophash[i] = Empty;

(Pusta jest stałą dla wartości 0)

Wydaje się, że mapa faktycznie przydziela określoną liczbę segmentów w zależności od rozmiaru mapy, która rośnie w miarę wykonywania wstawiania w tempie 2^B(z tego kodu źródłowego ):

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

Więc prawie zawsze jest przydzielonych więcej zasobników niż używasz, a kiedy przeglądasz rangemapę, sprawdza tę tophashwartość każdego zasobnika w tym, 2^Baby zobaczyć, czy może ją pominąć.

Podsumowując, element deletewewnątrz a rangejest bezpieczny, ponieważ dane technicznie wciąż tam są, ale kiedy sprawdza tophash, widzi, że może go po prostu pominąć i nie uwzględniać go w żadnej rangewykonywanej operacji. Kod źródłowy zawiera nawet TODO:

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

To wyjaśnia, dlaczego użycie tej delete(k,v)funkcji w rzeczywistości nie zwalnia pamięci, po prostu usuwa ją z listy zasobników, do których masz dostęp. Jeśli chcesz zwolnić rzeczywistą pamięć, musisz uczynić całą mapę nieosiągalną, aby wkroczyło czyszczenie pamięci. Możesz to zrobić za pomocą linii takiej jak

map = nil
Verran
źródło
2
Wygląda więc na to, że mówisz, że można bezpiecznie usunąć dowolną wartość z mapy, nie tylko tę „bieżącą”, prawda? A kiedy przyjdzie czas na ocenę skrótu, który wcześniej arbitralnie usunąłem, bezpiecznie go pominie?
Flimzy
@Flimzy Zgadza się, jak widać na tym placu zabaw play.golang.org/p/FwbsghzrsO . Zwróć uwagę, że jeśli usunięty indeks jest pierwszym z zakresu, nadal będzie pokazywał ten indeks, ponieważ jest już zapisany na k, v ale jeśli ustawisz indeks na dowolny inny niż pierwszy, który znajdzie w zakresie, będą wyświetlane tylko dwa klawisze / wartość par zamiast trzech i bez paniki.
Verran
1
Czy komunikat „faktycznie nie zwalnia pamięci” jest nadal aktualny? Próbowałem znaleźć w źródle ten komentarz, ale nie mogę go znaleźć.
Tony
11
Ważna uwaga: pamiętaj, że jest to tylko obecna implementacja i może ulec zmianie w przyszłości, więc nie możesz polegać na żadnych dodatkowych właściwościach, które mogą się wydawać, że „obsługują”. W zaledwie masz gwarancje są przewidziane w specyfikacji, jak opisano w odpowiedzi Sebastiana . (To powiedziawszy, badanie i wyjaśnianie wewnętrznych elementów Go jest z pewnością interesujące,
pouczające
5

Zastanawiałem się, czy może dojść do wycieku pamięci. Napisałem więc program testowy:

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)
}

Wygląda na to, że GC zwalnia pamięć. Więc jest w porządku.

vitvlkv
źródło
0

Krótko mówiąc, tak. Zobacz poprzednie odpowiedzi.

A także to, stąd :

ianlancetaylor skomentował 18 lutego 2015 r.
Myślę, że kluczem do zrozumienia tego jest uświadomienie sobie, że podczas wykonywania treści instrukcji for / range nie ma bieżącej iteracji. Istnieje zbiór wartości, które były widoczne, i zbiór wartości, które nie były widoczne. Podczas wykonywania treści jedna z widzianych par klucz / wartość - ostatnia para - została przypisana do zmiennej (zmiennych) instrukcji range. Nie ma nic specjalnego w tej parze klucz / wartość, to tylko jedna z tych, które były już widziane podczas iteracji.

Pytanie, na które odpowiada, dotyczy modyfikacji elementów mapy na miejscu podczas rangeoperacji, dlatego wspomina o „bieżącej iteracji”. Ale ma to również znaczenie tutaj: możesz usuwać klucze w zakresie, a to po prostu oznacza, że ​​nie zobaczysz ich później w zakresie (a jeśli już je widziałeś, to w porządku).

Larry Clapp
źródło