本章主要讲述多线程竞争下的原子操作.
目录
知识点
竞争条件
线程同步
CPU 时间片和上下文切换
阻塞
内核模式和用户模式
Interlocked 类
1, 出现问题
- ,Interlocked.Increment()
- ,Interlocked.Exchange()
- ,Interlocked.CompareExchange()
- ,Interlocked.Add()
- ,Interlocked.Read()
知识点
竞争条件
当两个或两个以上的线程访问共享数据, 并且尝试同时改变它时, 就发生争用的情况. 它们所依赖的那部分共享数据, 叫做竞争条件.
数据争用是竞争条件中的一种, 出现竞争条件可能会导致内存 (数据) 损坏或者出现不确定性的行为.
线程同步
如果有 N 个线程都会执行某个操作, 当一个线程正在执行这个操作时, 其它线程都必须依次等待, 这就是线程同步.
多线程环境下出现竞争条件, 通常是没有执行正确的同步而导致的.
CPU 时间片和上下文切换
时间片 (timeslice) 是操作系统分配给每个正在运行的进程微观上的一段 CPU 时间.
首先, 内核会给每个进程分配相等的初始时间片, 然后每个进程轮番地执行相应的时间, 当所有进程都处于时间 片耗尽的状态时, 内核会重新为每个进程计算并分配时间片, 如此往复.
请参考:
上下文切换(Context Switch), 也称做进程切换或任务切换, 是指 CPU 从一个进程或线程切换到另一个进程或线程.
在接受到中断 (Interrupt) 的时候, CPU 必须要进行上下文交换. 进行上下文切换时, 会带来性能损失.
请参考[https://zh.wikipedia.org/wiki / 上下文交換
阻塞
阻塞状态指线程处于等待状态. 当线程处于阻塞状态时, 会尽可能少占用 CPU 时间.
当线程从运行状态 (Runing) 变为阻塞状态时(WaitSleepJoin), 操作系统就会将此线程占用的 CPU 时间片分配给别的线程. 当线程恢复运行状态时(Runing), 操作系统会重新分配 CPU 时间片.
分配 CPU 时间片时, 会出现上下文切换.
内核模式和用户模式
只有操作系统才能切换线程, 挂起线程, 因此阻塞线程是由操作系统处理的, 这种方式被称为内核模式(kernel-mode).
Sleep(),Join() 等, 都是使用内核模式来阻塞线程, 实现线程同步(等待).
内核模式实现线程等待时, 出现上下文切换. 这适合等待时间比较长的操作, 这样会减少大量的 CPU 时间损耗.
如果线程只需要等待非常微小的时间, 阻塞线程带来的上下文切换代价会比较大, 这时我们可以使用自旋, 来实现线程同步, 这一方法称为用户模式(user-mode).
Interlocked 类
为多个线程共享的变量提供原子操作.
使用 Interlocked 类, 可以在不阻塞线程 (lock,Monitor) 的情况下, 避免竞争条件.
Interlocked 类是静态类, 让我们先来看看 Interlocked 的常用方法:
方法 | 作用 |
---|---|
CompareExchange() | 比较两个数是否相等,如果相等,则替换第一个值。 |
Decrement() | 以原子操作的形式递减指定变量的值并存储结果。 |
Exchange() | 以原子操作的形式,设置为指定的值并返回原始值。 |
Increment() | 以原子操作的形式递增指定变量的值并存储结果。 |
Add() | 对两个数进行求和并用和替换第一个整数,上述操作作为一个原子操作完成。 |
Read() | 返回一个以原子操作形式加载的值。 |
全部方法请查看:
1, 出现问题
问题:
C# 中赋值和一些简单的数学运算不是原子操作, 受多线程环境影响, 可能会出现问题.
我们可以使用 lock 和 Monitor 来解决这些问题, 但是还有没有更加简单的方法呢?
首先我们编写以下代码:
- private static int sum = 0;
- public static void AddOne()
- {
- for (int i = 0; i <100_0000; i++)
- {
- sum += 1;
- }
- }
这个方法的工作完成后, sum 会 +100.
我们在 Main 方法中调用:
- static void Main(string[] args)
- {
- AddOne();
- AddOne();
- AddOne();
- AddOne();
- AddOne();
- Console.WriteLine("sum =" + sum);
- }
结果肯定是 5000000, 无可争议的.
但是这样会慢一些, 如果作死, 要多线程同时执行呢?
好的, Main 方法改成如下:
- static void Main(string[] args)
- {
- for (int i = 0; i < 5; i++)
- {
- Thread thread = new Thread(AddOne);
- thread.Start();
- }
- Thread.Sleep(TimeSpan.FromSeconds(2));
- Console.WriteLine("sum =" + sum);
- }
笔者运行一次, 出现了 sum = 2633938
我们将每次运算的结果保存到数组中, 截取其中一段发现:
- 8757
- 8758
- 8760
- 8760
- 8760
- 8761
- 8762
- 8763
- 8764
- 8765
- 8766
- 8766
- 8768
- 8769
多个线程使用同一个变量进行操作时, 并不知道此变量已经在其它线程中发生改变, 导致执行完毕后结果不符合期望.
我们可以通过下面这张图来解释:
因此, 这里就需要原子操作, 在某个时刻, 必须只有一个线程能够进行某个操作. 而上面的操作, 指的是读取, 计算, 写入这一过程.
当然, 我们可以使用 lock 或者 Monitor 来解决, 但是这样会带来比较大的性能损失.
这时 Interlocked 就起作用了, 对于一些简单的操作运算, Interlocked 可以实现原子性的操作.
实现原子性, 可以通过多种锁来解决, 目前我们学习到了 lock,Monitor, 现在来学习 Interlocked , 后面会学到更加多的锁的实现.
2,Interlocked.Increment()
用于自增操作.
我们修改一下 AddOne 方法:
- public static void AddOne()
- {
- for (int i = 0; i < 100_0000; i++)
- {
- Interlocked.Increment(ref sum);
- }
- }
然后运行, 你会发现结果 sum = 5000000 , 这就对了.
说明 Interlocked 可以对简单值类型进行原子操作.
Interlocked.Increment()
是递增, 而
Interlocked.Decrement()
是递减.
3,Interlocked.Exchange()
Interlocked.Exchange() 实现赋值运算.
这个方法有多个重载, 我们找其中一个来看看:
public static int Exchange(ref int location1, int value);
意思是将 value 赋给 location1 , 然后返回 location1 改变之前的值.
测试:
- static void Main(string[] args)
- {
- int a = 1;
- int b = 5;
- // a 改变前为 1
- int result1 = Interlocked.Exchange(ref a, 2);
- Console.WriteLine($"a 新的值 a = {a} | a 改变前的值 result1 = {result1}");
- Console.WriteLine();
- // a 改变前为 2,b 为 5
- int result2 = Interlocked.Exchange(ref a, b);
- Console.WriteLine($"a 新的值 a = {a} | b 不会变化的 b = {b} | a 之前的值 result2 = {result2}");
- }
另外 Exchange() 也有对引用类型的重载:
- Exchange<T>(T, T)
- 4,Interlocked.CompareExchange()
其中一个重载:
public static int CompareExchange (ref int location1, int value, int comparand)
比较两个 32 位有符号整数是否相等, 如果相等, 则替换第一个值.
如果 comparand 和 location1 中的值相等, 则将 value 存储在 location1 中. 否则, 不会执行任何操作.
看准了, 是 location1 和 comparand 比较!
使用示例如下:
- static void Main(string[] args)
- {
- int location1 = 1;
- int value = 2;
- int comparand = 3;
- Console.WriteLine("运行前:");
- Console.WriteLine($"location1 = {location1} | value = {value} | comparand = {comparand}");
- Console.WriteLine("当 location1 != comparand 时");
- int result = Interlocked.CompareExchange(ref location1, value, comparand);
- Console.WriteLine($"location1 = {location1} | value = {value} | comparand = {comparand} | location1 改变前的值 {result}");
- Console.WriteLine("当 location1 == comparand 时");
- comparand = 1;
- result = Interlocked.CompareExchange(ref location1, value, comparand);
- Console.WriteLine($"location1 = {location1} | value = {value} | comparand = {comparand} | location1 改变前的值 {result}");
- }
- 5,Interlocked.Add()
对两个 32 位整数进行求和并用和替换第一个整数, 上述操作作为一个原子操作完成.
public static int Add (ref int location1, int value);
只能对 int 或 long 有效.
回到第一小节的多线程求和问题, 使用 Interlocked.Add() 来替换 Interlocked.Increment().
- static void Main(string[] args)
- {
- for (int i = 0; i < 5; i++)
- {
- Thread thread = new Thread(AddOne);
- thread.Start();
- }
- Thread.Sleep(TimeSpan.FromSeconds(2));
- Console.WriteLine("sum =" + sum);
- }
- private static int sum = 0;
- public static void AddOne()
- {
- for (int i = 0; i < 100_0000; i++)
- {
- Interlocked.Add(ref sum,1);
- }
- }
- 6,Interlocked.Read()
返回一个以原子操作形式加载的 64 位值.
64 位系统上不需要 Read 方法, 因为 64 位读取操作已是原子操作. 在 32 位系统上, 64 位读取操作不是原子操作, 除非使用 Read 执行.
public static long Read (ref long location);
就是说 32 位系统上才用得上.
具体场景我没有找到.
你可以参考一下 https://www.codenong.com/6139699/
貌似没有多大用处? 那我懒得看了.
来源: https://www.cnblogs.com/whuanle/p/12724371.html