生产者消费者模式及简单的运用场景

先考虑一个问题:服务端接受多个客户端提交的视频文件进行转码的操作,应该怎么设计?

由于转码比较花费时间,所以我们排除同步的想法。而转码需要用到的外部软件(exe文件),不能同时被多个线程用到,所以我们排除为每一个客户端提交新建一个线程进行转码的想法。

于是我们想到了静态加锁和队列。静态加锁有个缺点,稍后再提。当我们选择了队列,就选择了生产者消费者模式。

 

其流程图:

 

有流程图我们可以知道,生产者不关心数据什么时候被处理,消费者不关心数据什么时候产生,实现了解耦,也解决了阻塞。

 

还有一个比较典型的例子便是日志的记录,多线程产生日志,但写日志由于文件独占,不能多线程来写,于是我们就可以把线程压入队列,由日志线程来读取队列数据,完成写日志的操作。下面是一个简单的实现:

public class Log
{
    private static ConcurrentQueue<LogMessage> msgs = new ConcurrentQueue<LogMessage>();

    public static void WriteLog(string msg)
    {
        msgs.Enqueue(new LogMessage(msg));
    }

    public static void Start()
    {
        Task.Factory.StartNew(() =>
        {
            while (true)
            {
                while (msgs.TryDequeue(out LogMessage msg))
                {
                    using (StreamWriter sw = new StreamWriter(msg.LogFile, true))
                    {
                        sw.WriteLine(msg.Message);
                    }
                }
                Thread.Sleep(1000);
            }
        });
    }
}

这个是写日志的类

class LogMessage
{
    public string Message { get; set; }
    public string LogFile { get; set; }

    public LogMessage(string msg)
    {
        this.Message = $"{DateTime.Now.ToString("HH:mm:ss ")} {msg}";
        string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log", DateTime.Now.ToString("yyyy-MM"));
        if (!Directory.Exists(path))
            Directory.CreateDirectory(path);
        this.LogFile = Path.Combine(path, DateTime.Now.ToString("dd") + ".log");
    }
}

这个是日志结构。包括产生日志的时间和写日志的日志文件。可以实现23:59产生的日志写到当天的文件夹中。

日志工具类的调用也非常简单,直接调用静态方法WriteLog就行。

 

回到开头所说加锁的弊端:线程排队并不是在队列中,没有先后顺序的保证,牵扯到严格顺序时就会有问题,比如写日志,socket数据接受等。

 

模式的应用场景:处理数据比较消耗时间,线程独占,生产数据不需要即时的反馈等。

 

例子的不足:

1.省略掉了缓冲区,使得生产者和消费者并不是完全解绑。改进:用一个独立的数据结构来放置数据,可以是缓存、文件、数据库,实现仅依赖于数据格式的解绑。

2.程序结束时,我们不能保证缓冲区数据是否全部处理完。改进:生产日志时,写文件/数据库,处理数据后,对处理过的数据进行标记,程序异常结束也没问题,下次重启先加载未处理数据,再一次展现单纯加锁的弊端。

posted @ 2017-10-30 18:59  白衣如花  阅读(4708)  评论(1编辑  收藏  举报