使用Actor模型对词频统计程序进行多线程优化

词频统计程序是一个相当简单的程序:它读一个文件夹里的所有指定类型的文件,统计其中出现的英文单词的次数,并排序输出。

但是它却有很大的优化余地,甚至可以分布式到多台机器中(Map-Reduce模型)。但是,在单机中搞这么复杂反而会增加运行时间和内存。

我们希望将它改造成多线程。但是,分词过程和统计过程若分布到多个线程中,则对内存的锁会增加,因为大部分算法的时间效率都是O(n)的,而且对内存的操作很频繁,所以效率反而会降低。

在单机中,若为单线程运行,则IO操作(读写文件)时不能进行CPU运算,同理CPU进行运算时不能进行IO操作。所以一种很明显的优化方式就是,把IO操作和CPU操作分离到两个线程中去。所以,我尝试了这个优化方案。

由于之前在暑假学习过Scala语言,所以对Scala中的Actor模型印象很深刻。我希望在C#中也实现Actor模型,从而轻松的实现线程之间的通信。

Actor模型是啥?它把每一个的线程都当做一个Actor,每个Actor含有一个队列。Actor的工作是,循环的从队列里拿出队列头的数据并对它进行工作。同时,它还可以与其他Actor通信,传递给它数据(即把数据加入另一个Actor的队列尾中)。

这样的模型很适合这个双线程词频统计程序:IO线程读取文件内容随后把内容发给分析线程,分析线程对其进行分析。两个线程的工作都完成后,把分析好的结果输出。

这个优化的核心是Actor类型。定义如下:

public abstract class Actor<T>
    {
        System.Collections.Concurrent.BlockingCollection<T> messages;
        System.Threading.Thread th;
        public Actor(int maxmsgcount = 100,int timeout=-1)
        {
            messages = new System.Collections.Concurrent.BlockingCollection<T>(maxmsgcount);
            th = new System.Threading.Thread(new System.Threading.ThreadStart(() =>
            {
                T current;
                while(true)
                    if (messages.TryTake(out current,timeout))
                        HandleMessage(current);
            }));
            th.Start();
        }
        public bool PostMessage(T msg,int timeout=-1)
        {
            return messages.TryAdd(msg, timeout);
        }
        public abstract void HandleMessage(T msg);
        public void Close()
        {
            th.Abort();
            th = null;
        }
        
    }

其中使用了C# 4.0中新增的BlockingCollection<T>。为啥用它而不用队列呢,是因为ConcurrentQueue<T>不支持阻塞功能,我们不希望在Actor无所事事的时候依然在死循环,而且存放在里面的数据也不需要有序。

随后,我构建了两个Actor对象:一个负责IO,一个负责计算:

public class FileIOActor : Actor<string>
    {
        CPUActor actor;
        public FileIOActor(CPUActor cpuactor)
        {
            this.actor = cpuactor;
        }
        public override void HandleMessage(string msg)
        {
            if (msg == "END")
            {
                actor.PostMessage("");
                return;
            }
            System.IO.StreamReader reader = new System.IO.StreamReader(msg);
            while (!reader.EndOfStream)
            {
                string str = reader.ReadToEnd();
                actor.PostMessage(str+" ");
               
            }
            reader.Close();
        }
        public void Close()
        {
            base.Close();
            actor.Close();
        }
    }
public class CPUActor : Actor<string>
    {
        static int[] lettertype = new int[128];
        static CPUActor()
        {
            for (int i = 'a'; i <= 'z'; i++)
                lettertype[i] = 1;
            for (int i = 'A'; i <= 'Z'; i++)
                lettertype[i] = 1;
            
            for (int i = '0'; i <= '9'; i++)
                lettertype[i] = 2;
        }
        //定义单词
        public class Word:IComparable<Word>
        {
            public Word(string w) { str = w; }
            public string str;
            public int Count=0;

            public int CompareTo(Word obj)
            {
                if (obj.Count != this.Count) return Count - obj.Count;
                else return String.CompareOrdinal(obj.str, str);
            }
        }
        //计算结束后的事件
        Action<ICollection<Word>> finish;
        public CPUActor(Action<ICollection<Word>> classes)
        {
            finish = classes;
        }
        int state=0;
        StringBuilder sb = new StringBuilder(100);
        Dictionary<string, Word> classes = new Dictionary<string, Word>(10000);
        public override void HandleMessage(string msg)
        {
            if (msg == "")
            {
                finish(classes.Values);
                return;
            }
            for (int i = 0; i < msg.Length; i++)
            {
                //状态机
                switch (state)
                {
                    case 0:
                        if ((state = ((msg[i] & 0x7f) != msg[i]) ? 0 : lettertype[msg[i]]) != 0)
                        {
                            sb.Clear();
                            sb.Append(msg[i]);
                        }
                        break;
                    default:
                        if ((state = ((msg[i] & 0x7f) != msg[i]) ? 0 : lettertype[msg[i]]) == 0)
                        {
                            if (sb.Length >= 3)
                                take(sb.ToString());
                        }
                        else
                            sb.Append(msg[i]);
                        break;
                }
            }
        }
        public void take(string word)
        {
            
            if (!(lettertype[word[0]] == 1 && lettertype[word[1]] == 1 && lettertype[word[2]] == 1)) return;
            string lword = word.ToLower();
            Word w = null;
            if (classes.TryGetValue(lword, out w))
            {
                if (String.CompareOrdinal(word, w.str) < 0) w.str = word;
                w.Count++;
            }
            else
            {
                classes.Add(lword, w = new Word(word));
                w.Count = 1;
            }
            
        }
    }

其中,读文件时我采用了ReadToEnd方法,若文件过大可很容易的换成ReadBlock方法等。统计时只考虑第一种需求,考虑末尾的数字。

剩下的就是主程序了,很简单:

.........
                var files = from f in System.IO.Directory.GetFiles(dir, "*", System.IO.SearchOption.AllDirectories)
                            where f.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase) ||
                            f.EndsWith(".txt", StringComparison.OrdinalIgnoreCase) ||
                            f.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) ||
                            f.EndsWith(".h", StringComparison.OrdinalIgnoreCase)
                            select f;
                FileIOActor fileactor = null;
                fileactor = new FileIOActor(new CPUActor(a =>
                 {
                     var collection = from word in a
                                      orderby word descending
                                      select new StringBuilder(word.str).Append(": ").Append(word.Count);
                     System.IO.StreamWriter writer = new System.IO.StreamWriter(username + ".txt");
                     if (collection.Count() > 0)
                         foreach (var str in collection)
                             writer.WriteLine(str);
                     writer.Close();

                     if (fileactor != null) fileactor.Close();

                 }));
                foreach (var i in files)
                    fileactor.PostMessage(i);
                fileactor.PostMessage("END");
.........

于是,这个程序做好了。经过测试,它的答案与原来一致。在我的双核I5CPU下,用一个133MB的文件夹测试,对CPU性能分析结果如下:

多线程优化前:

 

优化后:

可以看出,优化效果还是很明显的。这也是在我电脑上唯一一个对于这个数据可以跑进10秒的词频统计程序。

优化前后的瓶颈都在于String.ToLower()函数:

通过黄杨的另一个优化方案,速度可以更快些。

posted @ 2012-09-25 23:14  wanganran  阅读(731)  评论(1编辑  收藏  举报