深入理解MVC
MVC无人不知,可很多程序员对MVC的概念的理解似乎有误,换言之他们一直在错用MVC,尽管即使如此软件也能被写出来,然而软件内部代码的组织方式却是不科学的,这会影响到软件的可维护性、可移植性,代码的可重用性。
MVC即Model、View、Controller即模型、视图、控制器。我在和同行讨论技术,阅读别人的代码时发现,很多程序员倾向于将软件的业务逻辑放在Controller里,将数据库访问操作的代码放在Model里。
最终软件(网站)的代码结构是,View层是界面,Controller层是业务逻辑,Model层是数据库访问。
不知道大家知不知道另外一种软件开发模式三层架构,它和MVC相似之处是也分为三层,分别是UI层表示用户界面,BLL层表示业务逻辑,DAL层表示数据访问。三层架构曾经红极一时,MVC大行其道之后它就销声匿迹了, 可现在看来, 它似乎只是改头换面, 装扮成MVC的样子,并且深受程序员们的欢迎,因为它的这种分层方式和前文描述的MVC如出一辙。
再说的直白点,很多程序员将MVC当成了三层架构在用,这看起来似乎没什么问题,毕竟三层架构也是一种和MVC齐名的架构模式。可问题在于用三成架构的思路写MVC,那么写出来的东西既不是三成架构也不是MVC,到是像一个什么都不是四不像。熟悉天龙八部的同学应该知道这样一段情节,吐蕃番僧鸠摩智强行用道家的小无相功为基础修炼少林的七十二绝技和易筋经最终导致走火入魔。其实用这个例子来形容现在一些程序员用三层架构的思想写MVC最恰当不过了,三层架构的核心思想是面向接口编程和各层之间的解耦和可替换性,MVC框架中没有这种概念,因为MVC要面对的问题本就不是三成架构要面对的问题,所以以MVC为基础写出来的三成架构是不会具备三层架构的核心要义的,换言之,这种代码是放弃了三层架构和MVC的精华,获得了它们的糟粕,是愚蠢的编码方式。
我吐槽了这么多, 对于吐槽的理由要是说不出个所以然来,估计要被人喷死,下面就来说说MVC本质原理和正确使用方式,当然,这里的MVC指的最纯粹MVC,适合各类软件,而不仅仅指Web框架中的变体MVC,然而万变不离其宗,文中所述的MVC思想同样适用于Web开发。
MVC要实现的目标是将软件用户界面和业务逻辑分离以使代码可扩展性、可复用性、可维护性、灵活性加强。
View层是界面,Model层是业务逻辑,Controller层用来调度View层和Model层,将用户界面和业务逻辑合理的组织在一起,起粘合剂的效果。所以Controller中的内容能少则少,这样才能提供最大的灵活性。
比方说,有一个View会提交数据给Model进行处理以实现具体的行为,View通常不会直接提交数据给Model,它会先把数据提交给Controller,然后Controller再将数据转发给Model。假如此时程序业务逻辑的处理方式有变化,那么只需要在Controller中将原来的Model换成新实现的Model就可以了,控制器的作用就是这么简单, 用来将不同的View和不同的Model组织在一起,顺便替双方传递消息,仅此而已。
合理的使用MVC有很多好处,要一一道尽是一件异常困难的任务。在这里我们通过一个反面示例来侧面的证明正确使用MVC的好处与依据。
如前文所言, 很多程序员偏爱于将业务逻辑放在Controller中,我极力反对这种做法,现在我就来证明这中做法的错误性。
我们知道在写程序时,业务逻辑的重复使用是经常要面对的场景。 如果业务逻辑写在控制器中, 要重用它的唯一方法就是将它提升到父类之中,通过继承来达到代码复用的效果。但这么做会带来一个巨大的副作用,违背了一项重要的面向对象设计原则:接口隔离。
什么是接口隔离,我在这里简单的讲述一下。通俗一点讲,接口隔离就是当一个类需要继承另一个类时, 如果被继承的类中有继承的类用不到的方法或者属性时,就不要去实现这个继承。如果真的情非得已必须要继承,那么也需要从被继承的类中再提取出一个只包含需要部分功能的新类型,最终去继承这个新类型才是正确的做法。 换句话说,实现继承的时候,不要去继承那些用不到的事物。
回到之前的话题,通过继承父控制器的方式复用业务逻辑时,往往会出现为了重用一个方法而继承来一大堆用不到的方法,表面上看起来似乎没什么问题,但是这会使代码变的难以理解,
长此以往, 软件的代码会朝着不健康的方向发展。
要知道,使用继承的条件是很苛刻的,我们学习面向对象变编程继承特性时,第一课就是只有满足IS-A(是一个)关系时才可以使用继承,如果仅仅是复用代码,并不是我们使用继承的理由。使用组合是复用代码提倡的方式,也就是所谓的HAS-A(有一个)的关系,相信每个程序员都听过“少用继承,多有组合”这句话,这句话是软件开发业的先驱们千锤百炼总结出来的,值得我们去遵循。
各Model之间是可以相互调用的, Controller也可以无障碍的调用Model,因此将业务逻辑放在Model中可以灵活的使用组合的方式复用代码。
而Controller之间是不可以相互调用的,要复用代码只能将代码提升至父类,通过继承实现,显然这种做法既不正确,也不灵活,因此完全不提倡。
综上所述,仅仅只是代码复用这一点,也足以将“厚Controller,薄Model”这种不健康的MVC思想打入十八层地狱。
现在我们大概知道了代码应该如何分布于MVC三层之间, 知其然,并且也知其所以然。接下来我们再从另一个角度深刻剖析MVC,脱它个精光,让它赤条条展示在我们眼前。
众所周知,GoF总结过23个设计模式,这23个设计模式是某些特定的编程问题的特效药,这是业内公认的。
MVC是一种模式,但却在GoF总结出来的这个23个设计模式之外,确切的说它不是一种设计模式,它是多种设计模式的组合,并不仅仅只是一个单独的一个模式。
组成MVC的三个模式分别是组合模式、策咯模式、观察者模式,MVC在软件开发中发挥的威力,最终离不开这三个模式的默契配合。 那些崇尚设计模式无用论的程序员,请了解只要你们使用MVC,就离不开设计模式。
注意,以下内容以这三个设计模式的知识为基础,如果对这三个设计模式没概念,或许会阅读困难。
先说组合模式在MVC中扮演什么样的角色。
组合模式只在视图层活动, 视图层的实现用的就是组合模式,当然,这里指的实现是底层的实现,是由编程框架厂商做的事情,用不着普通程序员插手。
组合模式的类层次结构是树状的, 而我们做Web时视图层是html页面,html的结构不正是树状的吗,这其实就是一个组合模式的应用,只是浏览器厂商已经把界面相关的工作帮我们做掉了,但它确确实实是我们应用MVC的其中一部分,只是我们感觉不到罢了,这也是我们觉得View是实现起来最简单最没有歧义的一层的原因。
除网页以外的其他用户界面程序,如WPF、Android、Asp.net等等都是使用树状结构来组织界面控件对象的,因为组合模式就是从界面设计的通用解决方案总提炼出来的。所以与其说MVC选择了组合模式,还不如说组合模式是必定会存在MVC中的,因为只要涉及到用户界面,组合模式就必定存。事实上即使不理解组合模式,也不影响程序员正确的使用MVC,组合模式本就存在于程序员接触不到的位置。
然而,观察者模式和策略模式就显得比较重要,是实实在在MVC中接触的到的部分。
观察者模式有两部分组成,被观察的对象和观察者,观察者也被称为监听者。对应到MVC中,Model是被观察的对象,View是观察者,Model层一旦发生变化,View层即被通知更新。View层和Model层互相之间是持有引用的。 我们在开发Web MVC程序时,因为视图层的html和Model层的业务逻辑之间隔了一个http,所以不能显示的进行关联,但是他们观察者和收听者的关系却没有改变。当View通过http提交数据给服务器,服务器上的Model接受到数据执行某些操作,再通过http响应将结果回送给View,View(浏览器)接受到数据更新界面,这不正是一个接受到通知并执行更新的行为吗,是观察者模式的另一种表现形式。
但是,脱离Web,当通过代码去纯粹的表示一个MVC结构的时候,View和Model间无疑是观察者和被观察的关系,是以观察者模式为理论基础的。即使在Web中因为http壁垒的原因导致真正的实现有点走样,但是原理核心和思路哲学却是不变的。
最后是策略模式。策略模式是View和Controller之间的关系,Controller是View的一个策略,Controller对于View是可替换的, View和Controller的关系是一对多,在实际的开发场景中,也经常会碰到一个View被多个Controller引用,这即使策咯模式的一种体现,只是不那么直观而已。
总结一下,关于MVC各层之间关系所对应的设计模式
View层,单独实现了组合模式
Model层和View层,实现了观察者模式
View层和Controller层,实现了策咯模式
MVC就是将这三个设计模式在一起用了,将这三个设计模式弄明白,MVC将毫无神秘感可言。如果不了解这三个设计模式去学习MVC,那不管怎么学总归是一知半解,用的时候也难免不会出想问题。
再次回到最前面讨论的业务逻辑应该放在Controller还是Model的问题上,从设计模式的角度讲,策略模式中的策略通常都很小很薄,不会包含太多的内容, Controller是一个策略, 自然不应该在里面放置过多的内容,否则要替换一个新的会相当麻烦,与此同时也会破坏View-Model的观察者模式,仿佛View-Controller之间即实现了策略模式又实现了观察者模式,这种混乱是罪恶的根源,是制造焦油坑让程序员陷入其中无法自拔的罪魁祸首。切忌,应当避免。
注:此文核心思想来自《head first设计模式》
C#+HtmlAgilityPack+Dapper走一波爬虫
最近因为公司业务需要,又有机会撸winform了,这次的需求是因为公司有项目申报的这块业务,项目申报前期需要关注政府发布的相关动态信息,政府部门网站过多,人工需要一个一个网站去浏览和查阅,有时候还会遗漏掉,因此呢,我们打算用爬虫+移动端web来做,我呢主要负责爬虫和web Api。
爬虫篇
爬虫主要采用.Net强大的开源解析HTML元素的类库HtmlAgilityPack,操作过XML的童鞋应该很快就可以上手,通过分析XPath来解析HTML,非常的方便的,不过还有一款不错的叫Jumony,没用过,对HtmlAgilityPack比较熟悉,所以首选了HtmlAgilityPack来作为主力军。
HtmlAgilityPack的基本使用可以参考这篇 https://www.cnblogs.com/GmrBrian/p/6201237.html,
效果图,多图慎入
采集广西财政厅例子
因为是政府发布的出来的信息,所以信息的对外开放的,只是机器代替人工来浏览,不会被和谐的,主要采集文章列表和文章内容,以广西财政厅网站为例子。
First
加载网站这个就不用说了,先查看网站的字符编码,如图<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> ,然后设置HtmlAgilityPack中的OverrideEncoding属性,
htmlAgilityPack.OverrideEncoding = Encoding.UTF8;
Second
分析文章列表,浏览器F12查看HTML标签情况,可以分析出XPath为
//ul[@class='dzjzw_list_main_ul']//li
流程代码:
//获取第一页的内容 HtmlNode row = GetHtmlDoc(htmlWeb, url); //根据xpath获取列表 var list = row.SelectNodes("//ul[@class='dzjzw_list_main_ul']//li"); foreach (var data in list) { .... } /// <summary> /// 这里偶尔会浏览网页失败的,所以失败了多浏览几次 /// </summary public static HtmlNode GetHtmlDoc(HtmlWeb htmlWeb, string url) { try { var doc = GetDoc(htmlWeb, url); if (doc == null) { int againIdx = 0; while (againIdx++ < 5) { System.Threading.Thread.Sleep(1000); doc = GetDoc(htmlWeb, url); if (doc != null) break; } if (doc == null) { var htmlData = HttpHelper.Get<string>(url).Result;//.GetStringAsync(url).Result; return HtmlNode.CreateNode(htmlData); } else { return doc.DocumentNode; } } return doc.DocumentNode; } catch { Log.Error("未能正确访问地址:" + url); return null; } }
文章内容的链接的XPath标签
//a
文章发布的时间XPath标签
//span[@class='date']
都可以使用 HtmlNode.InnerText 来获取到相关值,非常的方便。
Third
文章详细内容也如此,通过分析XPath来分析即可,最头疼的是翻页的问题,因为政府网站使用的技术一般都是比较那个的,你懂的,在这里的翻页也比较简单,通过拼接URL来进行翻页即可,有些使用到oncilck来触发的,有些表单提交,要具体问题具体分析了,用Fiddler和浏览器的F12大法来分析翻页数据来源,所以这里的例子比较简单
Fourth
爬取到的之后,再来一个钉钉通知,在群里拉入一个机器人,可以参考钉钉的开发文档(https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.ece6g3&treeId=257&articleId=105735&docType=1#)
这样我们爬取的消息就第一时间通知到群里的小伙伴啦,是不是很炫酷,哈哈哈。
后面做完了再上传到GitHub吧,下班下班。
StackExchange.Redis 二次封装
在NuGet直接搜索StackExchange.Redis,下载引用包;
帮助类:
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
public class RedisUtils { /// <summary> /// redis配置文件信息 /// </summary> public static string RedisPath = "172.16.3.82:6379"; private static object _locker = new Object(); private static ConnectionMultiplexer _instance = null; /// <summary> /// 使用一个静态属性来返回已连接的实例,如下列中所示。这样,一旦 ConnectionMultiplexer 断开连接,便可以初始化新的连接实例。 /// </summary> public static ConnectionMultiplexer Instance { get { if (_instance == null) { lock (_locker) { if (_instance == null || !_instance.IsConnected) { _instance = ConnectionMultiplexer.Connect(RedisPath); //注册如下事件 _instance.ConnectionFailed += MuxerConnectionFailed; _instance.ConnectionRestored += MuxerConnectionRestored; _instance.ErrorMessage += MuxerErrorMessage; _instance.HashSlotMoved += MuxerHashSlotMoved; _instance.InternalError += MuxerInternalError; } } } return _instance; } } #region Keys /// <summary> /// 判断键是否存在 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key">键值</param> /// <returns></returns> public static bool KeyExists(int dbIndex, string key) { var db = Instance.GetDatabase(dbIndex); return db.KeyExists(key); } /// <summary> /// 为指定的键设置失效时间 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key">键</param> /// <param name="expiry">时间间隔</param> /// <returns></returns> public static bool SetExpire(int dbIndex, string key, TimeSpan? expiry) { var db = Instance.GetDatabase(dbIndex); return db.KeyExpire(key, expiry); } /// <summary> /// 为指定的键设置失效时间 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key">键</param> /// <param name="timeout">时间间隔(秒)</param> /// <returns></returns> public static bool SetExpire(int dbIndex, string key, int timeout = 0) { var db = Instance.GetDatabase(dbIndex); return db.KeyExpire(key, DateTime.Now.AddSeconds(timeout)); } /// <summary> /// 删除键 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key">键</param> /// <returns></returns> public static bool KeyDelete(int dbIndex, string key) { var db = Instance.GetDatabase(dbIndex); return db.KeyDelete(key); } /// <summary> /// 键重命名 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="oldKey">旧值</param> /// <param name="newKey">新值</param> /// <returns></returns> public static bool KeyRename(int dbIndex, string oldKey, string newKey) { var db = Instance.GetDatabase(dbIndex); return db.KeyRename(oldKey, newKey); } #endregion #region Strings /// <summary> /// 获取字符串数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key">Redis键</param> /// <returns></returns> public static string StringGet(int dbIndex, string key) { var db = Instance.GetDatabase(dbIndex); if (db != null) { return db.StringGet(key); } return string.Empty; } /// <summary> /// 获取对象类型数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key">Redis键</param> /// <returns></returns> public static T StringGet<T>(int dbIndex, string key) where T : class { T data = default(T); var db = Instance.GetDatabase(dbIndex); if (db != null) { var value = db.StringGet(key); if (string.IsNullOrWhiteSpace(value)) return data; return JsonConvert.DeserializeObject<T>(value); } return data; } /// <summary> /// 设置值类型的值 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key">键</param> /// <param name="value">值</param> public static bool StringSet(int dbIndex, string key, RedisValue value, TimeSpan? expiry) { var db = Instance.GetDatabase(dbIndex); return db.StringSet(key, value, expiry); } /// <summary> /// 设置对象类型的值 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key">键</param> /// <param name="value">值</param> public static bool StringSet<T>(int dbIndex, string key, T value, TimeSpan? expiry) where T : class { if (value == default(T)) { return false; } var db = Instance.GetDatabase(dbIndex); return db.StringSet(key, JsonConvert.SerializeObject(value), expiry); } #endregion #region Hashes /// <summary> /// Hash是否存在 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="hashId">HashId</param> /// <param name="key">键</param> /// <returns></returns> public static bool HashExists(int dbIndex, string hashId, string key) { var db = Instance.GetDatabase(dbIndex); return db.HashExists(hashId, key); } /// <summary> /// Hash长度 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="hashId">HashId</param> /// <returns></returns> public static long HashLength(int dbIndex, string hashId) { var db = Instance.GetDatabase(dbIndex); var length = db.HashLength(hashId); return length; } /// <summary> /// 设置哈希值 /// </summary> /// <typeparam name="T">哈希值类型</typeparam> /// <param name="dbIndex">数据库</param> /// <param name="hashId">哈希ID</param> /// <param name="key">键</param> /// <param name="t">哈希值</param> /// <returns></returns> public static bool HashSet<T>(int dbIndex, string hashId, string key, T t) { var db = Instance.GetDatabase(dbIndex); var value = JsonConvert.SerializeObject(t); return db.HashSet(hashId, key, value); } /// <summary> /// 获取哈希值 /// </summary> /// <typeparam name="T">哈希值类型</typeparam> /// <param name="dbIndex">数据库</param> /// <param name="hashId">哈希ID</param> /// <param name="key">键</param> /// <returns></returns> public static T HashGet<T>(int dbIndex, string hashId, string key) { var db = Instance.GetDatabase(dbIndex); string value = db.HashGet(hashId, key); if (string.IsNullOrWhiteSpace(value)) return default(T); return JsonConvert.DeserializeObject<T>(value); } /// <summary> /// 获取哈希值 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="hashId">哈希ID</param> /// <param name="key">键</param> /// <returns></returns> public static string HashGet(int dbIndex, string hashId, string key) { var db = Instance.GetDatabase(dbIndex); string value = db.HashGet(hashId, key).ToString(); return value; } /// <summary> /// 获取哈希值的所有键 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="hashId">哈希ID</param> /// <returns></returns> public static List<string> HashKeys(int dbIndex, string hashId) { var db = Instance.GetDatabase(dbIndex); var result = new List<string>(); var list = db.HashKeys(hashId).ToList(); if (list.Count > 0) { list.ForEach(x => { var value = JsonConvert.DeserializeObject<string>(x); result.Add(value); }); } return result; } /// <summary> /// 获取所有哈希值 /// </summary> /// <typeparam name="T">哈希值类型</typeparam> /// <param name="dbIndex">数据库</param> /// <param name="hashId">哈希ID</param> /// <returns></returns> public static List<T> HashGetAll<T>(int dbIndex, string hashId) { var db = Instance.GetDatabase(dbIndex); var result = new List<T>(); var list = db.HashGetAll(hashId).ToList(); if (list.Count > 0) { list.ForEach(x => { var value = JsonConvert.DeserializeObject<T>(x.Value); result.Add(value); }); } return result; } /// <summary> /// 删除哈希值 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="hashId">哈希ID</param> /// <param name="key">键</param> /// <returns></returns> public static bool HashDelete(int dbIndex, string hashId, string key) { var db = Instance.GetDatabase(dbIndex); return db.HashDelete(hashId, key); } #endregion #region Lists /// <summary> /// 集合长度 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="listId">集合ID</param> /// <returns></returns> public static long ListLength(int dbIndex, string listId) { var db = Instance.GetDatabase(dbIndex); return db.ListLength(listId); } /// <summary> /// 向集合中添加元素 /// </summary> /// <typeparam name="T">元素类型</typeparam> /// <param name="dbIndex">数据库</param> /// <param name="listId">集合ID</param> /// <param name="list">元素值</param> /// <returns></returns> public static long AddList<T>(int dbIndex, string listId, List<T> list) { var db = Instance.GetDatabase(dbIndex); if (list != null && list.Count > 0) { foreach (var item in list) { db.ListRightPush(listId, JsonConvert.SerializeObject(item)); } } return ListLength(dbIndex, listId); } /// <summary> /// 获取集合元素(默认获取整个集合) /// </summary> /// <typeparam name="T">元素类型</typeparam> /// <param name="dbIndex">数据库</param> /// <param name="listId">集合ID</param> /// <param name="start">起始位置(0表示第1个位置)</param> /// <param name="stop">结束位置(-1表示倒数第1个位置)</param> /// <returns></returns> public static List<T> GetList<T>(int dbIndex, string listId, long start = 0, long stop = -1) { var db = Instance.GetDatabase(dbIndex); var result = new List<T>(); var list = db.ListRange(listId, start, stop).ToList(); if (list.Count > 0) { list.ForEach(x => { var value = JsonConvert.DeserializeObject<T>(x); result.Add(value); }); } return result; } #endregion #region ZSet #region 同步方法 /// <summary> /// 添加一个值到Key /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="value"></param> /// <param name="score">排序分数,为空将获取集合中最大score加1</param> /// <returns></returns> public static bool SortedSetAdd<T>(int dbIndex, string key, T value, double? score = null) { var db = Instance.GetDatabase(dbIndex); double scoreNum = score ?? _GetScore(key, db); return db.SortedSetAdd(key, ConvertJson<T>(value), scoreNum); } /// <summary> /// 添加一个集合到Key /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="value"></param> /// <param name="score">排序分数,为空将获取集合中最大score加1</param> /// <returns></returns> public static long SortedSetAdd<T>(int dbIndex, string key, List<T> value, double? score = null) { var db = Instance.GetDatabase(dbIndex); double scoreNum = score ?? _GetScore(key, db); SortedSetEntry[] rValue = value.Select(o => new SortedSetEntry(ConvertJson<T>(o), scoreNum++)).ToArray(); return db.SortedSetAdd(key, rValue); } /// <summary> /// 获取集合中的数量 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <returns></returns> public static long SortedSetLength(int dbIndex,string key) { var db = Instance.GetDatabase(dbIndex); return db.SortedSetLength(key); } /// <summary> /// 获取指定起始值到结束值的集合数量 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="startValue">起始值</param> /// <param name="endValue">结束值</param> /// <returns></returns> public static long SortedSetLengthByValue<T>(int dbIndex, string key, T startValue, T endValue) { var db = Instance.GetDatabase(dbIndex); var sValue = ConvertJson<T>(startValue); var eValue = ConvertJson<T>(endValue); return db.SortedSetLengthByValue(key, sValue, eValue); } /// <summary> /// 获取指定Key的排序Score值 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="value"></param> /// <returns></returns> public static double? SortedSetScore<T>(int dbIndex, string key, T value) { var db = Instance.GetDatabase(dbIndex); var rValue = ConvertJson<T>(value); return db.SortedSetScore(key, rValue); } /// <summary> /// 获取指定Key中最小Score值 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <returns></returns> public static double SortedSetMinScore(int dbIndex, string key) { var db = Instance.GetDatabase(dbIndex); double dValue = 0; var rValue = db.SortedSetRangeByRankWithScores(key, 0, 0, Order.Ascending).FirstOrDefault(); dValue = rValue != null ? rValue.Score : 0; return dValue; } /// <summary> /// 获取指定Key中最大Score值 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <returns></returns> public static double SortedSetMaxScore(int dbIndex, string key) { var db = Instance.GetDatabase(dbIndex); double dValue = 0; var rValue = db.SortedSetRangeByRankWithScores(key, 0, 0, Order.Descending).FirstOrDefault(); dValue = rValue != null ? rValue.Score : 0; return dValue; } /// <summary> /// 删除Key中指定的值 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="value"></param> public static long SortedSetRemove<T>(int dbIndex, string key, params T[] value) { var db = Instance.GetDatabase(dbIndex); var rValue = ConvertRedisValue<T>(value); return db.SortedSetRemove(key, rValue); } /// <summary> /// 删除指定起始值到结束值的数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="startValue">起始值</param> /// <param name="endValue">结束值</param> /// <returns></returns> public static long SortedSetRemoveRangeByValue<T>(int dbIndex, string key, T startValue, T endValue) { var db = Instance.GetDatabase(dbIndex); var sValue = ConvertJson<T>(startValue); var eValue = ConvertJson<T>(endValue); return db.SortedSetRemoveRangeByValue(key, sValue, eValue); } /// <summary> /// 删除 从 start 开始的 stop 条数据 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="start"></param> /// <param name="stop"></param> /// <returns></returns> public static long SortedSetRemoveRangeByRank(int dbIndex, string key, long start, long stop) { var db = Instance.GetDatabase(dbIndex); return db.SortedSetRemoveRangeByRank(key, start, stop); } /// <summary> /// 根据排序分数Score,删除从 start 开始的 stop 条数据 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="start"></param> /// <param name="stop"></param> /// <returns></returns> public static long SortedSetRemoveRangeByScore(int dbIndex, string key, double start, double stop) { var db = Instance.GetDatabase(dbIndex); return db.SortedSetRemoveRangeByScore(key, start, stop); } /// <summary> /// 获取从 start 开始的 stop 条数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="start">起始数</param> /// <param name="stop">-1表示到结束,0为1条</param> /// <param name="desc">是否按降序排列</param> /// <returns></returns> public static List<T> SortedSetRangeByRank<T>(int dbIndex, string key, long start = 0, long stop = -1, bool desc = false) { var db = Instance.GetDatabase(dbIndex); Order orderBy = desc ? Order.Descending : Order.Ascending; var rValue = db.SortedSetRangeByRank(key, start, stop, orderBy); return ConvetList<T>(rValue); } #endregion #region 异步方法 /// <summary> /// 添加一个值到Key /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="value"></param> /// <param name="score">排序分数,为空将获取集合中最大score加1</param> /// <returns></returns> public static async Task<bool> SortedSetAddAsync<T>(int dbIndex, string key, T value, double? score = null) { var db = Instance.GetDatabase(dbIndex); double scoreNum = score ?? _GetScore(key, db); return await db.SortedSetAddAsync(key, ConvertJson<T>(value), scoreNum); } /// <summary> /// 添加一个集合到Key /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="value"></param> /// <param name="score">排序分数,为空将获取集合中最大score加1</param> /// <returns></returns> public static async Task<long> SortedSetAddAsync<T>(int dbIndex, string key, List<T> value, double? score = null) { var db = Instance.GetDatabase(dbIndex); double scoreNum = score ?? _GetScore(key, db); SortedSetEntry[] rValue = value.Select(o => new SortedSetEntry(ConvertJson<T>(o), scoreNum++)).ToArray(); return await db.SortedSetAddAsync(key, rValue); } /// <summary> /// 获取集合中的数量 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <returns></returns> public static async Task<long> SortedSetLengthAsync(int dbIndex, string key) { var db = Instance.GetDatabase(dbIndex); return await db.SortedSetLengthAsync(key); } /// <summary> /// 获取指定起始值到结束值的集合数量 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="startValue">起始值</param> /// <param name="endValue">结束值</param> /// <returns></returns> public static async Task<long> SortedSetLengthByValueAsync<T>(int dbIndex, string key, T startValue, T endValue) { var db = Instance.GetDatabase(dbIndex); var sValue = ConvertJson<T>(startValue); var eValue = ConvertJson<T>(endValue); return await db.SortedSetLengthByValueAsync(key, sValue, eValue); } /// <summary> /// 获取指定Key中最大Score值 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <returns></returns> public static async Task<double> SortedSetMaxScoreAsync(int dbIndex, string key) { var db = Instance.GetDatabase(dbIndex); double dValue = 0; var rValue = (await db.SortedSetRangeByRankWithScoresAsync(key, 0, 0, Order.Descending)).FirstOrDefault(); dValue = rValue != null ? rValue.Score : 0; return dValue; } /// <summary> /// 删除Key中指定的值 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="value"></param> public static async Task<long> SortedSetRemoveAsync<T>(int dbIndex, string key, params T[] value) { var db = Instance.GetDatabase(dbIndex); var rValue = ConvertRedisValue<T>(value); return await db.SortedSetRemoveAsync(key, rValue); } /// <summary> /// 删除指定起始值到结束值的数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="startValue">起始值</param> /// <param name="endValue">结束值</param> /// <returns></returns> public static async Task<long> SortedSetRemoveRangeByValueAsync<T>(int dbIndex, string key, T startValue, T endValue) { var db = Instance.GetDatabase(dbIndex); var sValue = ConvertJson<T>(startValue); var eValue = ConvertJson<T>(endValue); return await db.SortedSetRemoveRangeByValueAsync(key, sValue, eValue); } /// <summary> /// 删除 从 start 开始的 stop 条数据 /// </summary> /// <param name="dbIndex">数据库</param> /// <param name="key"></param> /// <param name="start"></param> /// <param name="stop"></param> /// <returns></returns> public static async Task<long> SortedSetRemoveRangeByRankAsync(int dbIndex, string key, long start, long stop) { var db = Instance.GetDatabase(dbIndex); return await db.SortedSetRemoveRangeByRankAsync(key, start, stop); } #endregion #region 内部辅助方法 /// <summary> /// 将对象转换成string字符串 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="value"></param> /// <returns></returns> public static string ConvertJson<T>(T value) { string result = value is string ? value.ToString() : JsonConvert.SerializeObject(value, Formatting.None); return result; } /// <summary> /// 获取指定Key中最大Score值, /// </summary> /// <param name="key">key名称,注意要先添加上Key前缀</param> /// <returns></returns> private static double _GetScore(string key, IDatabase db) { double dValue = 0; var rValue = db.SortedSetRangeByRankWithScores(key, 0, 0, Order.Descending).FirstOrDefault(); dValue = rValue != null ? rValue.Score : 0; return dValue + 1; } /// <summary> /// 将值集合转换成RedisValue集合 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="redisValues"></param> /// <returns></returns> private static RedisValue[] ConvertRedisValue<T>(params T[] redisValues) => redisValues.Select(o => (RedisValue)ConvertJson<T>(o)).ToArray(); /// <summary> /// 将值反系列化成对象集合 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="values"></param> /// <returns></returns> public static List<T> ConvetList<T>(RedisValue[] values) { List<T> result = new List<T>(); foreach (var item in values) { var model = ConvertObj<T>(item); result.Add(model); } return result; } /// <summary> /// 将值反系列化成对象 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="value"></param> /// <returns></returns> public static T ConvertObj<T>(RedisValue value) { return value.IsNullOrEmpty ? default(T) : JsonConvert.DeserializeObject<T>(value); } /// <summary> /// 获取几个集合的交叉并集合,并保存到一个新Key中 /// </summary> /// <param name="db"></param> /// <param name="operation">Union:并集 Intersect:交集 Difference:差集 详见 <see cref="SetOperation"/></param> /// <param name="destination">保存的新Key名称</param> /// <param name="keys">要操作的Key集合</param> /// <returns></returns> private static long _SortedSetCombineAndStore(IDatabase db, SetOperation operation, string destination, params string[] keys) { RedisKey[] keyList = ConvertRedisKeysAddSysCustomKey(keys); var rValue = db.SortedSetCombineAndStore(operation, destination, keyList); return rValue; } /// <summary> /// 将string类型的Key转换成 <see cref="RedisKey"/> 型的Key,并添加前缀字符串 /// </summary> /// <param name="redisKeys"></param> /// <returns></returns> private static RedisKey[] ConvertRedisKeysAddSysCustomKey(params string[] redisKeys) => redisKeys.Select(redisKey => (RedisKey)redisKey).ToArray(); #endregion #endregion #region 当作消息代理中间件使用 一般使用更专业的消息队列来处理这种业务场景 /// <summary> /// 当作消息代理中间件使用 /// 消息组建中,重要的概念便是生产者,消费者,消息中间件。 /// </summary> /// <param name="channel"></param> /// <param name="message"></param> /// <returns></returns> public static long Publish(string channel, string message) { ISubscriber sub = Instance.GetSubscriber(); //return sub.Publish("messages", "hello"); return sub.Publish(channel, message); } /// <summary> /// 在消费者端得到该消息并输出 /// </summary> /// <param name="channelFrom"></param> /// <returns></returns> public static void Subscribe(string channelFrom) { ISubscriber sub = Instance.GetSubscriber(); sub.Subscribe(channelFrom, (channel, message) => { Console.WriteLine((string)message); }); } #endregion #region EventHandler /// <summary> /// 连接失败 , 如果重新连接成功你将不会收到这个通知 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void MuxerConnectionFailed(object sender, ConnectionFailedEventArgs e) { } /// <summary> /// 重新建立连接之前的错误 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void MuxerConnectionRestored(object sender, ConnectionFailedEventArgs e) { } /// <summary> /// 发生错误时 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void MuxerErrorMessage(object sender, RedisErrorEventArgs e) { } /// <summary> /// 更改集群 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void MuxerHashSlotMoved(object sender, HashSlotMovedEventArgs e) { // LogHelper.WriteInfoLog("HashSlotMoved:NewEndPoint" + e.NewEndPoint + ", OldEndPoint" + e.OldEndPoint); } /// <summary> /// redis类库错误 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void MuxerInternalError(object sender, InternalErrorEventArgs e) { } #endregion }
在以上RedisUtils帮助类的基础上封装一次调用:
/// <summary> /// Redis帮助类 /// </summary> public class RedisHelper { /// <summary> /// 缓存失效时长 /// </summary> public const int EXPIRY = 30; private static int CheckDbIndex(int dbIndex) { if (dbIndex > 16 || dbIndex < 0) { dbIndex = 0; } return dbIndex; } /// <summary> /// 获取缓存数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">Redis数据库索引</param> /// <param name="key">Redis键</param> /// <param name="fun">从其他地方获取数据源,并缓存到Redis中</param> /// <param name="timeout">过期时间,单位:分钟</param> /// <returns></returns> public static T GetObject<T>(int dbIndex, string key, Func<T> fun, int? timeout = EXPIRY) where T : class { dbIndex = CheckDbIndex(dbIndex); T data = RedisUtils.StringGet<T>(dbIndex, key); if (data != null) { return data; } if (fun != null) { data = fun(); } if (data != null) { TimeSpan? timeSp = null; if (timeout != null) timeSp = TimeSpan.FromMinutes(Convert.ToDouble(timeout)); RedisUtils.StringSet<T>(dbIndex, key, data, timeSp); } return data; } /// <summary> /// KV /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">库</param> /// <param name="key">键</param> /// <param name="func">如找不到则从func获取</param> /// <param name="timeout">超时时间</param> /// <returns></returns> public static T GetObject_KV<T>(int dbIndex, string key, Func<T> func, TimeSpan? timeout) where T : class { T data = RedisUtils.StringGet<T>(dbIndex, key); if (data != null) { return data; } if (func != null) { data = func(); } if (data != null) { RedisUtils.StringSet<T>(dbIndex, key, data, timeout); } return data; } /// <summary> /// 异步获取缓存数据 /// </summary> /// <typeparam name="T">数据集类型</typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key">键</param> /// <param name="fun">从其他地方获取数据源,并缓存到Redis中</param> /// <param name="timeout">过期时间,单位:分钟</param> /// <returns></returns> public static async Task<T> GetObjectAsync<T>(int dbIndex, string key, Func<T> fun, int timeout = EXPIRY) where T : class { dbIndex = CheckDbIndex(dbIndex); T data = RedisUtils.StringGet<T>(dbIndex, key); if (data != null) { return data; } if (fun != null) { data = await Task.Run(() => { return fun(); }); } if (data != null) { RedisUtils.StringSet<T>(dbIndex, key, data, TimeSpan.FromMinutes(timeout)); } return data; } /// <summary> /// 异步获取缓存数据 /// </summary> /// <typeparam name="T">数据集类型</typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key">键</param> /// <param name="fun">从其他地方获取数据源,并缓存到Redis中</param> /// <param name="timeout">过期时间,单位:分钟</param> /// <returns></returns> public static async Task<T> GetObjectAsync<T>(int dbIndex, string key, Func<RedisCache<T>> fun, int timeout = EXPIRY) where T : class { dbIndex = CheckDbIndex(dbIndex); RedisCache<T> cache = new RedisCache<T>(); cache.CacheData = RedisUtils.StringGet<T>(dbIndex, key); if (cache.CacheData != null) { return cache.CacheData; } var temp = await Task.Run(() => { return fun(); }); if (temp != null) cache = temp; if (cache.UseCache) { RedisUtils.StringSet<T>(dbIndex, key, cache.CacheData, TimeSpan.FromMinutes(timeout)); } return cache.CacheData; } /// <summary> /// 异步获取数据集合 /// </summary> /// <typeparam name="T">数据集类型</typeparam> /// <param name="dbIndex">数据库</param> /// <param name="key">键</param> /// <param name="fun">从其他地方获取数据源,并缓存到Redis中</param> /// <param name="timeout">过期时间,单位:分钟</param> /// <returns></returns> public static async Task<List<T>> GetListAsync<T>(int dbIndex, string key, Func<List<T>> fun, int timeout = EXPIRY) where T : class { dbIndex = CheckDbIndex(dbIndex); List<T> datas = RedisUtils.StringGet<List<T>>(dbIndex, key); if (datas != null && datas.Count > 0) { return datas; } datas = await Task.Run(() => { return fun(); }); if (datas != null && datas.Count > 0) { RedisUtils.StringSet<List<T>>(dbIndex, key, datas, TimeSpan.FromMinutes(timeout)); } return datas; } /// <summary> /// ZSet /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">库</param> /// <param name="key">键</param> /// <param name="func">如找不到则从func获取</param> /// <returns></returns> public static List<T> GetObject_ZSet<T>(int dbIndex, string key, Func<List<T>> func) where T : class { List<T> data = RedisUtils.SortedSetRangeByRank<T>(dbIndex, key); if (data != null && data.Count > 0) { return data; } if (func != null) { data = func(); } if (data != null) { RedisUtils.SortedSetAdd<T>(dbIndex, key, data); } return data; } /// <summary> /// Hash /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbIndex">库</param> /// <param name="hashID">hashID</param> /// <param name="key">键</param> /// <param name="func">如找不到则从func获取</param> /// <returns></returns> public static T GetObject_Hash<T>(int dbIndex, string hashID, string key, Func<T> func) where T : class { T data = RedisUtils.HashGet<T>(dbIndex, hashID, key); if (data != null) { return data; } if (func != null) { data = func(); } if (data != null) { RedisUtils.HashSet<T>(dbIndex, hashID, key, data); } return data; } }
C# WPF 用MediaElement控件实现视频循环播放
在WPF里用MediaElement控件,实现一个循环播放单一视频的程序,同时可以控制视频的播放、暂停、停止。
一种方式,使用MediaElement.MediaEnded事件,在视频播放结束后,自动重新播放;
另一种方式,使用WPF定时器,在定时器事件里写入视频播放代码。
后者优点是可以控制循环时长,不必等到视频播放结束就可以开始下一次播放,比如:同时启动多个播放程序,使多个时长不同的视频同时播放,无限循环,如果采用第一种方式,累计多次自动播放后,视频内容就无法同步。
第一种方式:
1 XAML: 2 <MediaElement x:Name="mediaElement" HorizontalAlignment="Left" Height="261" VerticalAlignment="Top" Width="507"/> 3 <Button x:Name="btnPlay" Content="Play" HorizontalAlignment="Left" Margin="68,279,0,0" VerticalAlignment="Top" Width="75" Click="btnPlay_Click"/> 4 <Button x:Name="btnPause" Content="Pause" HorizontalAlignment="Left" Margin="170,279,0,0" VerticalAlignment="Top" Width="75" Click="btnPause_Click"/> 5 <Button x:Name="btnStop" Content="Stop" HorizontalAlignment="Left" Margin="295,279,0,0" VerticalAlignment="Top" Width="75" Click="btnStop_Click"/>
1 C#: 2 // 窗口加载事件 3 private void Window_Loaded(object sender, RoutedEventArgs e) 4 { 5 // 绑定视频文件 6 mediaElement.Source = new Uri("D:/bird.mp4"); 7 8 // 交互式控制 9 mediaElement.LoadedBehavior = MediaState.Manual; 10 11 // 添加元素加载完成事件 -- 自动开始播放 12 mediaElement.Loaded += new RoutedEventHandler(media_Loaded); 13 14 // 添加媒体播放结束事件 -- 重新播放 15 mediaElement.MediaEnded += new RoutedEventHandler(media_MediaEnded); 16 17 // 添加元素卸载完成事件 -- 停止播放 18 mediaElement.Unloaded += new RoutedEventHandler(media_Unloaded); 19 } 20 21 /* 22 元素事件 23 */ 24 private void media_Loaded(object sender, RoutedEventArgs e) 25 { 26 (sender as MediaElement).Play(); 27 } 28 29 private void media_MediaEnded(object sender, RoutedEventArgs e) 30 { 31 // MediaElement需要先停止播放才能再开始播放, 32 // 否则会停在最后一帧不动 33 (sender as MediaElement).Stop(); 34 (sender as MediaElement).Play(); 35 } 36 37 private void media_Unloaded(object sender, RoutedEventArgs e) 38 { 39 (sender as MediaElement).Stop(); 40 } 41 42 /* 43 播放控制按钮的点击事件 44 */ 45 private void btnPlay_Click(object sender, RoutedEventArgs e) 46 { 47 mediaElement.Play(); 48 } 49 50 private void btnPause_Click(object sender, RoutedEventArgs e) 51 { 52 mediaElement.Pause(); 53 } 54 55 private void btnStop_Click(object sender, RoutedEventArgs e) 56 { 57 mediaElement.Stop(); 58 }
第二种方式:
注:使用DispatcherTimer,需要添加System.Windows.Threading命名空间。
1 XAML: 2 <MediaElement x:Name="mediaElement" HorizontalAlignment="Left" Height="243" Margin="19,10,0,0" VerticalAlignment="Top" Width="394" LoadedBehavior ="Manual"/> 3 <Button x:Name="btnPlay" Content="Play" HorizontalAlignment="Left" Margin="52,270,0,0" VerticalAlignment="Top" Width="75" Click="btnPlay_Click"/> 4 <Button x:Name="btnPause" Content="Pause" HorizontalAlignment="Left" Margin="163,270,0,0" VerticalAlignment="Top" Width="75" Click="btnPause_Click"/> 5 <Button x:Name="btnStop" Content="Stop" HorizontalAlignment="Left" Margin="266,270,0,0" VerticalAlignment="Top" Width="75" Click="btnStop_Click"/>
1 C#: 2 DispatcherTimer timer = new DispatcherTimer(); // 定时器timer 3 int durTime = 5; // 视频播放时长,也就是循环周期 4 5 // 窗口加载事件 6 private void Window_Loaded(object sender, RoutedEventArgs e) 7 { 8 mediaElement.Source = new Uri("D:/bird.mp4"); // 绑定视频文件 9 10 mediaElement.Play(); // 设置启动播放 11 timer.Interval = new TimeSpan(0, 0, 0, durTime); // 设置定时器重复周期 12 timer.Tick += new EventHandler(timerEvent); // 设置定时器事件 13 14 timer.Start(); // 启动定时器 15 } 16 17 // 定时器事件 18 public void timerEvent(object sender, EventArgs e) 19 { 20 // MediaElement需要先停止播放才能再开始播放, 21 // 否则会停在最后一帧不动 22 mediaElement.Stop(); 23 mediaElement.Play(); 24 } 25 26 /* 27 播放控制按钮的点击事件 28 */ 29 private void btnPlay_Click(object sender, RoutedEventArgs e) 30 { 31 mediaElement.Play(); // 开始播放 32 timer.Start(); // 重新启动定时器 33 } 34 35 private void btnPause_Click(object sender, RoutedEventArgs e) 36 { 37 mediaElement.Pause(); // 暂停当前播放 38 timer.Stop(); // 停止定时器 39 } 40 41 private void btnStop_Click(object sender, RoutedEventArgs e) 42 { 43 mediaElement.Stop(); // 停止当前播放 44 timer.Stop(); // 停止定时器 45 }
项目源码下载链接:https://files.cnblogs.com/files/walker-cheng/WpfVideoCyclePlayer.zip
net 异步与同步
一、摘论
为什么不是摘要呢?其实这个是我个人的想法,其实很多人在谈论异步与同步的时候都忽略了,同步异步不是软件的原理,其本身是计算机的原理及概念,这里就不过多的阐述计算机原理了。在学习同步与异步之前,我们需要先研究几个问题
在说到异步前,先来理一下几个容易混淆的概念,并行、多线程、异步。
并行,一般指并行计算,是说同一时刻有多条指令同时被执行,这些指令可能执行于同一CPU的多核上,或者多个CPU上,或者多个物理主机甚至多个网络中。
多线程,一般指同一进程中多个线程(包含其数据结构、上下文与代码片段)协作运行。在多核计算机中多个线程将有机会同时运行于多个核上,如果线程中进行的是计算,则行成并行计算。
异步,与同步相对应,是指呼叫另一操作后,不等待其结果,继续执行之后的操作,若之后没有其他操作,当前线程将进入睡眠状态,而CPU时间将有机会切至其他线程。在异步操作完成后通过回调函数的方式获取通知与结果。异步的实现方式有多种,如多线程与完成端口。多线程将异步操作放入另一线程中运行,通过轮询或回调方法得到完成通知;完成端口,由操作系统接管异步操作的调度,通过硬件中断,在完成时触发回调方法,此方式不需要占用额外线程。
通过上面的两张图,可以把三个概念透析的非常好理解,异步在某种意义上讲是“时空转换”即时间换空间,空间换时间。下边我们来学习下,在net 中的异步
二、同步和异步
1.同步执行
为了准备一个耗时的程序,本人准备了一本Txt修仙小说,我们用程序读取一行行输出,输出完成以后,我们输出一句话,"今天书就读到这里吧!!累了,休息一会,休息一会!一休哥",为了更好的演示同步异步,本文采用winform程序,同时为了体验winform 和控制台 带来的视觉效果,我们选择项目属性,应用程序这选择控制台。
在准备一个很费时的读书方法,
/// <summary> /// 读书,一个很废时间的任务 /// </summary> public void ReadBook() { //我们可以通过 Thread.CurrentThread.ManagedThreadId 获取当前线程的唯一标识符 Console.WriteLine("********************** ReadBook Start【" + Thread.CurrentThread.ManagedThreadId + "】等待............... **********************************************"); System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch(); watch.Start(); string Path= AppDomain.CurrentDomain.BaseDirectory + "zqjz.txt"; List<string> list = new List<string>(); System.IO.StreamReader sr = new System.IO.StreamReader(Path, Encoding.Default); string line = ""; Console.ForegroundColor = ConsoleColor.Black; while ((line = sr.ReadLine()) != null&& list.Count<120) { char[] array= line.ToArray(); for (int i = 0; i < array.Length; i++) { Console.Write(array[i]); if (i!=0) { // Thread.Sleep(128);//人眼最快敏感视觉是128毫秒左右,我们这里测试先使用10毫秒 Thread.Sleep(10); } } Console.WriteLine(); Console.BackgroundColor = (ConsoleColor)new Random().Next(3, 15); list.Add(line); } sr.Close(); sr.Dispose(); watch.Stop(); Console.WriteLine("今天读书用了"+ watch.ElapsedMilliseconds+"豪秒"); Console.WriteLine("********************** ReadBook End【" + Thread.CurrentThread.ManagedThreadId + " 】**********************************************"); }
这个方法比较简单,就是读取电子书,同时给方法加上了耗时记录,和当前线程的唯一标识。现在我们在窗体上加上一个buttion 调用下我们的读书。看看结果是怎么样的,同时建议打开任务管理器,监控下CPU,等cpu 平稳以后,我们在点击同步执行按钮。“现在是我们自己在读书”。
2.异步执行
关于异步在前边的摘论里面介绍了大概,这里不过多演示,请继续看!在早期,net 的异步都是在使用委托来做的,而委托使用的是线程池ThreadPool来实现的,曾取下一篇文章介绍线程,到时候在详细介绍线程池,关于委托请观看本人前边的文章 "linq to Objet",我们在程序上在加上一个按钮,里面老师读书,我的心缺飞了,在想下课玩什么?怎么和同学玩。
private void btnSync_Click(object sender, EventArgs e) {//同步 Console.WriteLine("**********************btnSync_Click Start【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************"); ReadBook(); MessageBox.Show("今天书就读到这里吧!!累了,休息一会,休息一会!一休哥"); Console.WriteLine("**********************btnSync_Click End 【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************"); } private void btnasync_Click(object sender, EventArgs e) {//异步 Console.WriteLine("**********************btnSync_Click Start【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************"); Action action = new Action(() => ReadBook()); action.BeginInvoke(null,null);//参数先不管,我们先给null,一会我们会继续演示 MessageBox.Show("今天想玩,怎么骗过老师呢!!书还在继续读,但是我已经在玩了!!!"); Console.WriteLine("**********************btnSync_Click End 【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************"); }
上面代码分别为异步调用和同步调用,下图为异步调用结果,我们会发现,异步调用窗体是可以移动的,并且CPU 会有很大的波峰,细心的人会发现,执行时间是一样的,只是他们的线程唯一标识是不一样的。
通过上述演示,异步和同步的区别很简单了吧!这里就不过多描述,自己总结。但是我们的要说下异步和多线程的区别?其实异步只是一种结果(目地),而多线程才是实现这种结果的一种方式,在NET 里面,异步和多线程没有本质的区别,个人总结唯一的区别就是,应用场景不同。
重点:多播委托不可以指定异步。不予显示,自己去尝试和找寻原理,实在找不到原理可以理解为这是任何高级语言的一个规定。有关多播委托请参考本人:一步一步带你了解 Linq to Object
三、异步回掉和异步等待(阻塞)
1.异步回掉:
刚才我们一直在上课读书,但是我的心里在想的是下课去哪里玩,如何玩?这个时候,我们需要在异步读书的方法之后也就是下课以后再去玩。看下代码是怎么写的。
//异步 Console.WriteLine("**********************btnSync_Click Start【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************"); #region 异步回调 IAsyncResult iAsyncResult = null; AsyncCallback callback = t => { Console.WriteLine(t); Console.WriteLine("下边代码是比较两个对象是否一样"); Console.WriteLine($"string.ReferenceEquals(t, iAsyncResult)={string.ReferenceEquals(t, iAsyncResult)}"); Console.WriteLine($"当前线程ID {Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"终于下课了!我们走吧,尽情的玩吧,你问我老师讲的啥,我知道!!"); };//AsyncCallback 本身就是一个委托,这个委托有一个参数,这个参数就是我们委托的BeginInvoke的返回值。我们使用这个委托去做异步回调 #endregion Action action = () => ReadBook();//简写 iAsyncResult= action.BeginInvoke(callback, null);//这里的第一个参数,我们就是异步回调 MessageBox.Show("今天想玩,怎么骗过老师呢,下课玩点什么呢!!书还在继续读,但是我的心已经飞了!!!"); Console.WriteLine("**********************btnSync_Click End 【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************");
所谓的异步回调,就是在异步线程执行完在执行的代码块。
2.异步等待(阻塞):
主线程等待子线程有这么几种方式:
1.主线程等待子线程的时候有返回,比如说我们常见的进度条功能,执行一点我就返回下。2.主线程等待子线程有时间限制,例如:中午放学我等你五分钟,你要是不完事,我就先吃饭去了。3.主线程等待子线程无返回,比如死等,今天的代码我学不会了,我就不睡觉了。下面我们分别看看这三种情况。我们管操作线程等待的过程叫做阻塞(se)进程.阻塞主进程以后等待子线程的执行,我们成为线程的阻塞,
刚才我们是使用回调,在异步执行完成,才执行了一个代码块,这个时候messagebax 已经输出了,现在我们开看看课堂下的学生表现。“将下列代码放到我们 MessageBox.Show("今天想玩,怎么骗过老师呢,下课玩点什么呢!!书还在继续读,但是我的心已经飞了!!!");”之后,我们来看看
while (!iAsyncResult.IsCompleted)//边等待边操作,可以用于做进度条 { Thread.Sleep(100);//建议控制100毫秒一次 Console.WriteLine("老师还在教我们读书.....请等待..............."); } //当异步完成以后,我们在执行下边的这句话 Console.WriteLine("学生甲:冲啊..............打篮球全"); Console.WriteLine("学生乙:王美美.......我爱你!咱们交往吧....*#*#*#**??!"); Console.WriteLine("学生丙:呼呼呼呼呼呼呼呼。。。。。噜。。。。。。。。。。今天的肉丸子真好吃,真希望这不是梦啊"); Console.WriteLine("学生丁:大海啊,就像妈妈一样,海浪啊!你为啥这么猛!总是在我人生巅峰......被打断"); Console.WriteLine("学生丙:别BiBi了,海浪是你后妈,滚一边去淫诗去!别TMD打扰老子睡觉");
刚才执行的线程等待在阻塞的过程中是有损耗的,我们损耗 的是时间,所以回调会在子线程之前执行,那么我们想要无损耗怎么去写,怎么去阻塞我们的主线程呢 “ bool RunBool = iAsyncResult.AsyncWaitHandle.WaitOne();”; 当子线程执行成功了,就会返回TRUE,当子线程执行过程中出现exection 以后,就返回false;
这种写法主线程就无法返回了。但是我们可以新建立一个线程去监控子线程。这里就不写那么复杂了。
第二种情况,我只等你两秒钟,有时间限制的阻塞
#region 异步等待1 有损耗 带返回 //while (!iAsyncResult.IsCompleted)//边等待边操作,可以用于做进度条 //{ // Thread.Sleep(100);//建议控制100毫秒一次 // Console.WriteLine("老师还在教我们读书.....请等待..............."); //} #endregion #region 异步等待2 无损耗 无返回 //bool RunBool = iAsyncResult.AsyncWaitHandle.WaitOne();//返回结果是子线程执行成功或者失败,不是实时返回的。 //iAsyncResult.AsyncWaitHandle.WaitOne(-1);//写法2 #endregion #region 有时间限制的异步等待 iAsyncResult.AsyncWaitHandle.WaitOne(2000);//我最多等你2秒钟,如果你提前完事,我们提前走 #endregion //当异步完成以后,我们在执行下边的这句话 Console.WriteLine("学生甲:冲啊..............打篮球全"); Console.WriteLine("学生乙:王美美.......我爱你!咱们交往吧....*#*#*#**??!"); Console.WriteLine("学生丙:呼呼呼呼呼呼呼呼。。。。。噜。。。。。。。。。。今天的肉丸子真好吃,真希望这不是梦啊"); Console.WriteLine("学生丁:大海啊,就像妈妈一样,海浪啊!你为啥这么猛!总是在我人生巅峰......被打断"); Console.WriteLine("学生丙:别BiBi了,海浪是你后妈,滚一边去淫诗去!别TMD打扰老子睡觉"); Console.WriteLine("**********************btnSync_Click End 【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************");
这种子线程执行两秒以后,主线程在执行这个问题经常会在面试里面问。面试经常会问,主线程A 至少要执行10,秒,子线程B至少要执行30秒,如何让主线程在子线程执行20秒开始执行。
下边我们就举例,代码不会,我就要学习了学习不会就不睡觉,就死学到底了。
#region 异步等待死等 //死等就是,只要你不异常,就必须给我一个结果,比如学习,必须学会为止 action.EndInvoke(iAsyncResult);//EndInvoke 的返回值取决与你的委托!你的委托有返回值,我就有返回值。 #endregion
注意图上反应的问题。其实回调执行的是子线程。我们死等(阻塞 主线程等待子线程)的是子线程,而不是子线程的回调。这个时候是主线程和子线程一起执行的(线程的无序)。这就会照成CPU 更大的波峰,很容易宕机。由于演示这种结果不容易,需要执行很多遍,这里没有截取到CPU 波峰。本人I7 CPU 基本都赶到顶了。
通过上图可以看出,主线程和子线程的执行先后顺序不一定谁先后,线程是无序的。
如果下了本文demo 的同学会发现,这个时候UI 是卡住的,主窗体UI阻塞,所以窗体是无法移动的。
。到这里异步我们就学习完了,下边总结下
四、总结
1.异步等待和异步回调的区别?面试会考的哦!!
答:异步等待是在子线程执行完成以后回到主线程,j解除主线程的阻塞继续执行,而异步回调是子线程执行完成以后在去以子线程再去执行的任务代码块。
异步等待卡主线程,回调不卡主线程。
在委托中回调不可以取得子线程执行的结果,等待可以通过线程状态参数取得执行结果。
2.主线程A 需要执行1秒,而子线程B需要执行3秒。如果让B执行2秒以后在执行?或者 接口A 调用5秒没结果,我就调用接口B去取数据?在接口B取到数据以后,接口如果也取到数据,仍然使用结果B的,怎么去做。
答:使用 iAsyncResult.AsyncWaitHandle.WaitOne(2000);
关于接口(WebApi ,Service)的情况,我们也是需要使用线程等待,但是这个时候我们就要加锁或者加计时器 StopWatch 去做。关于锁以后在谈。但是加锁会影响效率,计时器在多服务情况下还不准确,这是大多数面试者的回答。
我们把没有演示的一点点知识在这里演示下。
我们一直没有说这个参数有什么做用,这里简单介绍下。当我线程启动的时候,我可以启动多条线程,但是我无法确定那个线程执行的过程,这个时候我们可以通过这个参数传递线程状态。这里不过多解释。有用到的私聊本人。
3.如果我想使用子线程的结果去做主线程的参数,如何去做。请说明你的理由。这里不过多解释了,案列很清晰。
4.这里的阻塞是卡主线程的,我们如何不卡主线程??
下节多线程中找答案。
个人总结:
1.net 异步支持
Net framework可以让你异步调用任何方法。为达这样的目的,你可以定义一个与你要调用的方法的签名相同的委托。公共语言运行时将自动为该委托定义与签名相同的BeginInvok和EndInvoke方法。
异步委托调用BeginInvok和EndInvoke方法,但在.NET Compact Framework中并不支持。
.NET Framework 允许您异步调用任何方法。定义与您需要调用的方法具有相同签名的委托;公共语言运行库将自动为该委托定义具有适当签名
的 BeginInvoke 和 EndInvoke 方法。
BeginInvoke 方法用于启动异步调用。它与您需要异步执行的方法具有相同的参数,只不过还有两个额外的参数(将在稍后描述)。
BeginInvoke 立即返回,不等待异步调用完成。
BeginInvoke 返回 IasyncResult,可用于监视调用进度。
EndInvoke 方法用于检索异步调用结果。调用 BeginInvoke 后可随时调用 EndInvoke 方法;如果异步调用未完成,EndInvoke 将一直阻塞到
异步调用完成。EndInvoke 的参数包括您需要异步执行的方法的 out 和 ref 参数(在 Visual Basic 中为 <Out> ByRef 和 ByRef)以及由
BeginInvoke 返回的 IAsyncResult。
四种使用 BeginInvoke 和 EndInvoke 进行异步调用的常用方法。调用了 BeginInvoke 后,可以:
1.进行某些操作,然后调用 EndInvoke 一直阻塞到调用完成。
2.使用 IAsyncResult.AsyncWaitHandle 获取 WaitHandle,使用它的 WaitOne 方法将执行一直阻塞到发出 WaitHandle 信号,然后调用
EndInvoke。这里主要是主程序等待异步方法,等待异步方法的结果。
3.轮询由 BeginInvoke 返回的 IAsyncResult,IAsyncResult.IsCompeted确定异步调用何时完成,然后调用 EndInvoke。此处理个人认为与
相同。
4.将用于回调方法的委托传递给 BeginInvoke。该方法在异步调用完成后在 ThreadPool 线程上执行,它可以调用 EndInvoke。这是在强制装
换回调函数里面IAsyncResult.AsyncState(BeginInvoke方法的最后一个参数)成委托,然后用委托执行EndInvoke。
警告 始终在异步调用完成后调用 EndInvoke。
通过EndInvoke方法检测异步调用的结果。如果异步调用尚未完成,EndInvoke将阻塞调用线程,直到它完成。EndInvoke参数包括out和ref参数,本文没有讲到,另外本文没有演示EndInvoke 返回值 。
2.同步方法和异步方法的区别
同步方法调用在程序继续执行之前需要等待同步方法执行完毕返回结果
异步方法则在被调用之后立即返回以便程序在被调用方法完成其任务的同时执行其它操作
Demo 下载
88.睡觉