服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C# - C#多线程系列之原子操作

C#多线程系列之原子操作

2022-12-29 13:32痴者工良 C#

本文详细讲解了C#多线程的原子操作,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

知识点

竞争条件

当两个或两个以上的线程访问共享数据,并且尝试同时改变它时,就发生争用的情况。它们所依赖的那部分共享数据,叫做竞争条件。

数据争用是竞争条件中的一种,出现竞争条件可能会导致内存(数据)损坏或者出现不确定性的行为。

线程同步

如果有 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() 等,都是使用内核模式来阻塞线程,实现线程同步(等待)。

内核模式实现线程等待时,出现上下文切换。这适合等待时间比较长的操作,这样会减少大量的 CPU 时间损耗。

如果线程只需要等待非常微小的时间,阻塞线程带来的上下文切换代价会比较大,这时我们可以使用自旋,来实现线程同步,这一方法称为用户模式(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 来解决这些问题,但是还有没有更加简单的方法呢?

首先我们编写以下代码:

?
1
2
3
4
5
6
7
8
private static int sum = 0;
public static void AddOne()
{
    for (int i = 0; i < 100_0000; i++)
    {
        sum += 1;
    }
}

这个方法的工作完成后,sum 会 +100。

我们在 Main 方法中调用:

?
1
2
3
4
5
6
7
8
9
static void Main(string[] args)
{
    AddOne();
    AddOne();
    AddOne();
    AddOne();
    AddOne();
    Console.WriteLine("sum = " + sum);
}

结果肯定是 5000000,无可争议的。

但是这样会慢一些,如果作死,要多线程同时执行呢?

好的,Main 方法改成如下:

?
1
2
3
4
5
6
7
8
9
10
11
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

我们将每次运算的结果保存到数组中,截取其中一段发现:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
8757
8758
8760
8760
8760
8761
8762
8763
8764
8765
8766
8766
8768
8769

多个线程使用同一个变量进行操作时,并不知道此变量已经在其它线程中发生改变,导致执行完毕后结果不符合期望。

我们可以通过下面这张图来解释:

C#多线程系列之原子操作

因此,这里就需要原子操作,在某个时刻,必须只有一个线程能够进行某个操作。而上面的操作,指的是读取、计算、写入这一过程。

当然,我们可以使用 lock 或者 Monitor 来解决,但是这样会带来比较大的性能损失。

这时 Interlocked 就起作用了,对于一些简单的操作运算, Interlocked 可以实现原子性的操作。

实现原子性,可以通过多种锁来解决,目前我们学习到了 lock、Monitor,现在来学习 Interlocked ,后面会学到更加多的锁的实现。

2,Interlocked.Increment()

用于自增操作。

我们修改一下 AddOne 方法:

?
1
2
3
4
5
6
7
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() 实现赋值运算。

这个方法有多个重载,我们找其中一个来看看:

?
1
public static int Exchange(ref int location1, int value);

意思是将 value 赋给 location1 ,然后返回 location1 改变之前的值。

测试:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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() 也有对引用类型的重载:

?
1
Exchange<T>(T, T)

4,Interlocked.CompareExchange()

其中一个重载:

?
1
public static int CompareExchange (ref int location1, int value, int comparand)

比较两个 32 位有符号整数是否相等,如果相等,则替换第一个值。

如果 comparand 和 location1 中的值相等,则将 value 存储在 location1中。 否则,不会执行任何操作。

看准了,是 location1 和 comparand 比较!

使用示例如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 位整数进行求和并用和替换第一个整数,上述操作作为一个原子操作完成。

?
1
public static int Add (ref int location1, int value);

只能对 int 或 long 有效。

回到第一小节的多线程求和问题,使用 Interlocked.Add() 来替换Interlocked.Increment()

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 执行。

?
1
public static long Read (ref long location);

就是说 32 位系统上才用得上。

到此这篇关于C#多线程系列之原子操作的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:https://www.cnblogs.com/whuanle/p/12724371.html

延伸 · 阅读

精彩推荐
  • C#C#调用HTTP POST请求上传图片的示例代码

    C#调用HTTP POST请求上传图片的示例代码

    现在很多B/S系统的开发都是通过API方式来进行的,一般服务端会开放一个API接口,客户端调用API接口来实现图片或文件上传的功能,感兴趣的可以了解一下...

    薄心之心8802022-11-21
  • C#C#在PDF中绘制不同风格类型的文本方法实例

    C#在PDF中绘制不同风格类型的文本方法实例

    这篇文章主要给大家介绍了关于C#在PDF中绘制不同风格类型的文本的相关资料,文中通过图文以及示例代码介绍的非常详细,对大家的学习或者工作具有一...

    E-iceblue8062022-02-25
  • C#C#中File和FileStream的简单介绍和用法

    C#中File和FileStream的简单介绍和用法

    这篇文章主要给大家介绍了关于C#中File和FileStream用法的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需...

    忆汐辰11172022-10-26
  • C#C# 批量生成随机密码必须包含数字和字母并用加密算法加密

    C# 批量生成随机密码必须包含数字和字母并用加密算法加密

    这篇文章主要介绍了C# 批量生成随机密码必须包含数字和字母并用加密算法加密,需要的朋友参考下...

    SweetMemory10762021-12-20
  • C#C#byte数组传入C操作方法

    C#byte数组传入C操作方法

    在本篇内容中小编给大家分享了关于C#byte数组传入C操作方法以及相关知识点,需要的朋友们学习下。...

    C#教程网9182022-07-06
  • C#C#实现复制数据库 C#将A数据库数据转到B数据库

    C#实现复制数据库 C#将A数据库数据转到B数据库

    这篇文章主要为大家详细介绍了C#复制数据库,将数据库数据转到另一个数据库,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    至浊至愚8802022-02-10
  • C#C#常见应用函数实例小结

    C#常见应用函数实例小结

    这篇文章主要介绍了C#常见应用函数,结合实例形式总结分析了C#常用的时间、URL、HTML、反射、小数运算等相关函数,需要的朋友可以参考下...

    pan_junbiao9332021-12-22
  • C#Unity切割图集转换为多张图片

    Unity切割图集转换为多张图片

    这篇文章主要为大家详细介绍了Unity切割图集转换为多张图片,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    木小星10352022-09-24