GoLanguage Core 36 Speech (GoLanguage Practice and Application Thirteen) --Learning Notes

35 | Concurrent Security Dictionary sync.Map (bottom)

As we mentioned in the previous article, since the methods provided by the concurrent security dictionary involve all types of keys and values of interface {}, we often need to check the actual types of keys and values when invoking these methods.

There are roughly two options. We mentioned the first scenario in the last article, where the type of keys and values is completely determined when encoding, and then checked for us using the compiler for the Go language.

This is convenient, isn't it? However, while convenient, this type of dictionary lacks some flexibility.

If we still need a concurrent security dictionary with the key type uint32, we'll have to write the code as we did. As a result, when demand is diverse, the workload can be even greater, resulting in many identical codes.

Knowledge Expansion

Question 1: How can I ensure that the types of keys and values in the concurrent security dictionary are correct? (Plan II)

So what if we want to keep the sync.Map type flexible while constraining the type of keys and values? This involves the second scenario.

In the second scenario, all methods of the structure type that we encapsulate can be identical to those of the sync.Map type (including method name and method signature).

However, in these methods, we need to add some type-checking code. In addition, the key and value types of the concurrent security dictionary must be fully determined at initialization. In this case, we must first ensure that the key types are comparable.

So when designing such a structure type, it's not enough to include only fields of type sync.Map.

For example:

type ConcurrentMap struct {
 m         sync.Map
 keyType   reflect.Type
 valueType reflect.Type
}

The ConcurrentMap type here represents a concurrent security dictionary that can be customized for key and value types. This type also has a field m of type sync.Map, which represents the concurrent security dictionary used internally.

In addition, its fields keyType and valueType are used to hold key and value types, respectively. Both of these fields are of type reflect.Type, which we can call a reflection type.

This type can represent any data type in the Go language. Also, values of this type are very easy to obtain: simply call the reflect.TypeOf function and pass in a sample value.

The result value of the call to expression reflect.TypeOf(int(123)) represents the reflection type value of type int.

Let's now look at how the ConcurrentMap type method should be written.

Let's start with the Load method, which accepts a parameter key of type interface {}, which represents the value of a key.

Therefore, when we look for key-value pairs in m field values based on ConcurrentMap, we must ensure that ConcurrentMap is of the correct type. Since the operator==or!=can be used directly between reflection type values! So the type checking code here is very simple.

func (cMap *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {
 if reflect.TypeOf(key) != cMap.keyType {
  return
 }
 return cMap.m.Load(key)
}

We pass an interface type value into the reflect.TypeOf function to get the reflection type value that corresponds to the actual type of this value.

Therefore, if the reflection type of the parameter value is not the same as that represented by the keyType field, we ignore the subsequent operation and return directly.

In this case, the value of the first result value of the Load method is nil, and the value of the second result ok is false. This is exactly what the Load method means.

Let's talk about the Store method. The Store method accepts two parameters, key and value, both of which are of type interface {}. Therefore, we should do type checks on them.

func (cMap *ConcurrentMap) Store(key, value interface{}) {
 if reflect.TypeOf(key) != cMap.keyType {
  panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))
 }
 if reflect.TypeOf(value) != cMap.valueType {
  panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))
 }
 cMap.m.Store(key, value)
}

The type checking code here is similar to the code in the Load method, except that the checking results are handled. The Store method immediately raises a panic when the actual type of the parameter key or value does not meet the requirements.

This is mainly due to the fact that the Store method does not have a result declaration, so it cannot tell the caller in a more peaceful way when there is a problem with the parameter value. However, this also fits the original meaning of the Store method.

If you don't want to, and you can, add an error-type result to the Store method.

Also, when an incorrect parameter value type is found, let it return the corresponding error type value directly instead of raising a panic. You know, there is only one reference implementation shown here that you can optimize and improve based on the actual application scenario.

Other methods and functions related to the ConcurrentMap type are not shown here. They are not unique in how type checks are handled and how they are handled. You can see the code in the demo72.go file.

To summarize a little. The first scenario applies when we can completely determine the specific types of keys and values. In this case, we can use the Go language compiler to do type checking and use type assertion expressions as auxiliary, just like IntStrMap.

In the second scenario, we don't need to specify the type of keys and values before the program runs, as long as the concurrent security dictionaries are given dynamically when they are initialized. The functions and data types in the reflect package, along with some simple judgments, are needed here.

One obvious drawback of the first scheme is that it is not flexible to change the type of keys and values in a dictionary. Once the requirements diversify, the coding effort will follow.

The second scenario makes up for this shortcoming well, but those reflective operations can more or less degrade program performance. We often need to use rigorous and consistent testing to obtain and compare program metrics based on the actual application scenario as one of the important basis for program selection.

Question 2: How do concurrent security dictionaries try to avoid using locks?

The sync.Map type uses a large number of atomic operations internally to access keys and values, and uses two native maps as storage media.

One of the native maps exists in the read field of sync.Map, which is of type sync/atomic.Value.   This native dictionary can be thought of as a snapshot that always resaves all the key-value pairs contained in the sync.Map value to which it belongs when the condition is met.

For the sake of description, we'll refer to it as a read-only dictionary for short. However, while a read-only dictionary does not add or subtract keys, it allows you to change the values corresponding to those keys. So it's not a snapshot in the traditional sense, it's read-only only only for the collection of keys.

As you can see from the type of read field, sync.Map does not need locks at all when replacing read-only dictionaries. In addition, this read-only dictionary encapsulates a layer of values when storing key-value pairs.

It first converts the value to a value of type unsafe.Pointer, then encapsulates the latter and stores it in its native dictionary. This allows atomic manipulation to be used when changing the value of a key.

Another native dictionary in sync.Map is represented by its dirty field.   It stores key-value pairs in the same way as the native dictionary in the read field, has a key type of interface {}, and also converts and encapsulates values before storing them. Let's call it a dirty dictionary for now.

Note that if both dirty and read-only dictionaries have the same key-value pair, the two keys here must refer to the same base value, and the same is true for both values.

As mentioned earlier, these two dictionaries store keys and values only with one of their pointers, not with a base value.

When sync.Map looks for values corresponding to a specified key, it always looks in a read-only dictionary first, without locking mutexes. The dirty dictionary will only be accessed under lock protection if it is determined that "there is no read-only dictionary but this key may be present in the dirty dictionary".

Correspondingly, when sync.Map stores a key-value pair, as long as the key already exists in a read-only dictionary and the key-value pair is not marked as deleted, the new value is stored and returned directly, in which case no locks are required.

Otherwise, it stores key-value pairs in a dirty dictionary under lock protection. At this time, the Deleted flag of the key-value pair will be erased.

read and dirty in sync.Map

By the way, only when a key-value pair should be deleted but still exists in a read-only dictionary will it be logically deleted as marked Deleted, not physically deleted.

This can occur some time after the dirty dictionary is rebuilt. However, it won't be long before they are actually deleted. When looking for and traversing key-value pairs, key-value pairs that have been logically deleted are always ignored.

For deleting key-value pairs, sync.Map first checks for corresponding keys in a read-only dictionary. If not, the dirty dictionary may be, and it will try to delete the key-value pair from the dirty dictionary, protected by a lock.

Finally, sync.Map sets the pointer to the value in the key-value pair as nil, which is another way of logical deletion.

In addition, there is one detail to note that read-only dictionaries and dirty dictionaries convert to each other. When enough key-value pairs are found in a dirty dictionary, sync.Map saves the dirty dictionary directly as a read-only dictionary in its read field and sets the dirty field representing the dirty dictionary to nil.

After that, once a new key-value pair is saved, it rebuilds the dirty dictionary from a read-only dictionary. At this point, it filters out logically deleted key-value pairs in the read-only dictionary. Of course, these conversion operations must be done under lock protection.

Interchange between read and dirty in sync.Map

In summary, the set of key-value pairs in sync.Map's read-only and dirty dictionaries is not synchronized in real time and may vary over time.

Since the collection of keys in a read-only dictionary cannot be changed, key-value pairs may sometimes be incomplete. Conversely, the set of key-value pairs in a dirty dictionary is always complete and will not contain key-value pairs that have been logically deleted.

Therefore, you can see that concurrent secure dictionaries tend to perform better when there are many read operations but few write operations. Among several write operations, the operation of adding key-value pairs has the greatest impact on the performance of concurrent security dictionaries, followed by deletion, and finally modification.

If the key-value pair being manipulated already exists in the sync.Map read-only dictionary and is not logically deleted, modifying it will not use locks and will have little impact on its performance.

summary

In these two articles, we discussed the sync.Map type and how to ensure the type correctness of keys and values in a concurrent security dictionary.

To further clarify the actual type of key values in the concurrent security dictionary, there are roughly two options.

  • One option is to completely determine the type of keys and values when encoding, then use the compiler in the Go language to check for us.
  • Another option is to accept dynamic type settings and examine them by reflection while the program is running.

Both schemes have pros and cons. The former has some drawbacks in scalability, while the latter usually affects the performance of the program. In practice, we generally need to pass objective tests to help make decisions.

In addition, at other times, using sync.Map can significantly reduce lock contention compared to using native dictionaries and mutexes alone. The sync.Map itself does use locks, but it avoids them as much as possible.

Locks may be avoided. That's about sync.Map's clever use of holding two native dictionaries. One is called a read-only dictionary, the other is called a dirty dictionary. By analyzing them, we know the scenarios in which concurrent security dictionaries work and how each operation affects their performance.

Think Questions

Today's question is: Can you think of any other way to ensure that the types of keys and values in a concurrent security dictionary are correct?

package main

import (
	"errors"
	"fmt"
	"reflect"
	"sync"
)

// IntStrMap represents a concurrent security dictionary with key type int and value type string.
type IntStrMap struct {
	m sync.Map
}

func (iMap *IntStrMap) Delete(key int) {
	iMap.m.Delete(key)
}

func (iMap *IntStrMap) Load(key int) (value string, ok bool) {
	v, ok := iMap.m.Load(key)
	if v != nil {
		value = v.(string)
	}
	return
}

func (iMap *IntStrMap) LoadOrStore(key int, value string) (actual string, loaded bool) {
	a, loaded := iMap.m.LoadOrStore(key, value)
	actual = a.(string)
	return
}

func (iMap *IntStrMap) Range(f func(key int, value string) bool) {
	f1 := func(key, value interface{}) bool {
		return f(key.(int), value.(string))
	}
	iMap.m.Range(f1)
}

func (iMap *IntStrMap) Store(key int, value string) {
	iMap.m.Store(key, value)
}

// ConcurrentMap represents a concurrent security dictionary that can customize key and value types.
type ConcurrentMap struct {
	m         sync.Map
	keyType   reflect.Type
	valueType reflect.Type
}

func NewConcurrentMap(keyType, valueType reflect.Type) (*ConcurrentMap, error) {
	if keyType == nil {
		return nil, errors.New("nil key type")
	}
	if !keyType.Comparable() {
		return nil, fmt.Errorf("incomparable key type: %s", keyType)
	}
	if valueType == nil {
		return nil, errors.New("nil value type")
	}
	cMap := &ConcurrentMap{
		keyType:   keyType,
		valueType: valueType,
	}
	return cMap, nil
}

func (cMap *ConcurrentMap) Delete(key interface{}) {
	if reflect.TypeOf(key) != cMap.keyType {
		return
	}
	cMap.m.Delete(key)
}

func (cMap *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {
	if reflect.TypeOf(key) != cMap.keyType {
		return
	}
	return cMap.m.Load(key)
}

func (cMap *ConcurrentMap) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
	if reflect.TypeOf(key) != cMap.keyType {
		panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))
	}
	if reflect.TypeOf(value) != cMap.valueType {
		panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))
	}
	actual, loaded = cMap.m.LoadOrStore(key, value)
	return
}

func (cMap *ConcurrentMap) Range(f func(key, value interface{}) bool) {
	cMap.m.Range(f)
}

func (cMap *ConcurrentMap) Store(key, value interface{}) {
	if reflect.TypeOf(key) != cMap.keyType {
		panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))
	}
	if reflect.TypeOf(value) != cMap.valueType {
		panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))
	}
	cMap.m.Store(key, value)
}

// Pairs represent a list of key-value pairs for testing.
var pairs = []struct {
	k int
	v string
}{
	{k: 1, v: "a"},
	{k: 2, v: "b"},
	{k: 3, v: "c"},
	{k: 4, v: "d"},
}

func main() {
	// Example 1.
	var sMap sync.Map
	//sMap.Store([]int{1, 2, 3}, 4)//This line of code causes panic.
	_ = sMap

	// Example 2.
	{
		var iMap IntStrMap
		iMap.Store(pairs[0].k, pairs[0].v)
		iMap.Store(pairs[1].k, pairs[1].v)
		iMap.Store(pairs[2].k, pairs[2].v)
		fmt.Println("[Three pairs have been stored in the IntStrMap instance]")

		iMap.Range(func(key int, value string) bool {
			fmt.Printf("The result of an iteration in Range: %d, %s\n",
				key, value)
			return true
		})

		k0 := pairs[0].k
		v0, ok := iMap.Load(k0)
		fmt.Printf("The result of Load: %v, %v (key: %v)\n",
			v0, ok, k0)

		k3 := pairs[3].k
		v3, ok := iMap.Load(k3)
		fmt.Printf("The result of Load: %v, %v (key: %v)\n",
			v3, ok, k3)

		k2, v2 := pairs[2].k, pairs[2].v
		actual2, loaded2 := iMap.LoadOrStore(k2, v2)
		fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
			actual2, loaded2, k2, v2)
		v3 = pairs[3].v
		actual3, loaded3 := iMap.LoadOrStore(k3, v3)
		fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
			actual3, loaded3, k3, v3)

		k1 := pairs[1].k
		iMap.Delete(k1)
		fmt.Printf("[The pair with the key of %v has been removed from the IntStrMap instance]\n",
			k1)
		v1, ok := iMap.Load(k1)
		fmt.Printf("The result of Load: %v, %v (key: %v)\n",
			v1, ok, k1)
		v1 = pairs[1].v
		actual1, loaded1 := iMap.LoadOrStore(k1, v1)
		fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
			actual1, loaded1, k1, v1)

		iMap.Range(func(key int, value string) bool {
			fmt.Printf("The result of an iteration in Range: %d, %s\n",
				key, value)
			return true
		})
	}
	fmt.Println()

	// Example 2.
	{
		cMap, err := NewConcurrentMap(
			reflect.TypeOf(pairs[0].k), reflect.TypeOf(pairs[0].v))
		if err != nil {
			fmt.Printf("fatal error: %s", err)
			return
		}
		cMap.Store(pairs[0].k, pairs[0].v)
		cMap.Store(pairs[1].k, pairs[1].v)
		cMap.Store(pairs[2].k, pairs[2].v)
		fmt.Println("[Three pairs have been stored in the ConcurrentMap instance]")

		cMap.Range(func(key, value interface{}) bool {
			fmt.Printf("The result of an iteration in Range: %d, %s\n",
				key, value)
			return true
		})

		k0 := pairs[0].k
		v0, ok := cMap.Load(k0)
		fmt.Printf("The result of Load: %v, %v (key: %v)\n",
			v0, ok, k0)

		k3 := pairs[3].k
		v3, ok := cMap.Load(k3)
		fmt.Printf("The result of Load: %v, %v (key: %v)\n",
			v3, ok, k3)

		k2, v2 := pairs[2].k, pairs[2].v
		actual2, loaded2 := cMap.LoadOrStore(k2, v2)
		fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
			actual2, loaded2, k2, v2)
		v3 = pairs[3].v
		actual3, loaded3 := cMap.LoadOrStore(k3, v3)
		fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
			actual3, loaded3, k3, v3)

		k1 := pairs[1].k
		cMap.Delete(k1)
		fmt.Printf("[The pair with the key of %v has been removed from the ConcurrentMap instance]\n",
			k1)
		v1, ok := cMap.Load(k1)
		fmt.Printf("The result of Load: %v, %v (key: %v)\n",
			v1, ok, k1)
		v1 = pairs[1].v
		actual1, loaded1 := cMap.LoadOrStore(k1, v1)
		fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
			actual1, loaded1, k1, v1)

		cMap.Range(func(key, value interface{}) bool {
			fmt.Printf("The result of an iteration in Range: %d, %s\n",
				key, value)
			return true
		})
	}
}

Note Source

GitHub - MingsonZheng/go-core-demo: Go Language Core 36 Learning Source

Tags: Go

Posted on Thu, 25 Nov 2021 12:28:27 -0500 by Dvorak