惟有于技术中才能明得真相

留心类库的线程安全!

在最近的项目中看人使用使用一个单例SHA1Managed类来计算字节流的hash值,hash的作用在于防止用户篡改数据,这非Object.GetHashCode()方法得到的hash值,后者主要用于集合框架中。这里的问题在于使用了单例,因为SHA1Managed不是线程安全的,而在一个典型Web应用都运行在多线程环境下,在多线程环境下可能致使SHA1Managed的内部状态遭到破坏,这种破坏一旦发生便不能恢复,以后都会得到错误的运行结果。关于SHA1Managed的非线程安全性,MSDN其实是有说明的,并且是专有一栏,但如果程序员不关注线程安全的话,确实是很难注意到它的。MSDN是这样说的:

Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.
光说无用,我们可以写程序来验证一下:


using System.Threading;
using System.Security.Cryptography;
using System.Text;

public class MyClass
{
    private static readonly HashAlgorithm alg = new SHA1Managed();
    private static readonly byte[] data = createBytes(10000);
    private static readonly byte[] expectedHash = alg.ComputeHash(data);
   
    private static byte[] createBytes(int len) {
        byte[] bytes = new byte[len];
        for (int i = 0; i < len; i++) {
            bytes[i] = (byte)i;
        }
        return bytes;
    }
   
    public static void Main()
        Thread[] threads = new Thread[100];
        for (int i = 0; i < threads.Length; i++) {
            threads[i] = new Thread(ThreadMain);
        }
        for (int i = 0; i < threads.Length; i++) {
            threads[i].Start();
        }
        for (int i = 0; i < threads.Length; i++) {
            threads[i].Join();
        }
        Console.WriteLine("End All Threads");
    }

    private static bool Equals(byte[] bytes1, byte[] bytes2) {
        if (bytes1.Length != bytes2.Length) return false;
       
        for (int i = 0; i < bytes1.Length; i++) {
            if (bytes1[i] != bytes2[i]) return false;
        }
        return true;
    }
   
    static void ThreadMain() {
        for (int i = 0; i < 1000; i++) {
            byte[] hashedBytes = alg.ComputeHash(data);
            if (!Equals(expectedHash, hashedBytes)) {
    throw new Exception("thread safe vilotation");
            }
        }
    }
}


该类中首先创建的SHA1Managed一个单例,一个大小为10000的字节数组,然后计算它的hash值,在多线程环境中计算出来的hash值会与这个hash值进行比较,如果不同则表明
SHA1Managed的确不能在多线程环境中共享。在Main方法中同时启动了100个线程,每个线程调用SHA1Managed计算hash值100次,如果检测到计算错误,则抛出异常,程序退出。我在公司运行的结果是会出错的(这里没有贴出运行结果主要在于家里没有装.NET),这也就证实了SHA1Managed是非线程安全的。将字节数组的大小的改成1000,运行并不会出错,但这也并不能证明SHA1Managed对小数据就是线程安全的,只是说明对很小数据很难检测线程问题。其实不仅SHA1Managed是非线程安全的,在Cryptography命名空间下的大多数类都是非线程安全的,因此也不能在多线程环境中共享实例。要解决这个问题也很简单,就是不要共享实例,每个线程使用单独的实例,这对SHA1Managed不成问题,因为创建SHA1Managed对象的开销很小。之所以程序员会犯这样的错误,主要是想当然地认为SHA1Managed是不可变对象,从它的行为来看,它的确很像不可变对象,并且从理论上说也的确可以做到,但事实上它不是,这就是灾难的开始。在Java中也有一个很容易犯类似错误的类,那就是DateFormat,大概很少有程序员注意到它不是线程安全的(我也曾看到过项目中有在Util类使用DateFormat单例的),这在文档中也有详细说明。

这两个类没有做成线程安全,从理论上来说他们也可以做到线程安全,那是不是API的设计者的失误呢?设计者不将它们做成线程安全的,当然是有他们的原因的。这两个类的共同特点就是算法比较复杂,在计算过程中很可能涉及到许多中间变量,它们在方法之间也会用到,如果设计成线程安全的,就需要在方法之间将传递这些参数,这使得程序实现起来很麻烦,不仅要传递这么参数,并且每添加删除一个中间变量,都会涉及很多函数的修改,不如将它们都放在实例变量中,这样就不用在方法之间来回传递这些参数了,带来的后果就是多线程不再安全了。另外一个原因是,像这样复杂的算法有很多是从C语言中移植过来的,而C语言是没有考虑多线程安全的。

这样违反直觉的设计的确给程序员带来一些麻烦,尽管这样的例子不多,我们平时也要留心它们,因为这样的问题一旦发生就很难检测。

JQuery源代码阅读(一)

我使用的JQuery版本是1.3.2,先来看下jQuery对象的构造函数。


构造函数在第24行, 它被同时赋给window.jQuery和window.$,因此jQuery和$两者等价,构造函数直接调用jQuery.fn.init方法。init方法根据参数类型接受几种不同形式的方法调用,在第41行根据selector是否有nodeType属性来判断selector是否是DOMElement,如果是则将该JQuery对象设置为只包含一个元素,也就是selector,另外将上下文对象(context)也设置为selector。 在该对象48行,如果selector是一个字符串,则会根据selector是HTML字符串(如"<p>some content</p>")还是CSS选择器(如"div#myClass")来执行不同的逻辑,这在后面会详述。在82行,如果selector是一个函数,调用jQuery(document).ready(selector),表示在document的DOM树加载完成之后执行该函数。在86到88行作用是当selector也是一个JQuery对象时,将该JQuery对象的selector和context属性也复制过来。在91行处,selector一般是一个包含多个DOMElement的数组,如果不是则构造一个大小为1的数组,其唯一元素为该selector(makeArray实现),然后将selector数组的内容复制到jQuery对象中,这样jQuery表现得像数组,它有length属性,可以通过[0]...[length-1]来访问各个元素的内容,这个功能由setArray完成。setArray的实现很巧妙,它调用Array.prototype.push方法:



jQuery对象并不复杂,它包含三个属性:包含的DomElement数组,selector属性和context属性。将DomElement数组称为一个属性并不恰当,因为jQuery对象实际上包含length属性,以及名称0, 1, ..., length-1的属性。selector属性是一个CSS选择器,它是一个字符串,只有当调用jQuery构造函数时,selector参数为一个CSS选择器时,它的值才会被设置,否则就是空字符串("")。context属性则为jQuery对象的上下文对象,我的理解也可以称做“根对象”,当jQuery根据CSS选择器来选择匹配DOMElement时,只会搜索以context元素为根的DOM树。当构造HTML字符串构造jQuery对象时没有context忏悔。复杂的地方不在jQuery对象本身,而在于selector为字符串是它的构造过程,尤其是当selector为CSS选择器的时候。现在来看看上面被折叠的代码(48行到82行之间的代码):



在53行if判断:selector是HTML字符串(match[1]不为空)或者ID(match[3]不为空)时,或者没有指定context对象。接下来在56行,如果selector是HTML字符串,则调用clean方法将HTML字符串转化成DOMElement。clean方法实现比较复杂(这里就不列出源代码了),主要是因为要处理各种浏览器的兼容问题,但总体步骤也简单。首先它判断HTML是否为简单的单个元素(如<p>, <div/>等,有没有/并不重要),则直接调用document.createElement来创建该元素。否则,则调用document.createElement("DIV")来创建一个div元素,然后设置它的innerHTML为该HTML字符串,最后调用div元素的childNodes来得到DomElement数组。第60行处理selector为ID选择器的情形(#someID),主要是为了优化,因为这种情形出现得最多,61行到66的复杂之处是要处理IE的兼容问题,69到72行创建jQuery对象,并设置context,seletor属性。第77行处理selector为CSS选择器的情形,调用jQuery的find的方法,这是jQuery最复杂的部分,留到下次再说。

终于可以看Youtube了

一直使用Tor+Firefox+FoxyProxy来翻墙,除了速度有点慢,却也还能忍受。无奈Youtube却一直没法访问,每次打开视频都出现错误:“An error occurred, Please try again later.”。通过启用Firebug的跟踪网络请求,可以发现Youtube主要会请求两个地址:*.youtube.com/*和*.ytimg.com/*,但我将这两个地址添加到FoxProxy模板中后仍然不能访问Youtube。

今天闲着无聊参考了这篇文章,我对其中讲的创建绿色Firefox和使用PAC来配置IE代理比较感兴趣,按着这篇文章将IE代理配置好之后,竟意外地发现在IE下可以看Youtube了。我又拿Firefox来试可仍然不行,我想到可能是因为我的Firefox配置的是Socks代理,而在IE下配置的是HTTP代理。于是我将Firefox也改成使用HTTP代理,果然也可以访问Youtube了,看来问题的症结就在Socks代理。后来我又想了下,觉得可能不是Socks代理的问题,而是FoxyProxy的问题,我在Firefox下又装了个AutoProxy插件,使用Socks代理,却可以访问Youtube,证明确实是FoxyProxy的问题。但是我也没有因此换成AutoProxy,因为AutoProxy在自动重定向时不会使用代理,这会导致“Connection Reset”,再次刷新便好,可我心里极为讨厌Connection Reset,也就容忍不了这个小瑕疵。其实这个Bug早就有了,不知道为什么现在还没有修复。

总结一下,要使用Tor+Firefox+FoxyProxy看Youtube,必须使用HTTP代理,这需要启动Privoxy,或者使用Tor+Firefox+AutoProxy,如果你尚能容忍偶尔出现的Connection Reset。

test

inactive