同学让帮忙调试一个程序。程序主体是一个生产者消费者模式。生产者不断监听指定端口上的udp消息,收到消息就放到消息队列。工作线程则负责处理消息队列中的消息。问题是通过记录下来的时间来看,从收到消息到消息被处理时间过去了几百毫秒。

我首先改了下一些多线程中需要注意的同步事项。比如对于变量的修改,要加锁或者Interlocked。

c#中有一点要注意的,和c++不同,c#中的AutoResetEvent虽然和c++中的condition_variable类似,但是c++中的condition_vairable是在lock块内执行的,当sleep的时候会自动释放锁,唤醒的时候会自动加锁。但是c#中的AutoResetEvent则不是这个逻辑,这个和锁无关,不需要放到lock块内。

然后用jetbrain公司的vs resharp插件来做profile,收集好数据以后用dottrace打开。通过分析工作线程的cpu执行情况,发现工作线程是满负荷的,所以应该不是由于死锁或者锁竞争导致的延时。进一步发现,工作线程99.9%的时间都花在一个叫做bytetohexstr的函数上。这个函数顾名思义,就是把类似\x11\x22的bytes数组转为”1122“这样的string

那么这个函数是怎么写的呢?如下

        public string bytetohexstr(byte[] bytes, int length)     //字节转化为二进制
        {
            string returnstr = "";
            if (bytes != null)
            {
                for (int i = 0; i < length; i++)
                {
                    returnstr += bytes[i].ToString("X2");
                }
            }
            return returnstr;
        }

看到这问题就很明显了,由于c#中string是不可变对象,每次+=都要分配内存给一个新对象,并且把string的内容拼接复制到新对象。这样这个函数在内存层面上其实是个O(n^2)级别的时间复杂度。同时大量临时对象的产生,也加重了gc的压力。

我把这个函数改成如下:

    public string bytetohexstr(byte[] bytes, int length)     //字节转化为二进制
    {
        if (bytes == null)
            return "";
        char[] r = new char[2 * length];
        for(int i=0;i<length;i++)
        {
            int high = bytes[i] / 16;
            int low = bytes[i] % 16;
            r[i*2] = idx_to_hex[high];
            r[i*2 + 1] = idx_to_hex[low];
        }
        string ret = new string(r);
        return ret;
    }

因为我对c#不是很熟悉,后来发现用c#中的stirnbuilder应该效果也差不多。

再调试发现程序一下子飞快,根据profile的结果,四个线程处理十个消息,只需要不到0.7ms。

考虑到根据profile的结果,没必要这么多线程,我就改成了一个生产者,一个消费者的模式,测试下来效果也很好,每次处理10个消息都在1ms以内。

这次调试调试让我感受到代码不注意的话,效率真的可能会相差千倍乃至完备。这个函数的输入字节一般是两千多,按照原来那种写法得有2000*2000/2 = 2million的内存写入,这还没算不断申请小内存以及释放内存消耗的时间。而修改过的代码,内存写入只有2000次,效率相差一千倍。所以造成几百毫秒和零点几毫秒的极端差距。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注