C# 并行编程 之 自旋锁的使用

基本信息

如果持有锁的时间非常短,而且锁的粒度很精细,那么使用自旋锁会获得更好的性能。有时候,Monitor互斥锁的开销还是相当大的。但SpinLock 的与Monitor的使用形式还是基本类似的。

一段简单的代码示例:

using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Sample5_4_spinlock
{
    class Program
    {
        private static int _TaskNum = 3;
        private static Task[] _Tasks;
        private static StringBuilder _StrBlder;
        private const int RUN_LOOP = 50;

        private static SpinLock m_spinlock;

        private static void Work1(int TaskID)
        {
            int i = 0;
            string log = "";
            bool lockToken = false;
            while (i < RUN_LOOP)
            {
                log = String.Format("Time: {0}  Task : #{1}  Value: {2}  =====\n",
                                    DateTime.Now.TimeOfDay, TaskID, i);
                i++;
                try
                {
                    lockToken = false;
                    m_spinlock.Enter(ref lockToken);
                    _StrBlder.Append(log);
                }
                finally
                {
                    if (lockToken)
                        m_spinlock.Exit(false);
                }
            }
        }

        private static void Work2(int TaskID)
        {
            int i = 0;
            string log = "";
            bool lockToken = false;

            while (i < RUN_LOOP)
            {
                log = String.Format("Time: {0}  Task : #{1}  Value: {2}  *****\n",
                                    DateTime.Now.TimeOfDay, TaskID, i);
                i++;
                try
                {
                    lockToken = false;
                    m_spinlock.Enter(ref lockToken);
                    _StrBlder.Append(log);
                }
                finally
                {
                    if (lockToken)
                        m_spinlock.Exit(false);
                }
            }
        }

        private static void Work3(int TaskID)
        {
            int i = 0;
            string log = "";
            bool lockToken = false;

            while (i < RUN_LOOP)
            {
                log = String.Format("Time: {0}  Task : #{1}  Value: {2}  ~~~~~\n",
                                    DateTime.Now.TimeOfDay, TaskID, i);
                i++;
                try
                {
                    lockToken = false;
                    m_spinlock.Enter(ref lockToken);
                    _StrBlder.Append(log);
                }
                finally
                {
                    if (lockToken)
                        m_spinlock.Exit(false);
                }
            }
        }

        static void Main(string[] args)
        {
            _Tasks = new Task[_TaskNum];
            _StrBlder = new StringBuilder();
            m_spinlock = new SpinLock();


            _Tasks[0] = Task.Factory.StartNew((num) =>
            {
                var taskid = (int)num;
                Work1(taskid);
            }, 0);

            _Tasks[1] = Task.Factory.StartNew((num) =>
            {
                var taskid = (int)num;
                Work2(taskid);
            }, 1);

            _Tasks[2] = Task.Factory.StartNew((num) =>
            {
                var taskid = (int)num;
                Work3(taskid);
            }, 2);

            var finalTask = Task.Factory.ContinueWhenAll(_Tasks, (tasks) =>
            {
                Task.WaitAll(_Tasks);
                Console.WriteLine("==========================================================");
                Console.WriteLine("All Phase is completed");
                Console.WriteLine("==========================================================");
                Console.WriteLine(_StrBlder);
            });

            try
            {
                finalTask.Wait();
            }
            catch (AggregateException aex)
            {
                Console.WriteLine("Task failed And Canceled" + aex.ToString());
            }
            finally
            {
            }
            Console.ReadLine();
        }
    }
}

在每个任务的finally块中,会调用SpinLock的release,否则在SpinLock.Enter中,程序会在循环中不断的尝试获得锁,造成死锁。一旦获得了锁,ref 的 LockToken会被变成 true。

使用超时

SpinLock 同样也提供了超时机制供开发使用。

一个程序示例,worker 1会造成超时,work 2 和work 3则在程序中捕获超时产生的异常,终止运行。

using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Sample5_5_spinlock_timeout
{
    class Program
    {
        private static int _TaskNum = 3;
        private static Task[] _Tasks;
        private static StringBuilder _StrBlder;
        private const int RUN_LOOP = 50;
        private static SpinLock m_spinlock;


        private static void Work1(int TaskID)
        {
            int i = 0;
            string log = "";
            bool lockToken = false;
            while (i < RUN_LOOP)
            {
                log = String.Format("Time: {0}  Task : #{1}  Value: {2}  =====\n",
                                  DateTime.Now.TimeOfDay, TaskID, i);
                i++;
                try
                {
                    lockToken = false;
                    m_spinlock.TryEnter(2000, ref lockToken);
                    if (!lockToken)
                    {
                        Console.WriteLine("Work1 TIMEOUT!! Will throw Exception");
                        throw new TimeoutException("Work1 TIMEOUT!!");
                    }
                    System.Threading.Thread.Sleep(5000);
                    _StrBlder.Append(log);
                }
                finally
                {
                    if (lockToken)
                        m_spinlock.Exit(false);
                }
            }
        }

        private static void Work2(int TaskID)
        {
            int i = 0;
            string log = "";
            bool lockToken = false;

            while (i < RUN_LOOP)
            {
                log = String.Format("Time: {0}  Task : #{1}  Value: {2}  *****\n",
                                  DateTime.Now.TimeOfDay, TaskID, i);
                i++;
                try
                {
                    lockToken = false;
                    m_spinlock.TryEnter(2000, ref lockToken);
                    if (!lockToken)
                    {
                        Console.WriteLine("Work2 TIMEOUT!! Will throw Exception");
                        throw new TimeoutException("Work2 TIMEOUT!!");
                    }

                    _StrBlder.Append(log);
                }
                finally
                {
                    if (lockToken)
                        m_spinlock.Exit(false);
                }
            }
        }

        private static void Work3(int TaskID)
        {
            int i = 0;
            string log = "";
            bool lockToken = false;

            while (i < RUN_LOOP)
            {
                log = String.Format("Time: {0}  Task : #{1}  Value: {2}  ~~~~~\n",
                                  DateTime.Now.TimeOfDay, TaskID, i);
                i++;
                try
                {
                    lockToken = false;
                    m_spinlock.TryEnter(2000, ref lockToken);
                    if (!lockToken)
                    {
                        Console.WriteLine("Work3 TIMEOUT!! Will throw Exception");
                        throw new TimeoutException("Work3 TIMEOUT!!");
                    }
                    _StrBlder.Append(log);
                }
                finally
                {
                    if (lockToken)
                        m_spinlock.Exit(false);
                }
            }
        }

        static void Main(string[] args)
        {
            _Tasks = new Task[_TaskNum];
            _StrBlder = new StringBuilder();
            m_spinlock = new SpinLock();

            _Tasks[0] = Task.Factory.StartNew((num) =>
            {
                var taskid = (int)num;
                Work1(taskid);
            }, 0);

            _Tasks[1] = Task.Factory.StartNew((num) =>
            {
                var taskid = (int)num;
                Work2(taskid);
            }, 1);

            _Tasks[2] = Task.Factory.StartNew((num) =>
            {
                var taskid = (int)num;
                Work3(taskid);
            }, 2);

            var finalTask = Task.Factory.ContinueWhenAll(_Tasks, (tasks) =>
            {
                Task.WaitAll(_Tasks);
                Console.WriteLine("==========================================================");
                Console.WriteLine("All Phase is completed");
                Console.WriteLine("==========================================================");
                Console.WriteLine(_StrBlder);
            });

            try
            {
                finalTask.Wait();
            }
            catch (AggregateException aex)
            {
                Console.WriteLine("Task failed And Canceled" + aex.ToString());
            }
            finally
            {
            }
            Console.ReadLine();
        }

    }
}

使用基于自旋的等待

如果需要等待某个条件满足的时间很短,而且不希望发生上下文切换,基于自旋的【等待】是一种很好的解决方案。

  • SpinWait : 自旋等待
  • SpinUntil : 等待某个条件发生

如果发生了长时间的自旋,SpinWait会让出底层的时间片,并触发上下文切换。因为长时间的自旋会阻塞优先级更高的线程。当一个线程自旋时,它会将一个内核放入到一个繁忙的循环中,而且它不会让出处理器时间片的剩余部分。SpinWait的智能逻辑中会在自旋达到足够长的时间时停止自旋并让出处理器。当然可以考虑调用Thread.Sleep()方法,它会让出处理器时间,但开销比较大。

示例程序:这里通过使用SpinWait 来控制3个Task的执行顺序。

using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Sample5_6_spinwait
{
    class Program
    {
        private static int _TaskNum = 3;
        private static Task[] _Tasks;
        private static StringBuilder _StrBlder;
        private const int RUN_LOOP = 10;
        private static bool m_IsWork2Start = false;
        private static bool m_IsWork3Start = false;

        private static void Work1(int TaskID)
        {
            int i = 0;
            string log = "";

            while (i < RUN_LOOP)
            {
                log = String.Format("Time: {0}  Task : #{1}  Value: {2}  =====\n",
                                  DateTime.Now.TimeOfDay, TaskID, i);
                i++;
                try
                {
                    _StrBlder.Append(log);
                }
                finally
                {
                    m_IsWork2Start = true;
                }
            }
        }

        private static void Work2(int TaskID)
        {
            int i = 0;
            string log = "";

            System.Threading.SpinWait.SpinUntil(() => m_IsWork2Start);

            while ((i < RUN_LOOP) && (m_IsWork2Start))
            {
                log = String.Format("Time: {0}  Task : #{1}  Value: {2}  *****\n",
                                  DateTime.Now.TimeOfDay, TaskID, i);
                i++;
                try
                {
                    _StrBlder.Append(log);
                }
                finally
                {
                    m_IsWork3Start = true;
                }
            }
        }

        private static void Work3(int TaskID)
        {
            int i = 0;
            string log = "";

            System.Threading.SpinWait.SpinUntil(() => m_IsWork3Start);

            while (i < RUN_LOOP)
            {
                log = String.Format("Time: {0}  Task : #{1}  Value: {2}  ~~~~~\n",
                                  DateTime.Now.TimeOfDay, TaskID, i);
                i++;
                try
                {
                    _StrBlder.Append(log);
                }
                finally
                {
                }
            }
        }

        static void Main(string[] args)
        {
            _Tasks = new Task[_TaskNum];
            _StrBlder = new StringBuilder();

            _Tasks[0] = Task.Factory.StartNew((num) =>
            {
                var taskid = (int)num;
                Work1(taskid);
            }, 0);

            _Tasks[1] = Task.Factory.StartNew((num) =>
            {
                var taskid = (int)num;
                Work2(taskid);
            }, 1);

            _Tasks[2] = Task.Factory.StartNew((num) =>
            {
                var taskid = (int)num;
                Work3(taskid);
            }, 2);

            var finalTask = Task.Factory.ContinueWhenAll(_Tasks, (tasks) =>
            {
                Task.WaitAll(_Tasks);
                Console.WriteLine("==========================================================");
                Console.WriteLine("All Phase is completed");
                Console.WriteLine("==========================================================");
                Console.WriteLine(_StrBlder);
            });

            try
            {
                finalTask.Wait();
            }
            catch (AggregateException aex)
            {
                Console.WriteLine("Task failed And Canceled" + aex.ToString());
            }
            finally
            {
            }
            Console.ReadLine();
        }
    }
}

测试结果:
Time: 08:41:33.5505335 Task : #0 Value: 0 =====
Time: 08:41:33.5725357 Task : #0 Value: 1 =====
Time: 08:41:33.5725357 Task : #0 Value: 2 =====
Time: 08:41:33.5725357 Task : #0 Value: 3 =====
Time: 08:41:33.5725357 Task : #0 Value: 4 =====
Time: 08:41:33.5725357 Task : #0 Value: 5 =====
Time: 08:41:33.5725357 Task : #0 Value: 6 =====
Time: 08:41:33.5725357 Task : #0 Value: 7 =====
Time: 08:41:33.5725357 Task : #0 Value: 8 =====
Time: 08:41:33.5725357 Task : #0 Value: 9 =====
Time: 08:41:33.5735358 Task : #1 Value: 0 *
Time: 08:41:33.5735358 Task : #1 Value: 1 *
Time: 08:41:33.5735358 Task : #1 Value: 2 *
Time: 08:41:33.5735358 Task : #1 Value: 3 *
Time: 08:41:33.5735358 Task : #1 Value: 4 *
Time: 08:41:33.5735358 Task : #1 Value: 5 *
Time: 08:41:33.5735358 Task : #1 Value: 6 *
Time: 08:41:33.5735358 Task : #1 Value: 7 *
Time: 08:41:33.5735358 Task : #1 Value: 8 *
Time: 08:41:33.5735358 Task : #1 Value: 9 *
Time: 08:41:33.5735358 Task : #2 Value: 0 ~
Time: 08:41:33.5735358 Task : #2 Value: 1 ~
Time: 08:41:33.5735358 Task : #2 Value: 2 ~
Time: 08:41:33.5735358 Task : #2 Value: 3 ~
Time: 08:41:33.5735358 Task : #2 Value: 4 ~
Time: 08:41:33.5735358 Task : #2 Value: 5 ~
Time: 08:41:33.5735358 Task : #2 Value: 6 ~
Time: 08:41:33.5735358 Task : #2 Value: 7 ~
Time: 08:41:33.5735358 Task : #2 Value: 8 ~
Time: 08:41:33.5735358 Task : #2 Value: 9 ~

操作 SpinWait 的另一种方式 – 实例化 SpinWait。

SpinWait 提供了两个方法和两个只读属性。

方法:

  • SpinWait.Reset() : 重置自旋计数器,将计数器置 0。效果就好像没调用过SpinOnce一样。
  • SpinWait.Once() : 执行一次自旋。当SpinWait自旋达到一定次数后,如果有必要当前线程会让出底层的时间片并触发上下文切换。

属性:

  • SpinWait.Count:方法执行单次自旋的次数。
  • SpinWait.NextSpinWillYield:一个bool值,表示下一次通过SpinOnce方法自旋是否会让出底层线程的时间片并发生上下文切换。

你可能感兴趣的:(C#)