I wrote an article about understanding the value type and reference type two years ago, mainly about the differences between common value types and reference types. However, these two types have more origins than that. Today, let's talk about the atomicity of the value type's online path.
Directory
- 0. Concepts
- 1. Interlocked type
- 2. Use the reference type to declare
- 3. binning and unboxing
- 4. Use lock
Returned directory
0. Concepts
First of all, I would like to emphasize that "atomicity" and "thread security" are obfuscation-prone concepts and solve the problem that "atomicity" is not equal to solving "thread security ", however, if "atomicity" is not guaranteed, it is certainly not "thread security. "Atomicity" is considered a category of "thread security". This article only discusses "atomicity.
According to the C # language standard, only the following types of read/write are atomic:
Bool, Char, byte, sbyte, short, ushort, uint, Int, float, reference type, and the above value types are used as underlying enumeration types.
However, note that these types of operations (including increment ++ and decrease --) are not atomic.
But according to the CLI standard.
I .12.6.6 (atomic reads and writes): CLI should ensure that the following types of read/write are atomic:
The correct location in the memory, and the size is not greater than the word size that the CPU processes at a time (that is, the so-called word size, equal to the local int size ).
It can be inferred that in a 64-bit CPU environment, 64-bit built-in types (such as long, ulong, and double) read and write are also atomic. This is described in Eric lippert's blog. However, Eric lippert also mentioned in his article that some non-standard CLI execution environments may break this inference because this is a CLI rule, not a C # language rule. Therefore, for these special types, I personally think it is best to use the interlocked operation or lock. The content is described in detail below.
The following describes how to try to solve the value type atomicity. The more the above method is, the more recommended it is.
Returned directory
1. Interlocked type
To solve the atomic problem of value types, the first type is system. Threading. interlocked. For example, a method Member of the interlocked type:
As you can see, the interlocked type provides various forms of atomic operations: read, write, increment, and decrease. Multiple types are also supported.
The interlocked type provides a way to reduce the use of lock to increase performance. For example, to add atomic operations to a counter, there is no need to use lock like this:
Readonly object locker = new object ();
Int myvar;
// Method for performing atomic value-added operations
Public void atomicincrement (INT increment)
{
Lock (locker)
{
Myvar + = increment;
}
}
You can use the interlocked type to perform this atomic operation. The following code is recommended:
Int myvar;
// Method for performing atomic value-added operations
Public void atomicincrement (INT increment)
{
Interlocked. Add (ref myvar, increment );
}
This not only increases performance, but also reduces code and does not need to declare the object used for lock.
Therefore, if interlocked can be used, try to use the interlocked type to solve the problem. Of course, there will certainly be methods not supported by the interlocked type. For example, the read method only supports the long parameter and does not have double. (This problem will be resolved later)
Returned directory
2. Use the reference type to declare
For example, a value type:
Struct mystruct
{
Public long data1;
Public double data2;
}
Obviously, the occupied space of this type of object must be greater than or equal to 128 bits. In both 32-bit and 64-bit CPU environments, this type of read/write operations are not atomic. Moreover, the interlocked type does not support user-defined struct. How can we make it atomic?
In fact, if you can, the best way here is to change it to class. Yes, reading and writing of the reference type is always atomic. In this case, we still do not need to use lock.
// Change mystruct to myclass. This type of read/write is atomic.
Class myclass
{
Public long data1;
Public double data2;
}
Returned directory
3. binning and unboxing
Suppose we cannot define the above mystruct struct as a reference type. How can we solve the problem of atomic read/write? Another trick is to describe the problem first. Let's first declare the attributes of a mystruct struct:
Public mystruct mystructobject {Get; set ;}
Obviously, the read and write operations of the mystruct struct are not Atomic. If one thread is writing the mystructobject attribute while the other thread is reading the mystructobject variable, unexpected values may be output, it is possible that when one thread writes half of the data, the other thread starts the read operation. Therefore, the value read by another thread through the mystructobject attribute may not be the value before or after the write, but partially write the original incomplete state of some data.
// A thread is writing
Mystructobject = new mystruct () {data1 = 2, data2 = 3 };
// Another thread is reading
// The output is not the value before or after writing, but the incomplete status of some original data!
Console. writeline ("{0} {1}", mystructobject. data1, mystructobject. data2 );
The trick is to use packing and unpacking to declare attributes as objects, as follows:
// Object declaration type
// Read and write mystruct objects need to be packed and unpacked
Public static object mystructobject {Get; set ;}
In this way, if the preceding multi-threaded read/write code is encountered again, there will be no problem (note that a forced conversion is required when reading the mystructobject attribute, that is, the packing operation ), at this time, the read and write operations will be packed and unboxed. The final operation is the object reference object, and its read and write operations are atomic.
Returned directory
4. Use lock
All the methods mentioned above have their own advantages, but they are not omnipotent. If you use lock, although there is extra performance loss, it is omnipotent. So if there are any non-atomic operations, you can nest them into a lock, so that other threads will not interrupt these operations, the whole process is atomic!
For example, in a 32-bit CPU environment, the double addition operation of the atom can be performed as follows:
Readonly object locker = new object ();
Double mydouble;
// Method for performing atomic value-added operations
Public void atomicincrement (double increment)
{
// The interlocked. Add method does not support the double type !!!
Lock (locker)
{
Mydouble + = increment;
}
}