在 C# 中,关于队列(Queue)有两种,一种就是我们普通使用的队列,另一种是线程安全的队列 ConcurrentQueue<T> 。

ConcurrentQueue表示线程安全的先进先出 (FIFO) 集合。https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.concurrent.concurrentqueue-1?view=netstandard-2.1        这两者在数据结构上,都是先进先出(FIFO)的集合,一般情况下我们都是用的 Queue 这种常规队列就能满足需求。当然,在多线程情况下,如果是用 Queue,因为线程不安全,在线程竞争的时候(多线程入队或多线程出队)就会造成异常,此时我们就会用到 ConcurrentQueue。那么这两者性能差异是如何的呢?

        本文将对这两个队列进行一个简单的性能测试,同时讨论一种特殊情况:一个线程入队,一个线程出队时使用 Queue 的情况。

1、单一线程入队+单一线程出队情况

        这里我们讨论多线程的特殊情况:假设队列只有一个线程进行入队,同时,只有一个线程进行出队。那么在这种情况下,我们使用线程不安全队列 Queue 会不会有问题?如果在在 Unity 中,写入、读取有耗时操作,会不会出现异常?

        这里我们的测试用例很简单:

        public static void AddTaskItem()
        {
            System.Random rand = new System.Random();

            for (int i = 0; i < MaxCount; i++)
            {
                TaskItem item = new TaskItem();
                item.Index = i;
                TestQueue.Enqueue(item);
                Thread.Sleep(rand.Next(0, 5));
            }

            Debug.Log("全部数据添加完毕!");
        }


        public class TaskItem
        {
            public int Index;
    
            public void DoSomeWork(){}// 某耗时函数,此处略去
        }

        这里我将 MaxCount 设置为 10000,之后在 Unity 主线程进行 Update:

        public int CurIndex = 0;

        private void Update()
        {
            int updateCount = TestQueue.Count;
            int lastIndex = CurIndex;
            //取出当前队列的所有值,并比对;
            while (testQueue.Count > 0)
            {
                var item = testQueue.Dequeue();
                if (item.Index != CurIndex)
                {
                    Debug.LogError($"取值错误,应该是:{CurIndex},实际是  :{item.Index}");
                }

                item.DoSomeWork();

                CurIndex++;
            }

            Debug.Log($"本次取出队列:{CurIndex - lastIndex} / {updateCount}");
        }

        显然,只要取值错误,即当前取出的值不是在主线程记录的序号(CurIndex),就会抛出错误。不过我进行了多次测试,并没有出现过一次错误,即便是增加了耗时函数,导致每帧取值并不是一开始的值,但仍然不会出现出队入队异常。

        所以我们得出结论:

        在仅有一个线程入队、一个线程出队的情况下,使用队列 Queue 是不会有异常的。

2、性能测试

        这里我们就简单地进行入队出队的性能测试,这里就不贴测试代码了,直接出结论:

        整体来看,ConcurrentQueue 的性能开销都是大于 Queue 的,其中:

        写入耗时:ConcurrentQueue 约为 Queue 的 1.3 倍。

        读取耗时:ConcurrentQueue 约为 Queue 的 6 倍。

        同时,随着队列中的数据增多,ConcurrentQueue 的读取耗时将会显著增加。

        当然,如果队列中数量不是很多,这两者的差别并不算太大,微秒级别的差异在一般情况下可以无视。同时,队列中有上千万个元素的情况在一般游戏中非常少见(一般队列中有个几百个元素就不得了了),所以不用太在意 ConcurrentQueue 带来的额外性能开销。

        同时,本次测试都是单线程的读写,相当于抛弃了 ConcurrentQueue 的优势(多线程安全)来测试,有些许不公。在实际使用时 ConcurrentQueue 一定是在多线程读写的场景,其安全性与性能肯定会显著优于 Queue 。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐