在 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. Three.js实现可透视的水面效果

    1. 引言 Three.js是著名的JavaScript 3D图形库,用于浏览器中开发 3D 交互场景的 JS 引擎,可以快速的搭建三维场景 Three.js官网为:创建一个场景 – three.js ...

  2. vue要做权限管理该怎么做?如果控制到按钮级别的权限怎么做?

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 一.是什么 权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源 而前端权限归根结底是请求的发起权,请求的发起可 ...

  3. Python 汇总列数据到行

    Python汇总Excel列数据到行(方法一) import pandas as pd # 读取Excel文件 df = pd.read_excel('C:\\Users\\liuchunlin2\\ ...

  4. LOTO示波器实测过压保护芯片LP5300工作效果

      过压保护电路是电子产品设置中经常要用到的,以前都是用分立元件搭的各种经典电路,最近LOTO虚拟示波器客户推荐了一款很便宜的集成的过压保护芯片LP5300,体积很小,使用简单,外接两个电容就可以了, ...

  5. C# OpenCvSharp MatchTemplate 多目标匹配

    效果 项目 代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.D ...

  6. 前端ajax调用后端下载Excel模板流,解决输出乱码等问题

    JavaScript方法function importTemplate() { $.ajax({ url: "/importTemplate", type: "get&q ...

  7. KingabseES例程-函数和过程的 INVOKER 与 DEFINER

    调用者权利和定义者权利子句 指定子程序的权利属性.权利属性影响单元在运行时,执行的SQL语句的名称解析和权限检查. PG模式: SECURITY INVOKER SECURITY DEFINER Or ...

  8. java实战字符串4:寻找最长的元音子串的长度

    题目描述 定义:当一个字符串只有元音字母(aeiouAEIOU)组成,称为元音字符串.现给定一个字符串,请找出其中最长的元音字符子串,并返回其长度:如果找不到,则返回0. 子串:字符串中任意个连续的字 ...

  9. Java实现哈希表

    2.哈希表 2.1.哈希冲突 冲突位置,把数据构建为链表结构. 装载因子=哈希表中的元素个数 / (散列表)哈希表的长度 装载因子越大,说明链表越长,性能就越低,那么哈希表就需要扩容,把数据迁移到新的 ...

  10. 开启 Keep-Alive 可能会导致http 请求偶发失败

    大家好,我是蓝胖子,说起提高http的传输效率,很多人会开启http的Keep-Alive选项,这会http请求能够复用tcp连接,节省了握手的开销.但开启Keep-Alive真的没有问题吗?我们来细 ...