生产者消费者模式及简单的运用场景
先考虑一个问题:服务端接受多个客户端提交的视频文件进行转码的操作,应该怎么设计?
由于转码比较花费时间,所以我们排除同步的想法。而转码需要用到的外部软件(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.程序结束时,我们不能保证缓冲区数据是否全部处理完。改进:生产日志时,写文件/数据库,处理数据后,对处理过的数据进行标记,程序异常结束也没问题,下次重启先加载未处理数据,再一次展现单纯加锁的弊端。