.NET线程同步之SpinLock构造

来源:互联网 发布:星空卫视直播软件下载 编辑:程序博客网 时间:2024/06/03 13:21

接着上一篇博客讨论了.NET线程同步之Interlocked构造,本篇博客来讨论一下SpinLock构造。

Interlocked构造虽然很好用,但是它只是对一个变量或一个字段做一个原子操作,如果我们想对将一组操作封装为原子性操作,或者我们希望某段代码任何时候都不能在多个线程中同时运行,就可以使用SpinLock。

我想到一个应用场景。System.Collections.Generic.Stack<T>类是线程不安全的。Stack类内部维护了一个链表,push方法会增加一个元素到链表,让头指针指向它,并修改链表长度,如果多个线程同时修改头指针,会造成元素丢失,如果多个线程同时更新链表长度,会造成更新丢失。pop方法会移动头指针以删除栈顶的元素并更新链表长度,如果多个线程同时操作的话也会有同样的问题。这就是它为什么线程不安全。我使用SpinLock实现了一个简单的支持并发的栈,咱们一起体会一下SpinLock的用法。

public class MyConcurrentStack<T>{    public int Length { get; set; } = 0;    private MyConcurrentStackItem<T> Head { get; set; } = new MyConcurrentStackItem<T>();    private SpinLock spinLock = new SpinLock();    /// <summary>    /// 入栈    /// </summary>    /// <param name="value">Value</param>    public void Push(T value)    {        bool lockTocken = false;        try        {            //请求锁            spinLock.Enter(ref lockTocken);            MyConcurrentStackItem<T> item = new MyConcurrentStackItem<T>();            item.Value = value;            item.Next = Head.Next;            Head.Next = item;            Length++;        }        finally        {            if (lockTocken)            {                //释放锁                spinLock.Exit();            }        }    }    public T Pop()    {        bool lockTocken = false;        try        {            //请求锁            spinLock.Enter(ref lockTocken);            if (Head.Next == null)            {                throw new InvalidOperationException("栈内没有元素,无法出栈");            }            MyConcurrentStackItem<T> top = Head.Next;            Head.Next = Head.Next.Next;            Length--;            return top.Value;        }        finally        {            if (lockTocken)            {                //释放锁                spinLock.Exit();            }        }    }    /// <summary>    /// 返回所有栈内元素    /// </summary>    /// <returns>The list.</returns>    public List<T> ToList()    {        List<T> itemList = new List<T>();        MyConcurrentStackItem<T> temp = Head.Next;        while (temp != null)        {            itemList.Add(temp.Value);            temp = temp.Next;        }        return itemList;    }    /// <summary>    /// 内部类,封装一个栈元素    /// </summary>    public class MyConcurrentStackItem<T>    {        public T Value { get; set; }        public MyConcurrentStackItem<T> Next { get; set; }    }}

这个栈内部维护了一个MyConcurrentStackItem<T>类型的栈,每次入栈和出栈都使用SpinLock请求同步锁,成功地拿到锁之后,再进行一组操作(移动头指针、更细Length等),操作完成后释放同步锁,保证了这些操作在任何时间都不能并发执行。

下面的代码测试了MyConcurrentStack<T>的并发效果。

public class Class1{    static MyConcurrentStack<string> myStack = new MyConcurrentStack<string>();    static void Main(string[] args)    {        Parallel.For(0, 100, i => Method1());        Console.WriteLine("the final length of stack is " + myStack.Length);        List<string> strList = myStack.ToList();        Console.WriteLine("the real items' count is " + strList.Count);    }    static void Method1()    {        myStack.Push("hello");        myStack.Push("world");        string str = myStack.Pop();        //Console.WriteLine("pop value : {0}", str);    }}

Method1()方法入栈两个元素再出栈一个元素,通过Parallel.For来启动100个任务来运行Method1()方法,之后线程池会分配若干个线程来运行任务,在这种并发情况下,最终输出栈的Length属性和栈内元素数,如果多次运行的结果都是这两个数量一致,则可基本证明入栈和出栈操作使线程安全的。

下面重点介绍一下SpinLock的用法和特点。

SpinLock.Enter()会传递一个lockTocken,这个lockTocken在传递给Enter方法之前必须是false,Enter方法返回后,lockTocken会变成true。lockTocken是一个局部临时变量,每次调用Enter方法,一般都会用一个新的lockTocken。

如果多个线程同时调用同一个SpinLock实例的Enter方法的话,只有一个线程会成功地返回,其它的线程会不停地请求,原地打转,这也是它被叫做SpinLock(一般被翻译为回旋锁)的原因。Enter方法的内部使用了SpinWait结构来确保不停原地打转的线程不会阻碍那个拥有锁的线程的运行。当拥有锁的线程执行完相关的操作后,会调用Exit方法来释放锁,从而使原地打转的线程能够非常快地拿到锁。

SpinLock跟大家熟悉的lock关键字的执行方法非常不一样,SpinLock每次请求同步锁的效率非常高,但如果请求不到的话,会一直请求而浪费CPU时间,所以它适合那种并发程度不高、竞争性不强的场景。而lock关键字每次请求同步锁的代价比较大,但如果请求失败,当前线程会阻塞而让出宝贵的CPU时间,直到锁被释放以后线程被唤醒,它更适合并发程度高、竞争性很强的场景。

原创粉丝点击