• 线程安全知多少


    1. 如何定义线程安全

    线程安全,拆开来看:

    • 线程:指多线程的应用场景下。
    • 安全:指数据安全。

    多线程就不用过多介绍了,相关类型集中在System.Threading命名空间及其子命名空间下。
    数据,这里特指临界资源
    安全,简单来说就是多线程对某一临界资源进行并发操作时,其最终的结果应和单线程操作的结果保持一致。比如Parallel线程安全问题就是说的这个现象。

    2. 如何判断是否线程安全

    在查MSDN是,我们经常会看到这样一句话:

    Thread Safety
    Public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.

    直译过来就是说:该类型的公共静态成员是线程安全的,但其对应的任何实例成员不确保是线程安全的。

    你可能对这句话还是丈二和尚摸不着头脑。我也是。那现在我们来理一下。

    首先,我们来理清一下类型和类型成员:
    类型是指类本身,类型成员是指类所包含的方法、属性、字段、索引、委托、事件等。类型成员又分为静态成员和实例成员。静态成员,顾名思义就是static关键字修饰的成员。实例成员,就是对类型实例化创建的对象实例才能访问到的成员。

    然后,为什么它可以确保所有的公共静态成员是线程安全的呢?是因为它一定通过某种机制,去确保了公共静态成员的线程安全。(这一定是微软源码的一个规范)。
    那显而易见,对实例成员,可能由于没有了这样的一个限制,才会说,不确保实例成员是线程安全的。

    以上只是我个人的一种猜测。那显然仅仅是有猜测还是不够的,我们要验证它。而最直接有力的方法莫过于查源码了。

    2.1. StopWatch源码分析

    我们看下System.Diagnostics.StopWatch的源码实现。

    在这个类中,主要有以下几个公共静态成员:

    1. public static readonly long Frequency;
    2. public static readonly bool IsHighResolution;
    3. public static Stopwatch StartNew() {//.....}
    4. public static long GetTimestamp() { //....}

    首先前两个公共静态字段因为被readonly修饰,只读不可写,所以是线程安全的。
    后面两个静态方法因为没有涉及到对临界资源的操作,所以也是线程安全的。
    那针对这个StopWatch来说,保证线程安全的机制是:

    1. 使用readonly修饰公共静态字段
    2. 公共静态方法中不涉及对临界资源的操作。

    2.2. ArrayList源码分析

    我们再来看下System.Collections.ArrayList的源码实现。

    这个类中,公共静态成员主要是几个静态方法,我简单列举一个:

            public static IList ReadOnly(IList list) {
                if (list==null)
                    throw new ArgumentNullException("list");
                Contract.Ensures(Contract.Result<IList>() != null);
                Contract.EndContractBlock();
                return new ReadOnlyList(list);
            }
    

    这一个静态方法主要用来创建只读列表,因为不涉及到临界资源的操作,所以线程安全,其他几个静态方法类似。

    我们再来看一个公共实例方法:

            private Object[] _items;
            private int _size;
            private int _version;
            public virtual int Add(Object value) {
                Contract.Ensures(Contract.Result<int>() >= 0);
                if (_size == _items.Length) EnsureCapacity(_size + 1);
                _items[_size] = value;
                _version++;
                return _size++;
            }
    

    很显然,对集合进行新增处理时,我们涉及到对临界资源_items的操作,但是这里却没有任何线程同步机制去确保线程安全。所以其实例成员不确保是线程安全的。

    2.3. ConcurrentBag源码分析

    仅有以上两个例子,不足以验证我们的猜测。接下来我们来看看线程安全集合System.Collections.Concurrent.ConcurrentBag的源码实现。
    首先我们来看下MSDN中对ConcurrentBag线程安全的描述:

    Thread Safety
    All public and protected members of ConcurrentBag are thread-safe and may be used concurrently from multiple threads. However, members accessed through one of the interfaces the ConcurrentBag implements, including extension methods, are not guaranteed to be thread safe and may need to be synchronized by the caller.

    这里为什么可以自信的保证所有public和protected 成员是线程安全的呢?

    同样,我们还是来看看对集合进行Add的方法实现:

            public void Add(T item)
            {
                // Get the local list for that thread, create a new list if this thread doesn't exist 
                //(first time to call add)
                ThreadLocalList list = GetThreadList(true);
                AddInternal(list, item);
            }
    
            private ThreadLocalList GetThreadList(bool forceCreate)
            {
                ThreadLocalList list = m_locals.Value;
     
                if (list != null)
                {
                    return list;
                }
                else if (forceCreate)
                {
                    // Acquire the lock to update the m_tailList pointer
                    lock (GlobalListsLock)
                    {
                        if (m_headList == null)
                        {
                            list = new ThreadLocalList(Thread.CurrentThread);
                            m_headList = list;
                            m_tailList = list;
                        }
                        else
                        {
     
                            list = GetUnownedList();
                            if (list == null)
                            {
                                list = new ThreadLocalList(Thread.CurrentThread);
                                m_tailList.m_nextList = list;
                                m_tailList = list;
                            }
                        }
                        m_locals.Value = list;
                    }
                }
                else
                {
                    return null;
                }
                Debug.Assert(list != null);
                return list;
     
            }
     
            /// <summary>
            /// </summary>
            /// <param name="list"></param>
            /// <param name="item"></param>
            private void AddInternal(ThreadLocalList list, T item)
            {
                bool lockTaken = false;
                try
                {
    #pragma warning disable 0420
                    Interlocked.Exchange(ref list.m_currentOp, (int)ListOperation.Add);
    #pragma warning restore 0420
                    //Synchronization cases:
                    // if the list count is less than two to avoid conflict with any stealing thread
                    // if m_needSync is set, this means there is a thread that needs to freeze the bag
                    if (list.Count < 2 || m_needSync)
                    {
                        // reset it back to zero to avoid deadlock with stealing thread
                        list.m_currentOp = (int)ListOperation.None;
                        Monitor.Enter(list, ref lockTaken);
                    }
                    list.Add(item, lockTaken);
                }
                finally
                {
                    list.m_currentOp = (int)ListOperation.None;
                    if (lockTaken)
                    {
                        Monitor.Exit(list);
                    }
                }
            }
    

    看了源码,就一目了然了。首先使用lock锁获取临界资源list,再使用Moniter锁来进行add操作,保证了线程安全。

    至此,我们对MSDN上经常出现的对Thread Safety的解释,就不再迷糊了。

    如果你仔细看了ConcurrentBag关于Thread Safety的描述的话,后面还有一句:
    However, members accessed through one of the interfaces the ConcurrentBag implements, including extension methods, are not guaranteed to be thread safe and may need to be synchronized by the caller.
    这又是为什么呢,问题就留给你啦。

    3. 如何保证线程安全

    通过上面分析的几段源码,想必我们心里也有谱了。
    要解决线程安全问题,首先,最重要的是看是否存在临界资源,如果没有,那么就不涉及到线程安全的问题。

    如果有临界资源,就需要对临界资源进行线程同步处理了。而关于线程同步的方式,可参考C#编程总结(三)线程同步

    另外在书写代码时,为了避免潜在的线程安全问题,对于不需要改动的公共静态变量,使用readonly修饰不失为一个很好的方法。

    4. 总结

    通过以上分析,我们知道,在多线程的场景下,对于静态成员和实例成员没有绝对的线程安全,其关键在于是否有临界资源

  • 相关阅读:
    P1865 A % B Problem
    P1198 [JSOI2008]最大数
    从英语学习写起,收获2013属于我的金秋
    Hadoop--有关Hadoop的启动
    android中文字高亮设置案例
    <Win32_16>来看看标准菜单和右键菜单的玩法
    jQuery 自学笔记—3
    【译】如何精确判断最终用户响应时间过长的原因?
    网络爬虫返回json处理数据
    2013--转变
  • 原文地址:https://www.cnblogs.com/sheng-jie/p/8615062.html
Copyright © 2020-2023  润新知