在 dotnet 的最佳实践里面,不推荐在静态构造函数里面包含复杂的逻辑,其中也就包含了本文聊的和多线程相关的锁的使用。最佳做法是尽量不要在静态构造函数里面碰到任何和锁以及多线程安全相关的逻辑。本文来告诉大家,在静态构造函数里面使用锁将带来的问题以及原因

在 .NET 的设计里面,一个类型的静态构造函数,是在此类型第一次被碰到时将会被 CLR 调用。调用的时候,只允许一个线程执行进入静态构造函数,换句话说是一个类型的静态构造函数不会重复被多个线程执行,只会被执行一次。如此即可保证静态构造函数的安全性

不同于实例构造函数,实例构造函数大部分由代码里面的 new 关键词触发,执行代码的仅有一个线程。如果多个线程调用 new 关键词那么将创建出来不同的实例,分别引用不同的内存空间。如以下代码

var foo = new Foo();

如果有多个线程同时进入,调用到 new Foo() 这句代码,自然是创建出多个不同的实例。这就意味着无论是静态构造函数还是实例构造函数,都是只能被一个线程执行。当然,这是有例外的,由于在 .NET 里面,无论是静态构造函数还是实例构造函数,都是一个函数方法,通过反射,依然可以当成基础的方法调用,因此在使用反射时,以上的说法是不成立的

在不使用反射的黑科技下,保持让构造函数只能由一个线程执行,可以解决十分多的线程同步安全问题

对于实例的构造函数只能由一个线程执行这个十分好理解。由于进入代码里面,不同的线程将会创建出不同的对象,每个对象都有自己的独立的内存空间,独立的内存空间里面执行的实例构造函数执行的过程参数以及字段等都是独立的。实际有两个线程同时调用 new Foo() 代码,两个线程所使用的实例构造函数也是不同的,例如构造函数里面使用的过程参数 this.this 就分别属于不同的两个对象

然而静态构造函数就比较复杂起来的,大家都知道,在没有标记线程静态的前提下,所有的静态字段和属性等都是全局共享的,全局共享的就意味着所有的线程都访问到的相同的对象

如上文所说,一个类型的静态构造函数将在类型第一次被碰到时被 CLR 调用,那如何了解当前是第一次碰到?如果有两个线程同时都碰到呢,此时由哪个线程执行,还是两个线程都要执行?

在静态构造函数被多个线程碰到时,相当于进入了资源竞争,无论是多少个线程同时碰到某个类型,此类型的静态构造函数只能由其中的一个线程执行,而其他线程进入等待过程。相当于进入静态构造函数时设置了一个锁对象,只有一个线程能进入调用静态构造函数,其他线程只能等待静态构造函数执行完成才能继续

多线程在碰到某个类型的静态构造函数时,就和碰到竞态资源一样,也相当于碰到一个锁

然而静态构造函数的多线程安全问题可比其他的竞态资源更加复杂,原因也如上文描述,一个类型的静态构造函数是在这个类型第一次被碰到的时候触发。然而代码里面什么时候是第一次碰到,这个是非常复杂且不可控的,而且也会随着代码的迭代而被变更的。例如当前是十分确定有某个函数碰到了某个类型,然而很快就会因为函数之前的调用顺序变更,从而变更了静态构造函数的初始化时机。或者在代码迭代时,在新的时机更快碰到了某个类型,从而触发了类型的静态构造函数

没有开发者会在写代码的时候,想到碰到某个类型时,需要关注此类型的静态构造函数的初始化时机是否被更改,从而导致了问题。如果真的如此关注了,那代码也写不了了,碰到的每一个类型,都需要关注一下的话,这个开发就不好玩了

这就是为什么最佳实践里面推荐不要在静态构造函数里面放复杂的逻辑,推荐只是做一些简单的初始化逻辑。如此能很大解决因为静态构造函数的时机问题导致的问题,无论什么时候碰到静态构造函数,如果静态构造函数只是做非常简单的和无依赖的逻辑,那自然是没有什么问题

而如果是如本文要聊的,在类型的静态构造函数里面,碰到了锁,那这个故事就开始复杂起来了

无论是什么语言,只要还是在图灵的体系下,只要在玩多线程,那么锁和原子和事务是少不了的。不过这是一个很大的话题,本文只来和大家聊锁与静态构造函数。在使用锁的时候,能带来的优势是提供了一个解决多线程安全问题的方法,带来的问题是多线程安全问题。没错锁是一个会导致的线程安全问题的解决多线程问题的方法,是否会导致问题,完全取决于如何使用。锁不是一个完美的解决方案,如果使用不当,那带来的线程安全问题将会有很多,而且锁的使用注意点也非常多,这就是为什么会有本文的核心原因

在使用锁的最佳实践里面,就有确定性的说法。也就是说何时捕获锁、等待锁,以及合适释放锁都应该是确定的,而不能是不确定的行为,否则轻的话就是线程不安全,资源被意外抢入,重的话就是无限线程互等,应用进入摸鱼状态,啥都不做都在等着锁,或者应用拉满了计算资源疯狂执行

在静态构造函数里面使用锁将违背锁的最佳实践里面的确定性调用这一条,静态构造函数是在类型第一次碰到时被触发,也就是开发者是无法确定静态构造函数合适被调用的。再加上一些代码优化和内联,将会导致调试下和发布下的行为也会不同。再加上代码迭代,静态构造函数的触发时机也是很难进行控制的。在静态构造函数里面使用锁将是一个危险的行为,即使当前版本在调试下是能符合预期工作的,然而在发布的时候,在某些用户的设备上,也许就会遇到奇怪的问题。如果想要提升产品的代码质量,就需要尽量不要在静态构造函数里面使用锁的相关方法,包括直接或间接的调用到锁

举一个例子来告诉大家在静态构造函数里面调用锁的相关方法导致的多线程互等的问题

假设在 Foo 类型的静态构造函数里面需要使用到一个叫 LockObject 对象的锁,而这个 LockObject 对象的锁是有多个类型在调用的,定义代码如下

class Foo2
{
public static void Do(Action action)
{
lock (LockObject)
{
action();
}
} public static readonly object LockObject = new object();
}

此时有 Foo1 类型,在静态构造函数调用了 Foo2 的 Do 方法,代码如下

class Foo1
{
static Foo1()
{
Foo2.Do(() =>
{
// 忽略代码
Number = 0;
});
} public static int Number { get; private set; }
}

以上代码在 Foo1 被第一次碰到的过程中,可能会存在多线程相互等待,例如调用代码如下

        var task1 = Task.Run(() =>
{
Foo2.Do(() =>
{
Thread.Sleep(2000);
GetFoo1Number();
});
}); var task2 = Task.Run(() =>
{
GetFoo1Number();
}); private static int GetFoo1Number()
{
return Foo1.Number;
}

运行代码可以看到 task1 和 task2 在互等,点击暂停,可以看到 task1 和 task2 对应的线程的线程号分别是 9764 和 22044 两个。其调用堆栈分别如下

线程号是 9764 的 task1 的调用堆栈如下

>	Demo.dll!Demo.Foo1.Number.get() 行 67	C#
Demo.dll!Demo.MainWindow.GetFoo1Number() 行 51 C#
Demo.dll!Demo.MainWindow..ctor.AnonymousMethod__0_2() 行 35 C#
Demo.dll!Demo.Foo2.Do(System.Action action) 行 76 C#

线程号是 22044 的 task2 的调用堆栈如下

 	[正在等待线程 锁定 拥有的 9764,双击或按 Enter 可切换到线程]
System.Private.CoreLib.dll!System.Threading.Monitor.Enter(object obj, ref bool lockTaken) 未知
> Demo.dll!Demo.Foo2.Do(System.Action action) 行 74 C#
Demo.dll!Demo.Foo1.Foo1() 行 60 C#
[本机到托管的转换]
[托管到本机的转换]
Demo.dll!Demo.Foo1.Number.get() 行 67 C#

也就是说 task1 在尝试拿到 Foo1 的 Number 属性,需要先等待 Foo1 的静态构造函数执行完成。然而 Foo1 的静态构造函数是在 task2 对应的线程执行,而 Foo1 的静态构造函数碰到的 Foo2 的 LockObject 对象的锁被 task1 对应的线程获取。因此想要让 Foo1 的静态构造函数能继续执行,就需要等待 task1 线程释放锁对象。然而 task1 要释放锁对象的前提是能获取完成 Foo1 的 Number 属性。但是获取 Foo1 的 Number 属性需要等待在 task2 上执行的 Foo1 的静态构造函数执行完成

也就是说在 task1 上执行的代码,需要等待 task2 执行完成,才能释放锁。在 task2 上执行的代码,需要等待 task1 释放锁才能执行完成。完美让两个线程进入互等

这就是其中的一个线程不安全的例子。如果将 task1 里面的 Thread.Sleep 去掉,那才是可怕。因为运行代码,将会发现有时存在线程互等,有时不存在。如果这是发给用户端执行的应用,那将会有用户反馈说为什么有时候应用就啥也不干了,但有时又跑得好好的,说不定这时客服小姐姐的重启搞定一切的大法就能解决这个问题。但是如果刚好是大佬用户遇到了,要求开发者一定要解决,那预计开发者想要复现这个问题,也是很不好玩的,如果进入以上方法的步骤比较多,那大概可以连续多加几天的班,如果再加上逻辑稍微复杂,加班的时候自己不清醒,那预计还是解决不了的

保持静态构造函数的简单,可以解决大量的问题。不要在静态构造函数里面添加复杂的代码,如果真的有这个需求,将这些复杂的代码放在一个静态函数里面,自己寻找合适的时机调用

dotnet 谨慎在静态构造函数里使用锁的更多相关文章

  1. C#的静态构造函数

    “静态构造函数”典型应用于第一次使用类时的初始化工作,注意“第一次”,意思是它只执行一次. 有同学说了,类的初始化不是有构造函数嘛?我们回答:构造函数是每个实例被声明时都会执行的,它属于每一个实例,而 ...

  2. C#中的静态构造函数

    https://msdn.microsoft.com/en-us/library/k9x6w0hc(v=vs.140).aspx A static constructor is used to ini ...

  3. c#只读字段和常量的区别,以及静态构造函数的使用 .

    using System;using System.Collections.Generic;using System.Linq;using System.Text; namespace Console ...

  4. 【转载】关于C#静态构造函数的几点说明

    一.定义 静态构造函数是C#的一个新特性,其实好像很少用到.不过当我们想初始化一些静态变量的时候就需要用到它了.这个构造函数是属于类的,而不是属于哪里实例的,就是说这个构造函数只会被执行一次.也就是在 ...

  5. MVC5中Model层开发数据注解 EF Code First Migrations数据库迁移 C# 常用对象的的修饰符 C# 静态构造函数 MSSQL2005数据库自动备份问题(到同一个局域网上的另一台电脑上) MVC 的HTTP请求

    MVC5中Model层开发数据注解   ASP.NET MVC5中Model层开发,使用的数据注解有三个作用: 数据映射(把Model层的类用EntityFramework映射成对应的表) 数据验证( ...

  6. 静态构造函数c# 静态块java initallize oc

    静态构造函数c# 静态块java initallize oc 先看一道常见题目,以下代码的执行结果是什么? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1 ...

  7. C# 静态构造函数,静态变量执行顺序(升华版)

    上篇 是基本语法基础下的执行顺序,包括继承这个维度下的执行顺序,我们可以依照的规律顺下来,下面我们看下一些摸不到头脑的情况 我们实验 一个 类中的方法 去调用另一个非继承类的情况,  我们主要看下  ...

  8. 为什么不允许使用 Java 静态构造函数?

    不允许使用 Java 静态构造函数,但是为什么呢?在深入探讨不允许使用静态构造函数的原因之前,让我们看看如果要使 构造函数静态化 会发生什么. Java 静态构造函数 假设我们有一个定义为的类: pu ...

  9. c#静态构造函数 与 构造函数 你是否还记得?

    构造函数这个概念,在我们刚开始学习编程语言的时候,就被老师一遍一遍的教着.亲,现在你还记得静态构造函数的适用场景吗?如果没有,那么我们一起来复习一下吧. 静态构造函数是在构造函数方法前面添加了stat ...

  10. 深入了解C#中的静态变量和静态构造函数

    深入的剖析C#中静态变量和静态构造函数: 在日常的程序开发过程经常会使用到静态变量,众所周知,静态变量时常驻内存的变量,它的生命周期是从初始化开始一直到Application结束.但是,我们经常会忽略 ...

随机推荐

  1. 记录--开局一张图,构建神奇的 CSS 效果

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 假设,我们有这样一张 Gif 图: 利用 CSS,我们尝试来搞一些事情. 图片的 Glitch Art 风 在这篇文章中 --CSS 故障 ...

  2. Jmeter的Throughput有误差与分布式测试时的坑

    我是两台压力机,分布式启动jmeter压测180秒,结果throughput显示3075,我用总请求数/总耗时,64万左右/180秒,得到的TPS是3500左右.误差17% 网上说jmeter的thr ...

  3. JDK8 ::用法(双冒号)

    JDK8中有双冒号的用法,就是把方法当做参数传到stream内部,使stream的每个元素都传入到该方法里面执行一下. List<String> lt = Arrays.asList(&q ...

  4. c语言的printf常用的一些转换说明符及其含义

    整数类型: %d: 十进制整数 (decimal: 十进制的) %u: 无符号整数 (unsigned: 无符号的) %i: 十进制整数 (integer: 整数) %o: 八进制数 (octal: ...

  5. Tarjan 算法——图论学习笔记

    Part.1 引入 在图论问题中,我们经常去研究一些连通性问题,比如: 有向图的联通性:传递闭包--Floyd 算法: 有向图连通性的对称性:强联通分量(SCC)--Tarjan 算法缩点: 无向图的 ...

  6. 为什么js项目中金额强烈推荐使用分而不是元

    相信我们都已经知道在js中浮点数据精度的问题了 看下面的例子 0.1 + 0.2 0.30000000000000004 如何解决呢? 在前后端交互过程中统一使用分为单位进行通讯,在最后的表示层处理为 ...

  7. #贪心#洛谷 6093 [JSOI2015]套娃

    题目 分析 按好看度从大到小排序,每次选择一个尽量大的外径装入当前套娃的内径, 这样可以保证是最优的,删除选完的外径可以用平衡树实现 代码 #include <cstdio> #inclu ...

  8. #Multi-SG#HDU 5795 A Simple Nim

    题目 有\(n\)堆石子,每次可以从一堆中取出若干个或是将一堆分成三堆非空的石子, 取完最后一颗石子获胜,问先手是否必胜 分析 它的后继还包含了分成三堆非空石子的SG函数,找规律可以发现 \[SG[x ...

  9. #树状数组#洛谷 4113 [HEOI2012]采花

    题目 分析 与HH的项链类似 离线处理询问,按右端点排序,维护最近的颜色和第二近的颜色,修改以第二近的颜色为准 换句话说,若最近颜色的位置为\(pos2\),第二近颜色的位置为\(pos1\) 加入一 ...

  10. Web服务器启用HTTPS的配置方法

    本文于2016年3月完成,发布在个人博客网站上. 考虑个人博客因某种原因无法修复,于是在博客园安家,之前发布的文章逐步搬迁过来. nginx的配置方法 可以参考Jerry Qu的本博客 Nginx 配 ...