Go language core 36 (go language practice and application XIII) -- learning notes

35 | concurrent security dictionary sync.Map (Part 2)

We mentioned in the previous article that since the types of keys and values involved in the methods provided by the concurrent security dictionary are interface {}, we often need to check the actual types of keys and values when calling these methods.

There are roughly two schemes here. In our last article, we mentioned the first scheme, which completely determines the types of keys and values during coding, and then uses the compiler of Go language to check for us.

It's convenient, isn't it? However, although convenient, it makes such a dictionary type lack some flexibility.

If we need a concurrent security dictionary with uint32 key type, we have to write the code again. Therefore, after the requirements are diversified, the workload is larger, and even a lot of identical code will be generated.

Knowledge expansion

Question 1: how to ensure the type correctness of keys and values in the concurrent security dictionary? (scheme II)

So, what should we do if we want to maintain the original flexibility of the sync.Map type and constrain the types of keys and values? This involves the second scheme.

In the second scheme, all methods of the structure type we encapsulate can be completely consistent with the methods of sync.Map type (including method name and method signature).

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

Therefore, when designing such a structure type, it is not enough to include only fields of sync.Map type.

For example:

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

Here, the ConcurrentMap type represents a concurrent security dictionary that can customize key types and value types. This type also has a sync.Map field m, which represents the concurrency security dictionary used internally.

In addition, its fields keyType and valueType are used to save the key type and value type respectively. The type of these two fields is reflect.Type, which we can call reflection type.

This type can represent any data type of the Go language. Moreover, the value of this type is also very easy to obtain: you can call the reflect.TypeOf function and pass in a sample value.

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

Let's now take a look at how to write the ConcurrentMap type method.

Let's first talk about the Load method, which accepts a parameter key of interface {} type. The parameter key represents the value of a key.

Therefore, when we search for key value pairs in the value of m field according to ConcurrentMap, we must ensure that the type of ConcurrentMap is correct. Since the reflection type can be directly used between values, the operator = = or= 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 corresponding to the actual type of the value.

Therefore, if the reflection type of the parameter value is not equal to the reflection type represented by the keyType field, we ignore the subsequent operations and return directly.

At this time, the value of the first result of the Load method is nil, and the value of the second result ok is false. This is exactly what the Load method meant.

Let's talk about the Store method. The Store method accepts two parameters, key and value, both of which are of interface {}. Therefore, our type checking should be done for 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 check code here is very similar to the code in the Load method, except for the processing measures of the check results. When the actual type of the parameter key or value does not meet the requirements, the Store method will immediately raise panic.

This is mainly because the Store method has no result declaration, so it cannot tell the caller in a peaceful way when there is a problem with the parameter value. However, this is also in line with the original meaning of the Store method.

If you don't want to do this, it's OK, so you need to add an error type result to the Store method.

Moreover, when the parameter value type is found to be incorrect, let it directly return the corresponding error type value instead of raising panic. You know, there is only one reference implementation shown here. You can optimize and improve it according to the actual application scenario.

As for other methods and functions related to the ConcurrentMap type, I won't show them here. They have nothing special about the type checking method and processing flow. You can see this code in the demo72.go file.

To sum up a little. The first scheme is suitable for cases where 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 the type assertion expression as an aid, just like IntStrMap.

In the second scheme, we do not need to specify the types of keys and values before the program runs, as long as we dynamically give them when initializing the concurrent security dictionary. Here, we mainly need to use the functions and data types in the reflect package, plus some simple operations such as judgment.

The first scheme has an obvious defect, that is, it is unable to flexibly change the types of keys and values in the dictionary. Once the requirements are diversified, the coding workload will follow.

The second scheme makes up for this defect, but those reflection operations will reduce the performance of the program more or less. We often need to obtain and compare various indicators of the program through rigorous and consistent testing according to the actual application scenario, which can be used as one of the important basis for scheme selection.

Question 2: how do concurrent security dictionaries avoid locks as much as possible?

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

One of the native maps is stored in the read field of sync.Map, which is of type sync/atomic.Value. This native dictionary can be regarded as a snapshot. It always saves all key value pairs contained in the sync. Map value when the conditions are met.

For the convenience of description, we will refer to it as a read-only dictionary later. However, although the read-only dictionary does not increase or decrease the key, it allows the value corresponding to the key to be changed. Therefore, it is not a snapshot in the traditional sense. Its read-only feature is only for the collection of keys.

According to the type of read field, sync.Map does not need lock at all when replacing read-only dictionary. In addition, when storing key value pairs, the read-only dictionary also encapsulates a layer on the values.

It first converts the value to a value of type unsafe.Pointer, and then encapsulates the latter and stores it in its native dictionary. In this way, atomic operations can also be used when changing the value corresponding to a key.

Another native dictionary in sync.Map is represented by its dirty field. The way it stores key value pairs is consistent with the native dictionary in the read field. Its key type is also interface {}, and the values are also converted and encapsulated before storage. Let's call it a dirty dictionary for the time being.

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

As mentioned earlier, when storing keys and values, these two dictionaries only store one of their pointers, not the basic value.

When looking up the value corresponding to the specified key, sync.Map always looks for it in the read-only dictionary first, and there is no need to lock the mutex. Only when it is determined that "there is no key in the read-only dictionary, but there may be this key in the dirty dictionary", it will access the dirty dictionary under the protection of the lock.

Correspondingly, when sync.Map stores a key value pair, as long as the key already exists in the read-only dictionary and the key value pair is not marked as deleted, the new value will be saved and returned directly. In this case, no lock is required.

Otherwise, it will store the key value pairs in the dirty dictionary under the protection of the lock. At this time, the "deleted" mark of the key value pair will be erased.

read and dirty in sync.Map

Incidentally, only when a key value pair should be deleted but still exists in the read-only dictionary will it be logically deleted by marking it as "deleted", rather than directly deleted physically.

This situation will occur for a period of time after rebuilding the dirty dictionary. However, before long, they will be really deleted. When searching and traversing key value pairs, the key value pairs that have been logically deleted will always be ignored.

For deleting key value pairs, sync.Map will first check whether there are corresponding keys in the read-only dictionary. If not, it will try to delete the key value pair from the dirty dictionary under the protection of the lock.

Finally, sync.Map will set the pointer pointing to the value in the key value pair to nil, which is another way of logical deletion.

In addition, there is another detail to note that read-only dictionaries and dirty dictionaries will be converted to each other. When enough key value pairs are found in the dirty dictionary, sync.Map will directly treat the dirty dictionary as a read-only dictionary and save it in its read field, and then set the value of the dirty field representing the dirty dictionary to nil.

After that, once a new key value pair is stored, it will reconstruct the dirty dictionary according to the read-only dictionary. At this time, it will filter out the key value pairs that have been logically deleted in the read-only dictionary. Of course, these conversion operations must be carried out under the protection of the lock.

Interchange of read and dirty in sync.Map

To sum up, the set of key value pairs in the read-only dictionary and dirty Dictionary of sync.Map is not synchronized in real time, and they may be different in some time periods.

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

Therefore, it can be seen that the performance of concurrent security dictionary is often 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 dictionary, followed by deletion and modification.

If the operated key value pair already exists in the read-only Dictionary of sync.Map and has not been deleted logically, modifying it will not use the lock 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 the concurrent security dictionary.

In order to further clarify the actual types of key values in the concurrent security dictionary, there are roughly two options.

  • One scheme is to completely determine the types of keys and values during coding, and then use the Go language compiler to check for us.
  • Another solution is to accept dynamic type settings and check them through reflection operation when the program is running.

These two schemes have their own advantages and disadvantages. The former scheme lacks scalability, and the latter scheme usually affects the performance of the program. In practical use, we generally need to help decision-making through objective testing.

In addition, in some cases, using sync.Map can significantly reduce lock contention compared with the scheme using only native dictionaries and mutexes. sync.Map itself does use locks, but it will avoid locks as much as possible.

Avoid locks wherever possible. This comes to the clever use of sync.Map to hold two native dictionaries. One of the two native dictionaries is called a read-only dictionary and the other is called a dirty dictionary. By analyzing them, we know the applicable scenarios of concurrent security dictionary and the impact of each operation on its performance.

Thinking questions

Today's question is: can you think of other solutions to ensure the correctness of the types of keys and values in the concurrent security dictionary?

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 of customizable 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 represents 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 will cause 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 code

https://github.com/MingsonZheng/go-core-demo

This work adopts Knowledge sharing Attribution - non-commercial use - sharing in the same way 4.0 international license agreement License.

Welcome to reprint, use and republish, but be sure to keep the signature Zheng Ziming (including link: http://www.cnblogs.com/MingsonZheng/ ), shall not be used for commercial purposes, and the works modified based on this article must be distributed under the same license.

Posted on Thu, 25 Nov 2021 20:26:53 -0500 by Druid