paint-brush
Go'da Haritalarda Uzmanlaşmak: Bilmeniz Gereken Her Şeyile@smokfyz
4,243 okumalar
4,243 okumalar

Go'da Haritalarda Uzmanlaşmak: Bilmeniz Gereken Her Şey

ile Ivan Sharapenkov21m2024/04/27
Read on Terminal Reader

Çok uzun; Okumak

Bu makale, temel ilişkisel dizilerden çarpışma yönetimi stratejilerine sahip gelişmiş karma haritalara kadar Go programlamada harita kullanmanın temellerini kapsamaktadır. Go'da haritaların uygulanması, Sync.Map ile eş zamanlılığın nasıl yönetileceği anlatılıyor ve harita işlemlerini ve karmaşıklıklarını göstermek için pratik örnekler sunuluyor. Tartışmanın amacı, haritaları çeşitli programlama senaryolarında etkili bir şekilde kullanmak, hem verimliliği hem de performansı artırmak için sizi bilgiyle donatmaktır.
featured image - Go'da Haritalarda Uzmanlaşmak: Bilmeniz Gereken Her Şey
Ivan Sharapenkov HackerNoon profile picture
0-item
1-item
2-item


Bir harita (ilişkisel dizi, hash tablosu, hash haritası, hash seti, dict olarak da bilinir), günlük işler, programlama yarışmaları veya iş görüşmeleri sırasında farklı algoritmik problemleri çözmek için gerekli bir veri yapısıdır. Bu nedenle, haritaların bir geliştirici olarak size hangi özellikleri sağladığını anlamak çok önemlidir: haritaların desteklediği işlemler, farklı işlemlerin zaman ve mekan karmaşıklığı ve bu yapının kodda nasıl kullanılacağı. Bu bilgiyi ve bazı pratik deneyimleri kullanarak, sorununuzu çözmek için bir harita uygulamanız gereken durumları tespit edebileceksiniz. Şu anda okuduğunuz makale, haritaların kullanımına ilişkin kapsamlı bir rehber sunmaktadır. Genel olarak harita veri yapısına kısa bir genel bakışla başlayacağız ve ardından Go programlama dilinde haritaların uygulanmasına derinlemesine dalacağız ve haritaları eşzamanlı kodda kullanma stratejilerini tartışacağız.

Yeni Başlayanlar İçin Harita

İlişkisel Dizi

Çeşitli programlama dillerinde uygulanan tüm veri yapılarının arkasında yatan önemli bir kavramı tartışarak başlayalım: soyut veri türü. Bu, bir veri türünü, veri türü tarafından desteklenen işlemler, bu işlemlerin davranışı ve bunlara aktarılabilecek veya onlardan alınabilecek olası değerler açısından tanımlamak için kullanılan matematiksel bir modeldir. Ayrıca bir harita için ilişkisel dizi olarak bilinen soyut bir veri türü de vardır.


İlişkisel dizi, anahtar ve değer çiftlerinden oluşan bir koleksiyonu, her anahtarın koleksiyonda yalnızca bir kez görüneceği veya başka bir deyişle koleksiyondaki her anahtar için yalnızca bir değer olacak şekilde depolamak için kullanılan soyut bir veri türüdür. . get , set ve delete işlemlerini destekler.


Go programlama dilinde, anahtar ve değer çiftlerini (hem anahtar hem de değerin dize olduğu) depolayabilen bir ilişkisel dizi uygulamasına sahip olduğumuzu hayal edelim ve işlemlerin nasıl göründüğüne bakalım.


 package main import ( "fmt" ) // type AssociativeArray implementation somewhere here func main() { // Initialize a new associative array. associativeArray := NewAssociativeArray() // Set the value for the key "Alice". associativeArray.Set("Alice", "apple") // Attempt to retrieve and print the value associated with "Alice". valueForAlice, found := associativeArray.Get("Alice") if found { fmt.Println(valueForAlice) // Output: "apple" } else { fmt.Println("No value found for Alice.") } // Delete the entry associated with "Alice". associativeArray.Delete("Alice") // Attempt to retrieve the value for "Alice" after deletion to check error handling. valueForAlice, found = associativeArray.Get("Alice") if !found { fmt.Println("No value found for Alice. (Deleted)") } // Attempt to retrieve the value for "Bob" which was never set. valueForBob, found := associativeArray.Get("Bob") if !found { fmt.Println("No value found for Bob. (Never set)") } }


Daha önce ayarlanmış bir anahtarın ayarlanması geçerli bir işlemdir ancak bu durumda eski anahtarla yeni bir değer ilişkilendirilecek ve önceki değere ulaşmak mümkün olmayacaktır. İlişkisel dizinin uygulanmasına bağlı olarak silinebilir veya gölgelenebilir.


 package main import ( "fmt" ) // type AssociativeArray implementation somewhere here func main() { // Initialize a new associative array. associativeArray := NewAssociativeArray() // Set value associated with "Alice" key associativeArray.Set("Alice", "apple") // Get value associated with "Alice" key valueForAlice := associativeArray.Get("Alice") fmt.Println(valueForAlice) // This line will print "apple" // Set new value associated with "Alice" key associativeArray.Set("Alice", "orange") // Get new value associated with "Alice" key valueForAlice = associativeArray.Get("Alice") fmt.Println(valueForAlice) // This line will print "orange" }


Bu harika, ancak ilişkisel bir dizinin gerçek hayatta nasıl uygulanacağını sorabilirsiniz. Aslında uygulamanın sınırı yok. Hayal ettiğiniz her şekilde uygulayabilirsiniz; yalnızca bu soyut veri türü için gerekli işlemleri desteklemeniz gerekir. Ancak en yaygın olan birkaç yaklaşım vardır: karma haritası ve ikili arama ağacı. Aralarındaki farklar, zaman ve mekan karmaşıklıklarında ve tabii ki anahtarları ve değerleri depolamak ve almak için kullanılan algoritmalarda yatmaktadır. İleriye dönük olarak karma harita uygulamalarına odaklanacağız, ancak ikili arama ağacının aynı zamanda bir ilişkisel diziyi uygulamak için de kullanılabileceğini belirtmekte fayda var.

Karma Haritası

Karma haritasının, ilişkisel dizi işlemlerini uygulayan bir veri yapısı olduğunu öğrendik: bir anahtar için değer ayarlamak, anahtara göre bir değer almak ve bir anahtarla ilişkili bir değeri silmek. Peki bir karma haritası gerçekte nasıl çalışır? Hadi bulalım.


Buradaki temel fikir "hash" kelimesinin arkasında gizlidir. Bir karma haritası, anahtarı karma işlevine ileterek dizideki değerin saklanacağı yerin dizinini hesaplamak için bir karma işlevi kullanır. Hash fonksiyonunun ve dizinin bir hash haritası oluşturmak için birlikte nasıl çalıştığını görmek için Go programlama dilini kullanarak basit bir hash haritası uygulamaya çalışalım.


 package main import "fmt" func main() { // Define an array to hold string data array := make([]string, 10) // Hash function that returns an integer based on the key and array length hash := func(key string, length int) int { calculatedHash := 0 for _, char := range key { calculatedHash += int(char) } return calculatedHash % length } // Set operation: store a value associated with a key index := hash("Alice", len(array)) array[index] = "apple" // Get operation: retrieve the value associated with a key index = hash("Alice", len(array)) fmt.Println(array[index]) // Should print "apple" // Delete operation: remove the value associated with a key index = hash("Alice", len(array)) array[index] = "" // Setting it to empty string, assuming nil is not an option // Check if the deletion was successful index = hash("Alice", len(array)) if array[index] == "" { fmt.Println("Value deleted successfully.") } else { fmt.Println("Value still exists:", array[index]) } }


Bu aslında gerçek bir karma haritasının nasıl çalıştığının basitleştirilmiş bir yoludur. Bazı durumlarda uygulamamızın bozulabileceğini fark edebilirsiniz. Bunu fark etmediyseniz okumaya devam etmeden önce olası sorunları düşünmek için biraz zaman ayırın.

Çarpışmalar

Önceki uygulamada sorunun ne olduğunu bulmayı başardınız mı? Bu, iki farklı dizenin aynı karma değerini verebileceği bir durumdur. Aynen, elimizde sadece len(array) olası hash değerleri var ama sonsuz sayıda farklı anahtar var ve güvercin deliği ilkesine göre, aynı hash'i üretecek iki farklı anahtar bulabiliriz, bu da onların aynı konuma karşılık geleceği anlamına gelir dizide.


Bununla ne yapacağız? Bu durumla başa çıkmanın bir yolunu bulmamız gerekiyor. Neyse ki, akıllı insanlar bu sorunu zaten çözmüş ve birçok iyi bilinen çözümü uygulamıştır; o yüzden hadi tekerleği yeniden icat etmek yerine bunları kullanalım.


  • Ayrı zincirleme.

    Bu yaklaşımda, değerleri doğrudan dizide saklamak yerine, her indeks için dizide bağlantılı bir liste saklayacağız. Haritaya yeni bir değer eklediğimizde öncelikle aynı anahtara sahip bir öğenin olup olmadığına bakacağız. Varsa değeri güncelleyin; aksi takdirde bağlantılı listeye yeni bir öğe ekleyin.


  • Açık adresleme

    Açık adresleme, çarpışma çözümüne yönelik başka bir yaklaşımdır. Şimdi her dizi konumunda bir anahtar ve değer çifti saklayacağız. Eğer yeni bir değer eklemeye çalışırsak ve bu pozisyonda zaten bir değerin olduğu bir durumla karşılaşırsak probing kullanmaya başlarız. Sondalama, başka bir karma işlevi kullanılarak doğrusal, ikinci dereceden veya hatta rastgele olabilir. Doğrusal incelemede, temel dizinizde boş alan bulana kadar dizini birer birer artırmaya başlarsınız. Diziden bir değer almaya çalıştığınızda dizi konumundaki anahtarın arama anahtarına karşılık gelmediği bir durumla karşılaştığınızda da aynı süreç gerçekleşir.


Ayrı zincirleme yaklaşımının uygulanmasına bakalım çünkü Go'da uygulanan yaklaşıma çok benzer. Fakat şemayı incelemeden önce hash haritamızın nasıl görüneceğine bakalım.


Ayrı zincirlemeye sahip hash haritası


 package main import ( "fmt" "hash/fnv" ) // Entry represents a key-value pair in the linked list type Entry struct { Key string Value string Next *Entry } // HashMap represents the hash table structure type HashMap struct { Buckets []*Entry Size int } // NewHashMap creates a new hash map with a given size func NewHashMap(size int) *HashMap { return &HashMap{ Buckets: make([]*Entry, size), Size: size, } } // HashFunction computes the bucket index for a given key func (h *HashMap) HashFunction(key string) int { hasher := fnv.New32() hasher.Write([]byte(key)) return int(hasher.Sum32()) % h.Size } // Insert adds a new key-value pair to the hash map func (h *HashMap) Set(key, value string) { index := h.HashFunction(key) entry := &Entry{Key: key, Value: value} if h.Buckets[index] == nil { h.Buckets[index] = entry } else { current := h.Buckets[index] for current.Next != nil { current = current.Next } current.Next = entry } } // Search finds the value for a given key in the hash map func (h *HashMap) Get(key string) (string, bool) { index := h.HashFunction(key) current := h.Buckets[index] for current != nil { if current.Key == key { return current.Value, true } current = current.Next } return "", false } // Delete removes an entry from the hash map based on the key func (h *HashMap) Delete(key string) { index := h.HashFunction(key) if h.Buckets[index] != nil { if h.Buckets[index].Key == key { h.Buckets[index] = h.Buckets[index].Next } else { current := h.Buckets[index] for current.Next != nil { if current.Next.Key == key { current.Next = current.Next.Next break } current = current.Next } } } } func main() { hm := NewHashMap(10) hm.Set("name", "John") hm.Set("age", "30") value, exists := hm.Get("name") if exists { fmt.Println("Found:", value) } else { fmt.Println("Not found") } hm.Delete("name") value, exists = hm.Get("name") if exists { fmt.Println("Found:", value) } else { fmt.Println("Not found") } } // OUTPUT: // Found: John // Not found

Karmaşıklık

Bu, algoritmik karmaşıklık kavramına aşina olmayan kişiler için ileri düzeyde bir konu olabilir. Bu açıklamada kendinizi tanıyorsanız o zaman bu bağlantıyı takip edin ve bu kavram hakkında bilgi edinin.


Algoritmalar ve veri yapıları hakkında akıl yürüttüğümüzde zaman ve uzay karmaşıklığı gerçekten önemlidir çünkü bunlar kodumuzun performansını doğrudan etkiler. Önceki bölümde sunulan uygulamanın karmaşıklığını anlamaya çalışalım.


Hash haritasına yeni bir anahtar/değer çifti eklemek için hash hesaplamamız, indekste yer alan bağlantılı listeyi bulmamız ve anahtarımızı bulmak için bağlantılı listeden geçmemiz gerekir. Zaman karmaşıklığını etkileyebilecek ilk şey anahtar boyutudur; ortalama anahtar boyutunun küçük olduğunu ve karma fonksiyonunun zaman karmaşıklığının anahtar boyutuna doğrusal olarak bağlı olduğunu varsayıyoruz. Bu durumda anahtar karmasını hesaplamak için ortalama zaman karmaşıklığının O(1) olduğunu varsayabiliriz. Daha sonra bağlantılı listedeki tüm öğelerin üzerinden geçmemiz gerekiyor. Öğe sayısı, kaç çarpışmanın meydana geldiğine bağlıdır ve en kötü durumda, tüm öğeler aynı hash'e karşılık geldiğinde O(n) olacaktır. Dolayısıyla, toplama işleminin toplam zaman karmaşıklığı O(1) + O(n) = O(n) hem en kötü hem de ortalamadır.


Yük faktörü adı verilen buluşsal yönteme dayalı olarak temel dizinin yeniden boyutlandırılmasını uygulayarak ortalama zaman karmaşıklığını azaltabiliriz. Bu basitçe karma haritasındaki anahtar sayısının temel dizideki yuva sayısına bölünmesiyle elde edilir. Bu değer belirli bir eşiği aşarsa, temeldeki diziyi yeniden boyutlandırabilir ve tüm değerleri yeni diziye kopyalayabiliriz. Bundan sonra her bağlantılı listenin boyutu ortalama olarak sınırlı olacaktır. Bu durumda çoğu durumda ortalama zaman karmaşıklığının O(1) olacağı şekilde bir yük faktörü değeri bulabiliriz. Burada hesaplamaya girmeyeceğiz, ancak bunu anlamak önemlidir.


Böylece karma haritası aşağıdaki zaman ve mekan karmaşıklığı kısıtlamalarını sağlar:

Zaman karmaşıklığı: Tüm işlemler için ortalama O(1) .

Uzay karmaşıklığı: O(n) çünkü her anahtar için bir bağlantılı liste varlığı saklıyoruz.

Go'da Harita

Karma harita uygulama teorisi hakkında bilgi sahibi olarak, Go programlama dilinin bize hangi yerleşik harita türlerini sağladığına bakalım. Go'da harita oluşturmanın birkaç yolu vardır:


 package main func main() { // Using the make Function: This is the most common way to create a map. // You specify the type of the keys and values. m1 := make(map[string]int) // Create a map with string keys and integer values. // You can also optionally set the size of the underlying array using a second argument if // you know how many items you will store in the map before creation. m2 := make(map[string]int, 10) // Using Map Literals: This method is similar to array or slice literals and is // useful for initializing a map with some values. m3 := map[string]int{"one": 1, "two": 2} // Creates and initializes a map. // Nil Map: A map can also be declared without initialization. Such a map // is nil and has no keys, nor can it be added to. var m4 map[string]int // m4 is nil and you cannot add keys to it without initializing it first. // To add keys to a nil map, it must first be initialized using the make function. // m3["hello"] = 1 // Would panic. Map m3 is not nil here and will not cause a panic, this comment should refer to m4 or another nil map. m4 = make(map[string]int) // Now m4 is initialized and ready for use. }


Go yerleşik harita türü aynı zamanda bir ilişkisel dizinin gerektirdiği üç işlemin tümünü ve harita öğeleri üzerinde yineleme için ek özelliği de uygular:


 package main import "fmt" func main() { m := make(map[string]int) // Adding or updating an element. m["one"] = 1 // Retrieving an element and checking if a key exists. value, ok := m["four"] if ok { fmt.Println("Value exists in map:", value) } else { fmt.Println("Value doesn't exist.") } // Deleting an element. delete(m, "one") // You can iterate over a map using a for loop along with the range keyword. // This gives you access to each key and value in the map. // You shouldn't rely on the order of items; even if you run the for loop several times // in sequence, you can get a different order of items. It's a property of hash tables // in general. Items in the hash table do not have a particular order. for key, value := range m { fmt.Println(key, value) } }


Karşılaştırılabilir herhangi bir tür Go haritalarında anahtar olabilir. Karşılaştırılabilir türler arasında boolean, sayısal, dize, işaretçi, kanal ve arayüz türlerinin yanı sıra yalnızca karşılaştırılabilir türleri içeren yapılar veya diziler bulunur. Ayrıca, bir haritada değer olarak kullanılabilecek türler üzerinde neredeyse hiçbir kısıtlama yoktur. Tamsayılar ve dizeler gibi basit türlerden dilimler, diğer haritalar ve hatta işlevler gibi karmaşık türlere kadar her şey olabilirler.

Go Haritalarına Derinlemesine Bakış

Şimdi harita kaynak kodunu inceleyerek haritaların uygulanmasını inceleyeceğiz. Bu, haritaların dahili olarak nasıl uygulandığını daha iyi anlamamıza yardımcı olacaktır. Go'nun haritasının kaynak kodunu burada bulabilirsiniz.


Go'daki bir harita, daha önce tartışılan karma haritasının bir uygulamasıdır. Önceki örneğimizde çarpışma çözümü için bağlantılı liste kullandık. Go farklı bir yaklaşım kullanıyor; Bağlantılı bir liste yerine, her temel dizi indeksinde başka alt diziler olan paketler vardır. Yani temelde yatan dizi iki boyutlu bir alt dizidir. Her paket en fazla 8 anahtar/değer çifti içerir. Bir kovaya 8'den fazla anahtar hash edilirse, tek bir taşma kovasından çıkmak için fazladan kovaları zincirleriz. Hash'in düşük dereceli bitleri bir kova seçmek için kullanılır. Her bir grup, tek bir gruptaki girişleri ayırt etmek için her karmanın birkaç yüksek dereceli bitini içerir. Harita büyüdüğünde iki kat daha büyük yeni bir kova dizisi tahsis ediyoruz. Demetler eski demet dizisinden yeni demet dizisine artımlı olarak kopyalanır.


Haritayı temsil eden ana yapılara hmap ve bmap denir. Basitlik adına yapıdaki bazı yardımcı alanları atlıyoruz. Algoritmanın fikrini anlamak açısından önemli değiller:


 type hmap struct { count int // # of live cells == size of map. Must be first (used by len() builtin) buckets unsafe.Pointer // array of 2^B Buckets; may be nil if count==0. oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing // some other fields... } // A bucket for a Go map. type bmap struct { // tophash generally contains the top byte of the hash value // for each key in this bucket. If tophash[0] < minTopHash, // tophash[0] is a bucket evacuation state instead. tophash [abi.MapBucketCount]uint8 // Followed by bucketCnt keys and then bucketCnt elems. // Followed by an overflow pointer. }


count şu anda haritada depolanan öğelerin sayısıdır. buckets yeni öğeleri sakladığımız temel dizidir ve oldbuckets , harita yeniden boyutlandırılmadan önce öğeleri sakladığımız dizidir. Go, yeniden boyutlandırma sırasında harita öğelerini taşımak yerine bunu aşamalı olarak yapar. buckets ve oldbuckets , tophash - kovada depolanan her anahtar için karmanın ilk baytının bir dizisi - ve ardından MapBucketCount anahtarlarını ve MapBucketCount değerlerini ve sonunda bir taşma işaretçisini depolayan bmap yapısına işaret eden işaretçilerdir. MapBucketCount mimariye özgü bir değerdir ve belgelere göre 8'den fazla değildir. Bir haritanın hafızada nasıl göründüğünü daha iyi anlamak için işte bir resim.


Bir haritanın hafızadaki temsili


Bir anahtarla ilişkili değeri bulmak için öncelikle anahtarımızın bulunduğu kovayı belirlemek için kullanacağımız hash değerini hesaplamamız gerekir. Bunun için Go, hash edilmesi gereken mimariye ve türe bağlı olarak farklı hash işlevlerini kullanır. Karmayı hesapladıktan sonra Go, anahtarımızın bulunabileceği dizini hesaplamak için son birkaç biti kullanır. Go ayrıca hash'in ilk baytını tophash dizisinde saklar ve anahtarın hash'ini kullanarak, tophash kullanarak kovadaki olası anahtar indeksini kolayca bulabilirsiniz. Bundan sonra, aranan anahtarı kovada saklanan anahtarla da karşılaştırmamız gerekir çünkü hash'in ilk baytı arasında çarpışmalar da olabilir. Bu süreç resimde gösterilmektedir.


Anahtarı kullanarak değer bulma süreci

Eşzamanlılık: Harita ve senkronizasyon Haritası

Eşzamanlı Harita

Bir haritanın dahili uygulamasını düşündük, ancak Go, yüksek düzeyde eşzamanlı kod geliştirmek için tasarlanmış bir dildir. İki veya daha fazla goroutinin eş zamanlı olarak harita üzerinde işlemler gerçekleştirdiği durumlarda haritayla nasıl çalışacağız? İki durumu ele alalım: eş zamanlı çalışan iki goroutin önceden başlatılan bir haritadan bazı değerleri okur ve goroutinler yazma işlemlerini gerçekleştirir.


Paylaşılan bir haritadan bazı değerleri okumak için iki goroutin'in aynı anda çalışması durumunda sorunlarla karşılaşmıyoruz çünkü okuma işlemi sırasında haritanın dahili durumu değişmiyor. Bu nedenle aynı anda güvenle okuyabiliriz.


 package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup m := make(map[string]int) m["a"] = 1 m["b"] = 2 readMap := func(key string) { value, ok := m[key] if ok { fmt.Println("Read:", key, value) } else { fmt.Println("Key not found:", key) } wg.Done() } wg.Add(2) go readMap("a") go readMap("b") wg.Wait() }


Öte yandan, eşzamanlı yazma işlemi yaptığımızda, yazma işlemi sırasında haritanın durumu değişebilir. Önceki bölümde incelediğimiz gibi yazma işlemi atomik değildir. Bu, harita değiştirme adımları arasında başka işlemlerin olabileceği anlamına gelir ve başka bir yazma işlemi sırasında okumaya veya yazmaya çalışırsak, hatalı bir haritayla karşılaşabiliriz. Go, bir haritaya eşzamanlı yazma işlemi gerçekleştirmeye çalıştığınızda fatal error: concurrent map writes .


Bu sorunu çözmek için Mutex kullanmamız gerekiyor. Mutex, durumun aynı anda birden fazla yürütme iş parçacığı tarafından değiştirilmesini veya erişilmesini önleyen bir senkronizasyon ilkelidir. Atomik olarak yürütülen ve dolayısıyla güvenli olan kilitleme ve kilit açma olmak üzere iki işlemi destekler. Aslında, birden fazla okuyucunun okuması için kilitlemeye izin veren, ancak yazmalar için yalnızca bir okuyucunun kilitlenmesine izin veren RWMutex'i kullanabiliriz. Bu, çok fazla okumanın olduğu durumlarda performansı optimize etmemize yardımcı olabilir. Eşzamanlı güvenli haritanın uygulanmasına bakalım.


 package main import ( "fmt" "sync" ) // ConcurrentMap wraps a Go map with a sync.RWMutex to manage concurrent access with optimized read performance type ConcurrentMap struct { sync.RWMutex items map[string]interface{} } // NewConcurrentMap creates a new concurrent map func NewConcurrentMap() *ConcurrentMap { return &ConcurrentMap{ items: make(map[string]interface{}), } } // Set adds or updates an element in the map func (m *ConcurrentMap) Set(key string, value interface{}) { m.Lock() defer m.Unlock() m.items[key] = value } // Get retrieves an element from the map func (m *ConcurrentMap) Get(key string) (interface{}, bool) { m.RLock() defer m.RUnlock() value, exists := m.items[key] return value, exists } // Delete removes an element from the map func (m *ConcurrentMap) Delete(key string) { m.Lock() defer m.Unlock() delete(m.items, key) } // Items returns a copy of all items in the map for safe iteration func (m *ConcurrentMap) Items() map[string]interface{} { m.RLock() defer m.RUnlock() itemsCopy := make(map[string]interface{}) for key, value := range m.items { itemsCopy[key] = value } return itemsCopy } func main() { cmap := NewConcurrentMap() cmap.Set("name", "John Doe") cmap.Set("age", 30) if name, ok := cmap.Get("name"); ok { fmt.Println("Name:", name) } if age, ok := cmap.Get("age"); ok { fmt.Println("Age:", age) } items := cmap.Items() fmt.Println("All items:", items) }


senkronizasyon.Harita

Standart pakette eşzamanlı olarak güvenli bir haritanın bir uygulaması daha vardır: sink.Map. Peki ne zaman Sync.Map'i veya Mutex'li bir haritayı kullanmalıyız? Bunu çözmenin zamanı geldi. Belgelere göre Harita türü iki yaygın kullanım durumu için optimize edilmiştir:


  1. Belirli bir anahtara ilişkin giriş yalnızca bir kez yazıldığında ancak birçok kez okunduğunda (örneğin, yalnızca büyüyen önbelleklerde)

  2. Birden fazla goroutine ayrık anahtar kümeleri için girdileri okuduğunda, yazdığında ve üzerine yazdığında. Bu iki durumda, bir Haritanın kullanılması, ayrı bir Mutex veya RWMutex ile eşleştirilmiş bir Go haritasına kıyasla kilit çekişmesini önemli ölçüde azaltabilir.


Bu durumlarda Mutex'li bir harita yerine neden Sync.Map'in daha iyi çalıştığına dair bir fikir geliştirmek ve daha iyi anlamak için, Sync.Map'in kaynak kodunu incelemeliyiz.


 type Map struct { mu Mutex read atomic.Pointer[readOnly] dirty map[any]*entry // some other fields... } // readOnly is an immutable struct stored atomically in the Map.read field. type readOnly struct { m map[any]*entry amended bool // true if the dirty map contains some key not in m. } // An entry is a slot in the map corresponding to a particular key. type entry struct { p atomic.Pointer[any] }


Map harita verilerinin iki temsilini tutar: read ve dirty . Haritayı Oku ( read ): Bu, Map birincil veri deposudur ve eşzamanlı okuma erişimi için tasarlanmıştır. Haritanın anlık görüntüsünün atomik ve kilitsiz okunmasını sağlayan bir readOnly yapısına işaret eden bir atomic.Pointer ile temsil edilir. Bir anahtar arandığında ilk önce read haritasını kontrol eder. Anahtar bulunamazsa ve harita amended olarak işaretlenirse ( dirty haritada henüz read haritasında olmayan yeni anahtarlar olduğunu gösterir), dirty haritayı muteks kilidi ( mu ) altında kontrol etmeye geri döner. Kirli Harita ( dirty ): Bu harita, değiştirilmekte olan girişleri ve read haritasında henüz görünmeyen yeni girişleri saklar. Bu haritaya erişim, özel erişim sağlamak için muteks kilidinin tutulmasını gerektirir. Yazma işleminde anahtar read haritasında yoksa veya güncellenmesi gerekiyorsa işlem dirty harita üzerinde devam eder. dirty harita nil ise, read haritasının yüzeysel bir kopyası oluşturularak, artık geçerli olmayan girişler hariç tutularak başlatılır. Belirli bir "kaçırma" eşiğinden sonra ( dirty haritaya geri dönmeyi gerektiren başarısız aramalar), dirty harita yeni read haritası olarak yükseltilir ve gerekirse yeni bir dirty harita hazırlanır.

Mutex'li Yerleşik Go Haritasından Farklar

Artık Mutex'li yerleşik bir harita ile sink.Map arasındaki farkın ne olduğunu bulabiliriz:


  1. Tartışmayı Kilitle :
    • sync.Map : Kilit çekişmesini en aza indirecek şekilde tasarlanmıştır. Çoğu okuma işlemi için atomik read işaretçisi nedeniyle kilitlenmeye gerek yoktur. Haritanın yalnızca küçük bir bölümünü etkileyen yazma ve silme işlemleri, yalnızca dirty haritayı kilitlediğinden kilitlenme süresini de en aza indirir.
    • Mutex'li Yerleşik Harita : Her erişim (okuma veya yazma), yüksek eşzamanlılık senaryolarında darboğaz haline gelebilecek muteksin edinilmesini gerektirir.
  2. Bellek Yükü ve Karmaşıklık :
    • sync.Map : Haritanın iki versiyonunu korur ve hafızayı daha yoğun kullanabilir. Bu sürümleri yönetme mantığı karmaşıklığı artırır.
    • Mutex'li Yerleşik Harita : Haritanın yalnızca bir sürümünü koruduğu için sync.Map kıyasla daha basit ve daha az bellek kullanır.
  3. Kullanım Durumu Özelliği :
    • sync.Map : Anahtarların çoğunlukla okunduğu ve sıklıkla güncellenmediği veya ayrık anahtar kümelerinde birçok işlemin gerçekleştiği kullanım durumları için optimize edilmiştir.
    • Mutex'li Yerleşik Harita : Genel amaçlıdır, kullanımı kolaydır ancak evrensel kilit çekişmesi nedeniyle belirli yüksek eşzamanlılık senaryolarında iyi performans göstermeyebilir.


Özetle, sync.Map , artan karmaşıklık ve bellek kullanımı pahasına kilit çekişmesini azaltmak için eşzamanlılık optimizasyonlarından yararlanabilen senaryolar için uzmanlaşmıştır. Buna karşılık, muteksli standart bir Go haritası daha basit ve daha genel bir çözümdür ancak yüksek düzeyde eşzamanlı ortamlarda performans sorunları yaşayabilir.

Kapanış

Bu makale boyunca, harita veri yapılarını kullanmanın inceliklerini araştırdık, özellikle de bunların Go programlama dilinde uygulanmasına ve kullanımına odaklandık. İlişkisel dizinin temel konseptinden başlayarak karma haritaların mekaniğini derinlemesine inceledik, operasyonel prosedürlerini, zaman ve uzay karmaşıklıklarını ve çarpışmaları ele almak için ayrı zincirleme ve açık adresleme gibi farklı yaklaşımları tartıştık.


Go alanında, hmap ve bmap gibi temel yapıları inceleyerek haritaların nasıl kullanıldığını ve dile nasıl yerleştirildiğini araştırdık. Bu keşif, Go uygulamalarında haritaların nasıl başlatılacağını, değiştirileceğini ve etkili bir şekilde kullanılacağını gösteren pratik kod örneklerini içeriyordu. Ek olarak, haritaları kullanırken olası eşzamanlılık tehlikelerini vurguladık ve eşzamanlı erişim gerektiren senaryolar için kilit çekişmesini azaltan ve performansı optimize eden sync.Map tanıttık.


Harita veri yapılarının bu yönlerini anlamak, yalnızca kodlama araç setinizi geliştirmekle kalmaz, aynı zamanda sizi daha fazla verimlilik ve güvenle genel programlama zorluklarının üstesinden gelmeye hazırlar. Eş zamanlı ortamlarda en iyi performansı gerektiren basit uygulamalar veya karmaşık sistemler geliştiriyor olsanız da, farklı harita türlerinin nasıl ve ne zaman kullanılacağına dair bilgi çok değerlidir. Programlama uzmanlığınızı geliştirmeye devam ederken, projelerinizde potansiyellerinden tam olarak yararlanmak için bu yapıları ve bunların çeşitli uygulamalarını denemeye devam edin.