在 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 去掉,那才是可怕。因为运行代码,将会发现有时存在线程互等,有时不存在。如果这是发给用户端执行的应用,那将会有用户反馈说为什么有时候应用就啥也不干了,但有时又跑得好好的,说不定这时客服小姐姐的重启搞定一切的大法就能解决这个问题。但是如果刚好是大佬用户遇到了,要求开发者一定要解决,那预计开发者想要复现这个问题,也是很不好玩的,如果进入以上方法的步骤比较多,那大概可以连续多加几天的班,如果再加上逻辑稍微复杂,加班的时候自己不清醒,那预计还是解决不了的
保持静态构造函数的简单,可以解决大量的问题。不要在静态构造函数里面添加复杂的代码,如果真的有这个需求,将这些复杂的代码放在一个静态函数里面,自己寻找合适的时机调用
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po