C#多线程系列(3):原子操作
本章主要讲述多线程竞争下的原子操作。
知识点
竞争条件
当两个或两个以上的线程访问共享数据,并且尝试同时改变它时,就发生争用的情况。它们所依赖的那部分共享数据,叫做竞争条件。
数据争用是竞争条件中的一种,出现竞争条件可能会导致内存(数据)损坏或者出现不确定性的行为。
线程同步
如果有 N 个线程都会执行某个操作,当一个线程正在执行这个操作时,其它线程都必须依次等待,这就是线程同步。
多线程环境下出现竞争条件,通常是没有执行正确的同步而导致的。
CPU时间片和上下文切换
时间片(timeslice)是操作系统分配给每个正在运行的进程微观上的一段 CPU 时间。
首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间 片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。
请参考:https://zh.wikipedia.org/wiki/%E6%97%B6%E9%97%B4%E7%89%87
上下文切换(Context Switch),也称做进程切换或任务切换,是指 CPU 从一个进程或线程切换到另一个进程或线程。
在接受到中断(Interrupt)的时候,CPU 必须要进行上下文交换。进行上下文切换时,会带来性能损失。
请参考[https://zh.wikipedia.org/wiki/上下文交換
阻塞
阻塞状态指线程处于等待状态。当线程处于阻塞状态时,会尽可能少占用 CPU 时间。
当线程从运行状态(Runing)变为阻塞状态时(WaitSleepJoin),操作系统就会将此线程占用的 CPU 时间片分配给别的线程。当线程恢复运行状态时(Runing),操作系统会重新分配 CPU 时间片。
分配 CPU 时间片时,会出现上下文切换。
内核模式和用户模式
只有操作系统才能切换线程、挂起线程,因此阻塞线程是由操作系统处理的,这种方式被称为内核模式(kernel-mode)。
Sleep()、Join() 等,都是使用内核模式来阻塞线程,实现线程同步(等待)。
如果线程只需要等待非常微小的时间,阻塞线程带来的上下文切换代价会比较大,这时我们可以使用自旋,来实现线程同步,这一方法称为用户模式(user-mode)。
Interlocked 类
为多个线程共享的变量提供原子操作。
使用 Interlocked 类,可以在不阻塞线程(lock、Monitor)的情况下,避免竞争条件。
Interlocked 类是静态类,让我们先来看看 Interlocked 的常用方法:
| 方法 | 作用 |
|---|---|
| CompareExchange() | 比较两个数是否相等,如果相等,则替换第一个值。 |
| Decrement() | 以原子操作的形式递减指定变量的值并存储结果。 |
| Exchange() | 以原子操作的形式,设置为指定的值并返回原始值。 |
| Increment() | 以原子操作的形式递增指定变量的值并存储结果。 |
| Add() | 对两个数进行求和并用和替换第一个整数,上述操作作为一个原子操作完成。 |
| Read() | 返回一个以原子操作形式加载的值。 |
全部方法请查看:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked?view=netcore-3.1#methods
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 可以实现原子性的操作。
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/
貌似没有多大用处?那我懒得看了。

C#多线程系列(3):原子操作的更多相关文章
- Java多线程系列--“JUC锁”03之 公平锁(一)
概要 本章对“公平锁”的获取锁机制进行介绍(本文的公平锁指的是互斥锁的公平锁),内容包括:基本概念ReentrantLock数据结构参考代码获取公平锁(基于JDK1.7.0_40)一. tryAcqu ...
- Java多线程系列--“JUC原子类”02之 AtomicLong原子类
概要 AtomicInteger, AtomicLong和AtomicBoolean这3个基本类型的原子类的原理和用法相似.本章以AtomicLong对基本类型的原子类进行介绍.内容包括:Atomic ...
- Java多线程系列--“JUC原子类”03之 AtomicLongArray原子类
概要 AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray这3个数组类型的原子类的原理和用法相似.本章以AtomicLongArray对数 ...
- Java多线程系列--“JUC原子类”04之 AtomicReference原子类
概要 本章对AtomicReference引用类型的原子类进行介绍.内容包括:AtomicReference介绍和函数列表AtomicReference源码分析(基于JDK1.7.0_40)Atomi ...
- Java多线程系列
一.参考文献 1.:Java多线程系列目录 (一) 基础篇 01. Java多线程系列--“基础篇”01之 基本概念 02. Java多线程系列--“基础篇”02之 常用的实现多线程的两种方式 03. ...
- java多线程系列(一)
java多线程技能 前言:本系列将从零开始讲解java多线程相关的技术,内容参考于<java多线程核心技术>与<java并发编程实战>等相关资料,希望站在巨人的肩膀上,再通过我 ...
- java多线程系列(二)
对象变量的并发访问 前言:本系列将从零开始讲解java多线程相关的技术,内容参考于<java多线程核心技术>与<java并发编程实战>等相关资料,希望站在巨人的肩膀上,再通过我 ...
- java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析
java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析 前言:如有不正确的地方,还望指正. 目录 认识cpu.核心与线程 java ...
- Java多线程系列——原子类的实现(CAS算法)
1.什么是CAS? CAS:Compare and Swap,即比较再交换. jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronou ...
- java多线程系列(一)---多线程技能
java多线程技能 前言:本系列将从零开始讲解java多线程相关的技术,内容参考于<java多线程核心技术>与<java并发编程实战>等相关资料,希望站在巨人的肩膀上,再通过我 ...
随机推荐
- C++类复习及新的认识 6.1.1+6.1.2内容(适合看过一遍书的新手)
作者水平有限,文字表述大多摘抄课本,源码部分由课本加自己改编而成,所有代码均在vs2019中编译通过 定义类操作 class Tdate { public: void Set(int m, int d ...
- 【ES】Java High Level REST Client 使用示例(增加修改)
ES提供了多种编程语言的链接方式,有Java API,PHP API,.NET API 官网可以详细了解 https://www.elastic.co/guide/en/elasticsearch/c ...
- python爬取网站页面时,部分标签无指定属性而报错
在写爬取页面a标签下href属性的时候,有这样一个问题,如果a标签下没有href这个属性则会报错,如下: 百度了有师傅用正则匹配的,方法感觉都不怎么好,查了BeautifulSoup的官方文档,发现一 ...
- xpath模块使用
xpath模块使用 一.什么是xml(百度百科解释如下) 可扩展标记语言,标准通用标记语言的子集,简称XML.是一种用于标记电子文件使其具有结构性的标记语言. 在电子计算机中,标记指计算机所能理解的信 ...
- no parameterless constructor define for type 解决一例
在生成根据模型和上下文生成带增删查改操作的视图的控制器时,提示上述信息,网上查找了资料也没有解决,突然想起该项目是连接MSSQL数据库和Redis数据库的,并且已经依赖注入了,而Redis数据库的服务 ...
- IdentityServer4源码解析_4_令牌发放接口
目录 identityserver4源码解析_1_项目结构 identityserver4源码解析_2_元数据接口 identityserver4源码解析_3_认证接口 identityserver4 ...
- Python第四章-流程控制
流程控制 在以前的代码中,所有的代码都是交由 Python 忠实地从头执行到结束.但是这些远远不够.很多时候需要根据不同的情况执行不同的代码. 如果你想改变这一工作流程,应该怎么做? 就像这样的情况: ...
- 从JSON中自动生成对应的对象模型
编程的乐趣和挑战之一,就是将体力活自动化,使效率成十倍百倍的增长. 需求 做一个项目,需要返回一个很大的 JSON 串,有很多很多很多字段,有好几层嵌套.前端同学给了一个 JSON 串,需要从这个 J ...
- NeurIPS审稿引发吐槽大会,落选者把荒唐意见怼了个遍:“我谢谢你们了”
七月份的尾巴,机器学习顶会NeurIPS 2019的初步结果已经来了. 一年一度的吐槽盛会也由此开始. "有评审问我啥是ResNet." "有评审问我为啥没引用X论文.我 ...
- TensorFlow系列专题(六):实战项目Mnist手写数据集识别
欢迎大家关注我们的网站和系列教程:http://panchuang.net/ ,学习更多的机器学习.深度学习的知识! 目录: 导读 MNIST数据集 数据处理 单层隐藏层神经网络的实现 多层隐藏层神经 ...