第一節: Timer的定時任務的複習、Quartz.Net的入門使用、Aop思想的體現 第十九節: 結合【表達式目錄樹】來封裝EF的BaseDal層的方法 第二十節: 深刻理解併發機制以及解...

第一節: Timer的定時任務的複習、Quartz.Net的入門使用、Aop思想的體現

 

一. 前奏-Timer類實現定時任務html

   在沒有引入第三方開源的定時調度框架以前,咱們處理一些簡單的定時任務同時都是使用Timer類, DotNet中的Timer類有三個,分別位於不一樣的命名空間下,分別是:nginx

    ①.位於System.Windows.Forms裏,即定時器控件,不過多介紹了git

    ②.位於System.Threading.Timer類裏 (重點介紹)github

    ③.位於System.Timers.Timer類裏 (不介紹)sql

  下面重點介紹第二種,位於Threading下面的Timer類,觀察其源碼,發現有多種構造函數,咱們重點介紹其中的一種。數據庫

 

  分享一段代碼:2秒後開啓該線程,而後每隔4s調用一次。編程

1           //2秒後開啓該線程,而後每隔4s調用一次
2             System.Threading.Timer timer = new System.Threading.Timer((n) =>
3             {
4                 //書寫業務邏輯
5                 Console.WriteLine("我是定時器中的業務邏輯哦{0}",n);
6             }, "1", 2000, 4000);

   分析總結:上面的代碼顯而易見,只能控制:延遲多久開始執行,每隔多久執行一次,至於執行多少次、什麼時間關閉均沒法實現,更不用說處理一些複雜的時間間隔了,因此Timer類僅僅適合處理對時間要求很是簡單的定時任務。設計模式

 

二. 進入主題-Quartz.Net的入門使用數組

使用步驟:緩存

  前提:經過NuGet引入程序集或者直接下載源碼進行引入,而後分五步走。

  步驟一:建立做業調度池(Scheduler)

  步驟二:建立一個具體的做業即job (具體的job須要單獨在一個文件中執行)

  步驟三:建立並配置一個觸發器即trigger

  步驟四:將job和trigger加入到做業調度池中

  步驟五:開啓調度

 下面分享一段簡單的代碼(當即執行、每隔一秒執行一次、永久執行)

複製代碼
1        /// <summary>
 2         /// Quartz框架的使用
 3         /// </summary>
 4         public static void Show()
 5         {
 6             //1.建立做業調度池(Scheduler)
 7             IScheduler scheduler =StdSchedulerFactory.GetDefaultScheduler();
 8            
 9             //2.建立一個具體的做業即job (具體的job須要單獨在一個文件中執行)
10             var job = JobBuilder.Create<HelloJob>().Build();
11 
12             //3.建立並配置一個觸發器即trigger   1s執行一次
13             var trigger = TriggerBuilder.Create().WithSimpleSchedule(x => x.WithIntervalInSeconds(1)
14                                                                            .RepeatForever()).Build();
15             //4.將job和trigger加入到做業調度池中
16             scheduler.ScheduleJob(job, trigger);
17 
18             //5.開啓調度
19             scheduler.Start();
20         }
複製代碼
複製代碼
1     /// <summary>
 2     /// 實現IJob接口
 3     /// </summary>
 4     class HelloJob : IJob
 5     {
 6         void IJob.Execute(IJobExecutionContext context)
 7         {
 8             Console.WriteLine("Hellow JOB");
 9         }
10     }
複製代碼

  分析:每一個Job都須要實現IJob接口,而且顯式的實現Execute方法;建立調度器除了上述方法外,還能夠:

1   //另一種建立調度池的方法
2   var factory = new StdSchedulerFactory();
3   IScheduler scheduler2 = factory.GetScheduler();

  執行結果:

 

三. 擴展-Aop思想的體現

   咱們想在每次Job執行的先後,分別執行一段通用的業務,但有不想和原業務寫在一塊兒,這個時候就須要面向切面編程了,即AOP思想的體現。

   Quartz.Net中Aop思想經過JobListener來實現,代碼以下:

複製代碼
1         /// <summary>
 2         /// Quartz中的AOP思想
 3         /// </summary>
 4         public static void AopShow()
 5         {
 6             //1.建立Schedule
 7             IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler();
 8             
 9             //2.建立job (具體的job須要單獨在一個文件中執行)
10             var job = JobBuilder.Create<HelloJob>().Build();
11 
12             //3.配置trigger   1s執行一次
13             var trigger = TriggerBuilder.Create().WithSimpleSchedule(x => x.WithIntervalInSeconds(1)
14                                                                            .RepeatForever()).Build();
15             //AOP配置
16             scheduler.ListenerManager.AddJobListener(new MyAopListener(), GroupMatcher<JobKey>.AnyGroup());
17 
18             //4.將job和trigger加入到做業調度池中
19             scheduler.ScheduleJob(job, trigger);
20 
21             //5. 開始調度
22             scheduler.Start();
23         }
24     /// <summary>
25     /// Aop類
26     /// </summary>
27     public class MyAopListener : IJobListener
28     {
29         public string Name
30         {
31             get
32             {
33                 return "hello world";
34             }
35         }
36         public void JobExecutionVetoed(IJobExecutionContext context)
37         {
38 
39         }
40         public void JobToBeExecuted(IJobExecutionContext context)
41         {
42             Console.WriteLine("執行前寫入日誌");
43         }
44         public void JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException)
45         {
46             Console.WriteLine("執行後寫入日誌");
47         }
48     }
複製代碼

  執行結果:

 

 

 

 

第十九節: 結合【表達式目錄樹】來封裝EF的BaseDal層的方法

 

一. 簡介

   該章節,能夠說是一個簡單輕鬆的章節,只要你對Expression表達式樹、EF的基本使用、泛型有所瞭解,那麼本章節實質上就是一個很是簡單的封裝章節,便於咱們快捷開發。

 PS:在該章節對於EF的上下文怎麼處理,怎麼來的,不作介紹,在後續的框架篇將詳細介紹,下面的EF上下文,將直接使用db代替。

 若是你對Expression、EF的增刪改查、泛型生疏的話,能夠先閱讀如下章節:

  (1). Expression表達式目錄樹:http://www.cnblogs.com/yaopengfei/p/7486870.html

  (2). EF的基本增刪改查:http://www.cnblogs.com/yaopengfei/p/7674715.html

  (3). 泛型的使用:http://www.cnblogs.com/yaopengfei/p/6880629.html

二. 代碼封裝分享

   下面的代碼封裝,主要就是圍繞EF的增刪改查進行封裝以及各自對應的擴展,其中包括事務一體的封裝、事務分離的封裝、集成 Z.EntityFramework.Extensions 插件的封裝、以及EF調用SQL語句的封裝。

1. EF調用SQL語句:

複製代碼
1         /// <summary>
 2         /// 執行增長,刪除,修改操做(或調用存儲過程)
 3         /// </summary>
 4         /// <param name="sql"></param>
 5         /// <param name="pars"></param>
 6         /// <returns></returns>
 7         public int ExecuteSql(string sql, params SqlParameter[] pars)
 8         {
 9             return db.Database.ExecuteSqlCommand(sql, pars);
10         }
11 
12         /// <summary>
13         /// 執行查詢操做
14         /// </summary>
15         /// <typeparam name="T"></typeparam>
16         /// <param name="sql"></param>
17         /// <param name="pars"></param>
18         /// <returns></returns>
19         public List<T> ExecuteQuery<T>(string sql, params SqlParameter[] pars)
20         {
21             return db.Database.SqlQuery<T>(sql, pars).ToList();
22         }
複製代碼

2. EF增刪改查封裝(事務一體)

 (1). 新增

1   public int Add(T model)
2   {
3        DbSet<T> dst = db.Set<T>();
4        dst.Add(model);
5        return db.SaveChanges();
6   }

(2). 刪除

複製代碼
1         /// <summary>
 2         /// 刪除(適用於先查詢後刪除的單個實體)
 3         /// </summary>
 4         /// <param name="model">須要刪除的實體</param>
 5         /// <returns></returns>
 6         public int Del(T model)
 7         {
 8             db.Set<T>().Attach(model);
 9             db.Set<T>().Remove(model);
10             return db.SaveChanges();
11         }
12         /// <summary>
13         /// 根據條件刪除(支持批量刪除)
14         /// </summary>
15         /// <param name="delWhere">傳入Lambda表達式(生成表達式目錄樹)</param>
16         /// <returns></returns>
17         public int DelBy(Expression<Func<T, bool>> delWhere)
18         {
19             List<T> listDels = db.Set<T>().Where(delWhere).ToList();
20             listDels.ForEach(d =>
21             {
22                 db.Set<T>().Attach(d);
23                 db.Set<T>().Remove(d);
24             });
25             return db.SaveChanges();
26         }
複製代碼

(3). 查詢

複製代碼
1         /// <summary>
 2         /// 根據條件查詢
 3         /// </summary>
 4         /// <param name="whereLambda">查詢條件(lambda表達式的形式生成表達式目錄樹)</param>
 5         /// <returns></returns>
 6         public List<T> GetListBy(Expression<Func<T, bool>> whereLambda)
 7         {
 8             return db.Set<T>().Where(whereLambda).ToList();
 9         }
10         /// <summary>
11         /// 根據條件排序和查詢
12         /// </summary>
13         /// <typeparam name="Tkey">排序字段類型</typeparam>
14         /// <param name="whereLambda">查詢條件</param>
15         /// <param name="orderLambda">排序條件</param>
16         /// <param name="isAsc">升序or降序</param>
17         /// <returns></returns>
18         public List<T> GetListBy<Tkey>(Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true)
19         {
20             List<T> list = null;
21             if (isAsc)
22             {
23                 list = db.Set<T>().Where(whereLambda).OrderBy(orderLambda).ToList();
24             }
25             else
26             {
27                 list = db.Set<T>().Where(whereLambda).OrderByDescending(orderLambda).ToList();
28             }
29             return list;
30         }
31         /// <summary>
32         /// 分頁查詢
33         /// </summary>
34         /// <typeparam name="Tkey">排序字段類型</typeparam>
35         /// <param name="pageIndex">頁碼</param>
36         /// <param name="pageSize">頁容量</param>
37         /// <param name="whereLambda">查詢條件</param>
38         /// <param name="orderLambda">排序條件</param>
39         /// <param name="isAsc">升序or降序</param>
40         /// <returns></returns>
41         public List<T> GetPageList<Tkey>(int pageIndex, int pageSize, Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true)
42         {
43 
44             List<T> list = null;
45             if (isAsc)
46             {
47                 list = db.Set<T>().Where(whereLambda).OrderBy(orderLambda)
48                .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
49             }
50             else
51             {
52                 list = db.Set<T>().Where(whereLambda).OrderByDescending(orderLambda)
53               .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
54             }
55             return list;
56         }
57         /// <summary>
58         /// 分頁查詢輸出總行數
59         /// </summary>
60         /// <typeparam name="Tkey">排序字段類型</typeparam>
61         /// <param name="pageIndex">頁碼</param>
62         /// <param name="pageSize">頁容量</param>
63         /// <param name="whereLambda">查詢條件</param>
64         /// <param name="orderLambda">排序條件</param>
65         /// <param name="isAsc">升序or降序</param>
66         /// <returns></returns>
67         public List<T> GetPageList<Tkey>(int pageIndex, int pageSize, ref int rowCount, Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true)
68         {
69             int count = 0;
70             List<T> list = null;
71             count = db.Set<T>().Where(whereLambda).Count();
72             if (isAsc)
73             {
74                 var iQueryList = db.Set<T>().Where(whereLambda).OrderBy(orderLambda)
75                    .Skip((pageIndex - 1) * pageSize).Take(pageSize);
76 
77                 list = iQueryList.ToList();
78             }
79             else
80             {
81                 var iQueryList = db.Set<T>().Where(whereLambda).OrderByDescending(orderLambda)
82                  .Skip((pageIndex - 1) * pageSize).Take(pageSize);
83                 list = iQueryList.ToList();
84             }
85             rowCount = count;
86             return list;
87         }
複製代碼

(4). 修改

複製代碼
1         /// <summary>
  2         /// 修改
  3         /// </summary>
  4         /// <param name="model">修改後的實體</param>
  5         /// <returns></returns>
  6         public int Modify(T model)
  7         {
  8             db.Entry(model).State = EntityState.Modified;
  9             return db.SaveChanges();
 10         }
 11 
 12         /// <summary>
 13         /// 單實體擴展修改(把不須要修改的列用LAMBDA數組表示出來)
 14         /// </summary>
 15         /// <param name="model">要修改的實體對象</param>
 16         /// <param name="ignoreProperties">不需要修改的相關字段</param>
 17         /// <returns>受影響的行數</returns>
 18         public int Modify(T model, params Expression<Func<T, object>>[] ignoreProperties)
 19         {
 20             using (DbContext db = new DBContextFactory().GetDbContext())
 21             {
 22                 db.Set<T>().Attach(model);
 23 
 24                 DbEntityEntry entry = db.Entry<T>(model);
 25                 entry.State = EntityState.Unchanged;
 26 
 27                 Type t = typeof(T);
 28                 List<PropertyInfo> proInfos = t.GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList();
 29 
 30                 Dictionary<string, PropertyInfo> dicPros = new Dictionary<string, PropertyInfo>();
 31                 proInfos.ForEach(
 32                     p => dicPros.Add(p.Name, p)
 33                     );
 34 
 35                 if (ignoreProperties != null)
 36                 {
 37                     foreach (var ignorePropertyExpression in ignoreProperties)
 38                     {
 39                         //根據表達式獲得對應的字段信息
 40                         var ignorePropertyName = new PropertyExpressionParser<T>(ignorePropertyExpression).Name;
 41                         dicPros.Remove(ignorePropertyName);
 42                     }
 43                 }
 44 
 45                 foreach (string proName in dicPros.Keys)
 46                 {
 47                     entry.Property(proName).IsModified = true;
 48                 }
 49                 return db.SaveChanges();
 50             }
 51         }
 52 
 53         /// <summary>
 54         /// 批量修改(非lambda)
 55         /// </summary>
 56         /// <param name="model">要修改實體中 修改後的屬性 </param>
 57         /// <param name="whereLambda">查詢實體的條件</param>
 58         /// <param name="proNames">lambda的形式表示要修改的實體屬性名</param>
 59         /// <returns></returns>
 60         public int ModifyBy(T model, Expression<Func<T, bool>> whereLambda, params string[] proNames)
 61         {
 62             List<T> listModifes = db.Set<T>().Where(whereLambda).ToList();
 63             Type t = typeof(T);
 64             List<PropertyInfo> proInfos = t.GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList();
 65             Dictionary<string, PropertyInfo> dicPros = new Dictionary<string, PropertyInfo>();
 66             proInfos.ForEach(p =>
 67             {
 68                 if (proNames.Contains(p.Name))
 69                 {
 70                     dicPros.Add(p.Name, p);
 71                 }
 72             });
 73             foreach (string proName in proNames)
 74             {
 75                 if (dicPros.ContainsKey(proName))
 76                 {
 77                     PropertyInfo proInfo = dicPros[proName];
 78                     object newValue = proInfo.GetValue(model, null);
 79                     foreach (T m in listModifes)
 80                     {
 81                         proInfo.SetValue(m, newValue, null);
 82                     }
 83                 }
 84             }
 85             return db.SaveChanges();
 86         }
 87 
 88         /// <summary>
 89         /// 批量修改(支持lambda)
 90         /// </summary>
 91         /// <param name="model">要修改實體中 修改後的屬性 </param>
 92         /// <param name="whereLambda">查詢實體的條件</param>
 93         /// <param name="proNames">lambda的形式表示要修改的實體屬性名</param>
 94         /// <returns></returns>
 95         public int ModifyBy(T model, Expression<Func<T, bool>> whereLambda, params Expression<Func<T, object>>[] proNames)
 96         {
 97             List<T> listModifes = db.Set<T>().Where(whereLambda).ToList();
 98             Type t = typeof(T);
 99             List<PropertyInfo> proInfos = t.GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList();
100             Dictionary<string, PropertyInfo> dicPros = new Dictionary<string, PropertyInfo>();
101             if (proNames != null)
102             {
103                 foreach (var myProperyExp in proNames)
104                 {
105                     var my_ProName = new PropertyExpressionParser<T>(myProperyExp).Name;
106                     proInfos.ForEach(p =>
107                     {
108                         if (p.Name.Equals(my_ProName))
109                         {
110                             dicPros.Add(p.Name, p);
111                         }
112                     });
113                     if (dicPros.ContainsKey(my_ProName))
114                     {
115                         PropertyInfo proInfo = dicPros[my_ProName];
116                         object newValue = proInfo.GetValue(model, null);
117                         foreach (T m in listModifes)
118                         {
119                             proInfo.SetValue(m, newValue, null);
120                         }
121                     }
122                 }
123             }
124             return db.SaveChanges();
125         }
126
複製代碼

3. EF增刪改封裝(事務分離)

 (1). 事務批量處理

複製代碼
1         /// <summary>
2         /// 事務批量處理
3         /// </summary>
4         /// <returns></returns>
5         public int SaveChange()
6         {
7             return db.SaveChanges();
8         }
複製代碼

(2). 新增

複製代碼
1         /// <summary>
2         /// 新增
3         /// </summary>
4         /// <param name="model">須要新增的實體</param>
5         public void AddNo(T model)
6         {
7             db.Set<T>().Add(model);
8         }
複製代碼

(3). 修改

複製代碼
1         /// <summary>
2         /// 修改
3         /// </summary>
4         /// <param name="model">修改後的實體</param>
5         public void ModifyNo(T model)
6         {
7             db.Entry(model).State = EntityState.Modified;
8         }
複製代碼

(4). 刪除

複製代碼
/// <summary>
        /// 刪除
        /// </summary>
        /// <param name="model">須要刪除的實體</param>
        public void DelNo(T model)
        {
            db.Entry(model).State = EntityState.Deleted;
        }
         /// <summary>
         /// 條件刪除
        /// </summary>
        /// <param name="delWhere">須要刪除的條件</param>
        public void DelByNo(Expression<Func<T, bool>> delWhere)
        {
            List<T> listDels = db.Set<T>().Where(delWhere).ToList();
            listDels.ForEach(d =>
            {
                db.Set<T>().Attach(d);
                db.Set<T>().Remove(d);
            });
        }
複製代碼

4. Z.EntityFramework.Extensions 插件封裝

方案一:在使用EF事務分離的方法的前提下,單獨調用提交方法

複製代碼
1         /// <summary>
2         /// 事務提交,速度約爲saveChange的10倍-15倍
3         /// </summary>
4         public void BulkSaveChanges()
5         {
6             db.BulkSaveChanges();
7         }
複製代碼

方案二:插件特有的增刪改方法

複製代碼
/// <summary>
        /// 新增
        /// </summary>
        /// <param name="model">新增的實體集合</param>
        public void BulkInsert(List<T> model)
        {
            db.BulkInsert<T>(model);
        }
        /// <summary>
        /// 刪除
        /// </summary>
        /// <param name="model">須要刪除的實體集合</param>
        public void BulkDelete(List<T> model)
        {
            db.BulkDelete<T>(model);
        }
        /// <summary>
        /// 根據條件刪除
        /// </summary>
        /// <param name="delWhere">刪除條件</param>
        public void BulkDeleteBy(Expression<Func<T, bool>> delWhere)
        {
            List<T> listDels = db.Set<T>().Where(delWhere).ToList();
            db.BulkDelete<T>(listDels);
        }
        /// <summary>
        /// 須要修改的實體集合
        /// </summary>
        /// <param name="model"></param>
        public void BulkUpdate(List<T> model)
        {
            db.BulkUpdate<T>(model);
        }
複製代碼

 

 

第二十節: 深刻理解併發機制以及解決方案(鎖機制、EF自有機制、隊列模式等)

 

一. 理解併發機制

1. 什麼是併發,併發與多線程有什麼關係?

①. 先從廣義上來講,或者從實際場景上來講.

  高併發一般是海量用戶同時訪問(好比:12306買票、淘寶的雙十一搶購),若是把一個用戶看作一個線程的話那麼併發能夠理解成多線程同時訪問,高併發即海量線程同時訪問。

      (ps:咱們在這裏模擬高併發能夠for循環多個線程便可)

②.從代碼或數據的層次上來講.

  多個線程同時在一條相同的數據上執行多個數據庫操做。

2. 從代碼層次上來講,給併發分類。

①.積極併發(樂觀併發、樂觀鎖):不管什麼時候從數據庫請求數據,數據都會被讀取並保存到應用內存中。數據庫級別沒有放置任何顯式鎖。數據操做會按照數據層接收到的前後順序來執行。

 積極併發本質就是運行衝突發生,而後在代碼自己採起一種合理的方式去解決這個併發衝突,常見的方式有:

a.忽略衝突強制更新:數據庫會保存最後一次更新操做(以更新爲例),會損失不少用戶的更新操做。

b.部分更新:容許全部的更改,可是不容許更新完整的行,只有特定用戶擁有的列更新了。這就意味着,若是兩個用戶更新相同的記錄但卻不一樣的列,那麼這兩個更新都會成功,並且來自這兩個用戶的更改都是可見的。(EF默認實現不了這種狀況、能夠考略SQL語句、或者EF擴展只容許修改指定列來處理這種問題)

c.詢問用戶:當一個用戶嘗試更新一個記錄時,可是該記錄自從他讀取以後已經被別人修改了,這時應用程序就會警告該用戶該數據已經被某人更改了,而後詢問他是否仍然要重寫該數據仍是首先檢查已經更新的數據。(EF能夠實現這種狀況,在後面詳細介紹)

d.拒絕修改:當一個用戶嘗試更新一個記錄時,可是該記錄自從他讀取以後已經被別人修改了,此時告訴該用戶不容許更新該數據,由於數據已經被某人更新了。

(EF能夠實現這種狀況,在後面詳細介紹)

②.消極併發(悲觀併發、悲觀鎖):不管什麼時候從數據庫請求數據,數據都會被讀取,而後該數據上就會加鎖,所以沒有人能訪問該數據。這會下降併發出現問題的機會,缺點是加鎖是一個昂貴的操做,會下降整個應用程序的性能。

 消極併發的本質就是永遠不讓衝突發生,一般的處理凡是是隻讀鎖和更新鎖。

a. 當把只讀鎖放到記錄上時,應用程序只能讀取該記錄。若是應用程序要更新該記錄,它必須獲取到該記錄上的更新鎖。若是記錄上加了只讀鎖,那麼該記錄仍然可以被想要只讀鎖的請求使用。然而,若是須要更新鎖,該請求必須等到全部的只讀鎖釋放。一樣,若是記錄上加了更新鎖,那麼其餘的請求不能再在這個記錄上加鎖,該請求必須等到已存在的更新鎖釋放才能加鎖。

總結,這裏咱們能夠簡單理解把併發業務部分用一個鎖(如:lock,實質是數據庫鎖,後面章節單獨介紹)鎖住,使其同時只容許一個線程訪問便可。

b. 加鎖會帶來不少弊端:

 (1):應用程序必須管理每一個操做正在獲取的全部鎖;

 (2):加鎖機制的內存需求會下降應用性能

 (3):多個請求互相等待須要的鎖,會增長死鎖的可能性。

總結:儘可能不要使用消極併發,EF默認是不支持消極併發的!!!

注意:EF默認就是積極併發,固然EF也能夠配置成消極併發。

二. 並機制的解決方案

1. 從架構的角度去解決(大層次 如:12306買票)

  nginx負載均衡、數據庫讀寫分離、多個業務服務器、多個數據庫服務器、NoSQL, 使用隊列來處理業務,將高併發的業務依次放到隊列中,而後按照先進先出的原則, 逐個處理(隊列的處理能夠採用 Redis、RabbitMq等等)

  (PS:在後面的框架篇章裏詳細介紹該方案)

2. 從代碼的角度去解決(在服務器能承載壓力的狀況下,併發訪問同一條數據)

  實際的業務場景:如進銷存類的項目,涉及到同一個物品的出庫、入庫、庫存,咱們都知道庫存在數據庫裏對應了一條記錄,入庫要查出如今庫存的數量,而後加上入庫的數量,假設兩個線程同時入庫,假設查詢出來的庫存數量相同,可是更新庫存數量在數據庫層次上是有前後,最終就保留了後更新的數據,顯然是不正確的,應該保留的是兩次入庫的數量和。

(該案例的實質:多個線程同時在一條相同的數據上執行多個數據庫操做)

事先準備一張數據庫表:

解決方案一:(最經常使用的方式)

  給入庫和出庫操做加一個鎖,使其同時只容許一個線程訪問,這樣即便兩個線程同時訪問,但在代碼層數上,因爲鎖的緣由,仍是有先有後的,這樣就保證了入庫操做的線程惟一性,固然庫存量就不會出錯了.

總結:該方案能夠說是適合處理小範圍的併發且鎖內的業務執行不是很複雜。假設一萬線程同時入庫,每次入庫要等2s,那麼這一萬個線程執行完成須要的總時間很是多,顯然不適合。

    (這種方式的實質就是給核心業務加了個lock鎖,這裏就不作測試了)

 

解決方案二:EF處理積極併發帶來的衝突

1. 配置準備

  (1). 針對DBFirst模式,能夠給相應的表額外加一列RowVersion,byte類型,而且在Edmx模型上給該字段的併發模式設置爲fixed(默認爲None),這樣該表中全部字段都監控併發。

若是不想監視全部列(在不添加RowVersion的狀況下),只需在Edmx模型是給特定的字段的併發模式設置爲fixed,這樣只有被設置的字段被監測併發。

  測試結果: (DBFirst模式下的併發測試)

  事先在UserInfor1表中插入一條id、userName、userSex、userAge均爲1的數據(清空數據)。

測試狀況1:

  在不設置RowVersion併發模式爲Fixed的狀況下,兩個線程修改不一樣字段(修改同一個字段一個道理),後執行的線程的結果覆蓋前面的線程結果.

  發現測試結果爲:1,1,男,1 ; 顯然db1線程修改的結果被db2線程給覆蓋了. (修改同一個字段一個道理)

複製代碼
1             {
 2                 //1.建立兩個EF上下文,模擬表明兩個線程
 3                 var db1 = new ConcurrentTestDBEntities();
 4                 var db2 = new ConcurrentTestDBEntities();
 5 
 6                 UserInfor1 user1 = db1.UserInfor1.Find("1");
 7                 UserInfor1 user2 = db2.UserInfor1.Find("1");
 8 
 9                 //2. 執行修改操做
10                 //(db1的線程先執行完修改操做,並保存)
11                 user1.userName = "ypf";
12                 db1.Entry(user1).State = EntityState.Modified;
13                 db1.SaveChanges();
14 
15                 //(db2的線程在db1線程修改完成後,執行修改操做)
16                 try
17                 {
18                     user2.userSex = "男";
19                     db2.Entry(user2).State = EntityState.Modified;
20                     db2.SaveChanges();
21 
22                     Console.WriteLine("測試成功");
23                 }
24                 catch (Exception)
25                 {
26                     Console.WriteLine("測試失敗");
27                 }
28             }
複製代碼

測試狀況2:

  設置RowVersion併發模式爲Fixed的狀況下,兩個線程修改不一樣字段(修改同一個字段一個道理),若是該條數據已經被修改,利用DbUpdateConcurrencyException能夠捕獲異常,進行積極併發的衝突處理。測試結果以下:

  a.RefreshMode.ClientWins: 1,1,男,1

  b.RefreshMode.StoreWins: 1,ypf,1,1

  c.ex.Entries.Single().Reload(); 1,ypf,1,1

複製代碼
1             {
 2                 //1.建立兩個EF上下文,模擬表明兩個線程
 3                 var db1 = new ConcurrentTestDBEntities();
 4                 var db2 = new ConcurrentTestDBEntities();
 5 
 6                 UserInfor1 user1 = db1.UserInfor1.Find("1");
 7                 UserInfor1 user2 = db2.UserInfor1.Find("1");
 8 
 9                 //2. 執行修改操做
10                 //(db1的線程先執行完修改操做,並保存)
11                 user1.userName = "ypf";
12                 db1.Entry(user1).State = EntityState.Modified;
13                 db1.SaveChanges();
14 
15                 //(db2的線程在db1線程修改完成後,執行修改操做)
16                 try
17                 {
18                     user2.userSex = "男";
19                     db2.Entry(user2).State = EntityState.Modified;
20                     db2.SaveChanges();
21 
22                     Console.WriteLine("測試成功");
23                 }
24                 catch (DbUpdateConcurrencyException ex)
25                 {
26                     Console.WriteLine("測試失敗:" + ex.Message);
27 
28                     //1. 保留上下文中的現有數據(即最新,最後一次輸入)
29                     //var oc = ((IObjectContextAdapter)db2).ObjectContext;
30                     //oc.Refresh(RefreshMode.ClientWins, user2);
31                     //oc.SaveChanges();
32 
33                     //2. 保留原始數據(即數據源中的數據代替當前上下文中的數據)
34                     //var oc = ((IObjectContextAdapter)db2).ObjectContext;
35                     //oc.Refresh(RefreshMode.StoreWins, user2);
36                     //oc.SaveChanges();
37 
38                     //3. 保留原始數據(而Reload處理也就是StoreWins,意味着放棄當前內存中的實體,從新到數據庫中加載當前實體)
39                     ex.Entries.Single().Reload();
40                     db2.SaveChanges();
41                 }
42             }
複製代碼

測試狀況3:

  在不設置RowVersion併發模式爲Fixed的狀況下(也不須要RowVersion這個字段),單獨設置userName字段的併發模式爲Fixed,兩個線程同時修改該字段,利用DbUpdateConcurrencyException能夠捕獲異常,進行積極併發的衝突處理,但若是是兩個線程同時修改userName之外的字段,將不能捕獲異常,將走EF默認的處理方式,後執行的覆蓋先執行的。

  a.RefreshMode.ClientWins: 1,ypf2,1,1

  b.RefreshMode.StoreWins: 1,ypf,1,1

  c.ex.Entries.Single().Reload(); 1,ypf,1,1

複製代碼
1             {
 2                 //1.建立兩個EF上下文,模擬表明兩個線程
 3                 var db1 = new ConcurrentTestDBEntities();
 4                 var db2 = new ConcurrentTestDBEntities();
 5 
 6                 UserInfor1 user1 = db1.UserInfor1.Find("1");
 7                 UserInfor1 user2 = db2.UserInfor1.Find("1");
 8 
 9                 //2. 執行修改操做
10                 //(db1的線程先執行完修改操做,並保存)
11                 user1.userName = "ypf";
12                 db1.Entry(user1).State = EntityState.Modified;
13                 db1.SaveChanges();
14 
15                 //(db2的線程在db1線程修改完成後,執行修改操做)
16                 try
17                 {
18                     user2.userName = "ypf2";
19                     db2.Entry(user2).State = EntityState.Modified;
20                     db2.SaveChanges();
21 
22                     Console.WriteLine("測試成功");
23                 }
24                 catch (DbUpdateConcurrencyException ex)
25                 {
26                     Console.WriteLine("測試失敗:" + ex.Message);
27 
28                     //1. 保留上下文中的現有數據(即最新,最後一次輸入)
29                     var oc = ((IObjectContextAdapter)db2).ObjectContext;
30                     oc.Refresh(RefreshMode.ClientWins, user2);
31                     oc.SaveChanges();
32 
33                     //2. 保留原始數據(即數據源中的數據代替當前上下文中的數據)
34                     //var oc = ((IObjectContextAdapter)db2).ObjectContext;
35                     //oc.Refresh(RefreshMode.StoreWins, user2);
36                     //oc.SaveChanges();
37 
38                     //3. 保留原始數據(而Reload處理也就是StoreWins,意味着放棄當前內存中的實體,從新到數據庫中加載當前實體)
39                     //ex.Entries.Single().Reload();
40                     //db2.SaveChanges();
41                 }
42             }
複製代碼

  (2). 針對CodeFirst模式,須要有這樣的一個屬性 public byte[] RowVersion { get; set; },而且給屬性加上特性[Timestamp],這樣該表中全部字段都監控併發。若是不想監視全部列(在不添加RowVersion的狀況下),只需給特定的字段加上特性 [ConcurrencyCheck],這樣只有被設置的字段被監測併發。

  除了再配置上不一樣於DBFirst模式覺得,是經過加特性的方式來標記併發,其它捕獲併發和積極併發的幾類處理方式均同DBFirst模式相同。(這裏不作測試了)

2. 積極併發處理的三種形式總結:

  利用DbUpdateConcurrencyException能夠捕獲異常,而後:

    a. RefreshMode.ClientWins:保留上下文中的現有數據(即最新,最後一次輸入)

    b. RefreshMode.StoreWins:保留原始數據(即數據源中的數據代替當前上下文中的數據)

    c.ex.Entries.Single().Reload(); 保留原始數據(而Reload處理也就是StoreWins,意味着放棄當前內存中的實體,從新到數據庫中加載當前實體)

3. 該方案總結:

  這種模式實質上就是獲取異常告訴程序,讓開發人員結合需求本身選擇怎麼處理,但這種模式是解決代碼層次上的併發衝突,並非解決大數量同時訪問崩潰問題的。

解決方案三:利用隊列來解決業務上的併發(架構層次上其實也是這種思路解決的)

1.先分析:

  前面說過所謂的高併發,就是海量的用戶同時向服務器發送請求,進行某個業務處理(好比定時秒殺的搶單),而這個業務處理是須要 必定時間的。

2.處理思路:

  將海量用戶的請求放到一個隊列裏(如:Queue),先不進行業務處理,而後另一個服務器從線程中讀取這個請求(MVC框架能夠放到Gloabl全局裏),依次進行業務裏,至於處理完成後,是否須要告訴客戶端,能夠根據實際需求來定,若是須要的話(能夠藉助Socket、Signlar、推送等技術來進行).

  特別注意:讀取隊列的線程是一直在運行,只要隊列中有數據,就給他拿出來.

  這裏使用Queue隊列,能夠參考:http://www.cnblogs.com/yaopengfei/p/8322016.html

  (PS:架構層次上的處理方案無非,隊列是單獨一臺服務器,執行從隊列讀取的是另一臺業務服務器,處理思想是相同的)

隊列單例類的代碼:

複製代碼
1  /// <summary>
 2     /// 單例類
 3     /// </summary>
 4     public class QueueUtils
 5     {
 6         /// <summary>
 7         /// 靜態變量:由CLR保證,在程序第一次使用該類以前被調用,並且只調用一次
 8         /// </summary>
 9         private static readonly QueueUtils _QueueUtils = new QueueUtils();
10 
11         /// <summary>
12         /// 聲明爲private類型的構造函數,禁止外部實例化
13         /// </summary>
14         private QueueUtils()
15         {
16 
17         }
18         /// <summary>
19         /// 聲明屬性,供外部調用,此處也能夠聲明成方法
20         /// </summary>
21         public static QueueUtils instanse
22         {
23             get
24             {
25                 return _QueueUtils;
26             }
27         }
28 
29 
30         //下面是隊列相關的
31          System.Collections.Queue queue = new System.Collections.Queue();
32 
33         private static object o = new object();
34 
35         public int getCount()
36         {
37             return queue.Count;
38         }
39 
40         /// <summary>
41         /// 入隊方法
42         /// </summary>
43         /// <param name="myObject"></param>
44         public void Enqueue(object myObject)
45         {
46             lock (o)
47             {
48                 queue.Enqueue(myObject);
49             }
50         }
51         /// <summary>
52         /// 出隊操做
53         /// </summary>
54         /// <returns></returns>
55         public object Dequeue()
56         {
57             lock (o)
58             {
59                 if (queue.Count > 0)
60                 {
61                     return queue.Dequeue();
62                 }
63             }
64             return null;
65         }
66 
67     }
複製代碼

臨時存儲數據類的代碼:

複製代碼
1     /// <summary>
 2     /// 該類用來存儲請求信息
 3     /// </summary>
 4     public class TempInfor
 5     {
 6         /// <summary>
 7         /// 用戶編號
 8         /// </summary>
 9         public string userId { get; set; }
10     }
複製代碼

模擬高併發入隊,單獨線程出隊的代碼:

複製代碼
1  {
 2                 //3.1 模擬高併發請求 寫入隊列
 3                 {
 4                     for (int i = 0; i < 100; i++)
 5                     {
 6                         Task.Run(() =>
 7                         {
 8                             TempInfor tempInfor = new TempInfor();
 9                             tempInfor.userId = Guid.NewGuid().ToString("N");
10                             //下面進行入隊操做
11                             QueueUtils.instanse.Enqueue(tempInfor);
12 
13                         });
14                     }
15                 }        
16                 //3.2 模擬另一個線程隊列中讀取數據請求標記,進行相應的業務處理(該線程一直運行,不中止)
17                 Task.Run(() =>
18                 {
19                     while (true)
20                     {
21                         if (QueueUtils.instanse.getCount() > 0)
22                         {
23                             //下面進行出隊操做
24                             TempInfor tempInfor2 = (TempInfor)QueueUtils.instanse.Dequeue();
25 
26                             //拿到請求標記,進行相應的業務處理
27                             Console.WriteLine("id={0}的業務執行成功", tempInfor2.userId);
28                         }
29                     }           
30                 });
31                 //3.3 模擬過了一段時間(6s後),又有新的請求寫入
32                 Thread.Sleep(6000);
33                 Console.WriteLine("6s的時間已通過去了");
34                 {
35                     for (int j = 0; j < 100; j++)
36                     {
37                         Task.Run(() =>
38                         {
39                             TempInfor tempInfor = new TempInfor();
40                             tempInfor.userId = Guid.NewGuid().ToString("N");
41                             //下面進行入隊操做
42                             QueueUtils.instanse.Enqueue(tempInfor);
43 
44                         });
45                     }
46                 }
47             }
複製代碼

3.下面案例的測試結果:

  一次輸出100條數據,6s事後,再一次輸出100條數據。

4. 總結:

  該方案是一種迂迴的方式處理高併發,在業內這種思想也是很是常見,但該方案也有一個弊端,客戶端請求的實時性很難保證,或者即便要保證(好比引入實時通信技術),

 也要付出很多代價.

解決方案四:本身擴展EF只修改指定字段(處理同時修改同一條記錄的不一樣字段問題)

分析這裏有兩個思路,分別是:

   ①:把不修改的屬性排除掉。 ②:只標記要修改的屬性。

 顯然這兩種思路是解決不了同一個字段上的併發的,那麼同一條記錄上不一樣字段的併發可否解決呢(即合併處理)?下面接着分析:

   (在Expression表達式目錄樹章節進行處理)

解決方案五: 利用數據庫自有的鎖機制進行處理

   (在後面數據鎖機制章節進行介紹)

 

框架搭建篇

 

1. 基礎框架搭建

  方案一: MVC5+EF6+Spring.Net+T4 (DBFirst 模式)

  方案二: MVC6+EF6+Unity (CodeFirst模式)

  結合主要的設計模式+AOP思想

2. 日誌系統和異常處理

  方案一: Log4Net

  方案二:ELK

3.  整合定時任務框架

  Quartz.Net

4. 整合實時通信框架

  SignalR

5. 權限系統的構建

  按鈕級別的權限設計,從需求分析→數據庫設計→代碼實現

6.  整合EasyUi框架

  常見控件的使用

        分析目前公司內部正在使用的皮膚樣式

7. 經常使用類的封裝,便於快捷開發

      基礎封裝類

      大數據量處理的封裝類

8. 緩存的引入,優化系統性能,爲後面分佈式框架的打造作準備

 服務器端緩存

    Memecached

9.  完善框架的健壯性

 導出功能(Excel的導出、word導出、pdf導出)

    打印功能

10. 框架結構升級

 引入Redis 或 RabbitMQ,打造分佈式隊列

    Nagix負載均衡

11. 框架延伸

 .Net Core2.0

     Linux基礎

 

  

複製代碼
/// <summary>
    /// 經過線程對數據上下文進行一個優化
    /// </summary>
   public class DBContextFactory: IDBContextFactory
    {
       public DbContext GetDbContext()
       {
           DbContext dbContext = CallContext.GetData(typeof(DBContextFactory).Name) as DbContext;
           if (dbContext == null)
           {
               dbContext = new MagicDBEntities();
               CallContext.SetData(typeof(DBContextFactory).Name,dbContext);
           }

           return dbContext;
       }
    }
複製代碼

 

 

第二十一節:ADO層次上的海量數據處理方案(SqlBulkCopy類插入和更新)

 

一. 簡介

1. 背景:

  雖然前面EF的擴展插件Z.EntityFramework.Extensions,性能很快,並且也很方便,可是該插件要收費,使用免費版本的話,須要按期更新,若是不更新,將失效,很是麻煩,這個時候SqlBulkCopy類既免費又高效,顯得很是合適了。

2. 使用步驟:

   ①. 引入命名空間:using System.Data.SqlClient;

   ②. 使用DataTable構造與目標數據庫表結構相同的字段,並給其賦值。

   ③. 使用SqlBulkCopy類,將內存表中的數據一次性插入到目標表中。(看下面的封裝可知,能夠自行設置插入塊的大小)

  補充:調用下面的封裝這種形式必須內存表中的字段名和數據庫表中的字段名一一對應。

二. 使用方式及其性能測試

 1.  涉及到的數據庫結構:

 

2. 數據庫鏈接字符串 

  <add name="CodeFirstModel2" connectionString="data source=localhost;initial catalog=EFDB2;persist security info=True;user id=sa;password=123456;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" />

3. 代碼實踐

  有兩種插入方式,一種是循序漸進的每一個字段 內存表和數據表進行映射,這個狀況無須名稱一致,只要映射正確便可。另一種方式是,直接調用下面的封裝方法便可,這種要求內存表中字段和數據庫表中的字段名稱必須徹底一致,一一對應,這樣就省去了方法一 中一一匹配映射的繁瑣步驟了。

複製代碼
1    public class SqlBulkCopyTest
 2     {
 3         public static void TestSqlBulkCopy()
 4         {
 5             //一. 構造DataTable結構而且給其賦值
 6             //1.定義與目標表的結構一致的內存表 排除自增id列  
 7             DataTable dtSource = new DataTable();
 8             //列名稱若是和目標表設置爲同樣,則後面能夠不用進行字段映射  
 9             dtSource.Columns.Add("id");
10             dtSource.Columns.Add("t21");
11             dtSource.Columns.Add("t22");
12             //2. 向dt中增長4W條測試數據  
13             DataRow dr;
14             for (int i = 0; i <40000; i++)
15             {
16                 // 建立與dt結構相同的DataRow對象  
17                 dr = dtSource.NewRow();
18                 dr["id"] = Guid.NewGuid().ToString("N");
19                 dr["t21"] = "Name" + i;
20                 dr["t22"] = "Address" + i;
21                 // 將dr追加到dt中  
22                 dtSource.Rows.Add(dr);
23             }
24             //二.將內存表dt中的4W條數據一次性插入到t_Data表中的相應列中 
25             System.Diagnostics.Stopwatch st = new System.Diagnostics.Stopwatch();
26             st.Start();
27             string connStr = ConfigurationManager.ConnectionStrings["CodeFirstModel2"].ToString();
28 
29             #region 01-循序漸進一一對應
30             //{
31             //    using (SqlBulkCopy copy = new SqlBulkCopy(connStr))
32             //    {
33             //        //1 指定數據插入目標表名稱  
34             //        copy.DestinationTableName = "TestTwo";
35 
36             //        //2 告訴SqlBulkCopy對象 內存表中的 字段和目標表中的字段 對應起來(這裏有多個重載,也能夠用索引對應)
37             //        //前面是內存表,後面是目標表即數據庫中的字段
38             //        copy.ColumnMappings.Add("id", "id");
39             //        copy.ColumnMappings.Add("t21", "t21");
40             //        copy.ColumnMappings.Add("t22", "t22");
41 
42             //        //3 將內存表dt中的數據一次性批量插入到目標表中  
43             //        copy.WriteToServer(dtSource);
44             //    }
45             //}
46 
47             #endregion
48 
49             #region 02-調用封裝
50             {
51                 AddByBluckCopy(connStr, dtSource, "TestTwo");
52             }
53             #endregion
54             
55             st.Stop();
56             Console.WriteLine("數據插入成功,總耗時爲:" + st.ElapsedMilliseconds + "毫秒");
57 
58         }
59 
60         /// <summary>
61         /// 海量數據插入方法
62         /// (調用該方法須要注意,DataTable中的字段名稱必須和數據庫中的字段名稱一一對應)
64         /// </summary>
65         /// <param name="connectstring">數據鏈接字符串</param>
66         /// <param name="table">內存表數據</param>
67         /// <param name="tableName">目標數據的名稱</param>
68         public static void AddByBluckCopy(string connectstring,DataTable table, string tableName)
69         {
70             if (table != null && table.Rows.Count > 0)
71             {
72                 using (SqlBulkCopy bulk = new SqlBulkCopy(connectstring))
73                 {
74                     bulk.BatchSize = 1000;
75                     bulk.BulkCopyTimeout = 100;
76                     bulk.DestinationTableName = tableName;
77                     bulk.WriteToServer(table);
78                 }
79             }
80         }
81     }
複製代碼

4. 性能測試的結論

  根據上述的數據測試能夠看出來,SqlBulkCopy在處理大數據量插入上速度很是快,甚至比付費的插件Z.EntityFramework.Extensions都要快,因此值得參考和推薦。

 

  既然SqlBulkCopy解決大數據量的插入問題,那麼刪除和更新怎麼辦呢? 詳見  第二十三節: EF性能篇(三)之基於開源組件 Z.EntityFrameWork.Plus.EF6解決EF性能問題

 

先記下地址,後面補充一下
https://github.com/MikaelEliasson/EntityFramework.Utilities

 

第十四節: 介紹四大併發集合類並結合單例模式下的隊列來講明線程安全和非安全的場景及補充性能調優問題。

 

一. 四大併發集合類

背景:咱們目前使用的全部集合都是線程不安全的 。

  A. ConcurrentBag:就是利用線程槽來分攤Bag中的全部數據,鏈表的頭插法,0表明移除最後一個插入的值.

  (等價於同步中的List)

  B. ConcurrentStack:線程安全的Stack是使用Interlocked來實現線程安全, 而沒有使用內核鎖.

  (等價於同步中的數組)

  C. ConcurrentQueue: 隊列的模式,先進先出

  (等價於同步中的隊列)

  D. ConcurrentDictionary: 字典的模式

  (等價於同步中的字典)

以上四種安全的併發集合類,也能夠採用同步版本+Lock鎖(或其它鎖)來實現

 代碼實踐:

複製代碼
01-ConcurrentBag
            {
                Console.WriteLine("---------------- 01-ConcurrentBag ---------------------");
                ConcurrentBag<int> bag = new ConcurrentBag<int>();
                bag.Add(1);
                bag.Add(2);
                bag.Add(33);
                //鏈表的頭插法,0表明移除最後一個插入的值
                var result = 0;
                //flag爲true,表示移除成功,而且返回被移除的值
                var flag = bag.TryTake(out result);
                Console.WriteLine("移除的值爲:{0}", result);

            }
            #endregion

             02-ConcurrentStack
            {
                Console.WriteLine("---------------- 02-ConcurrentStack ---------------------");
                ConcurrentStack<int> stack = new ConcurrentStack<int>();
                stack.Push(1);
                stack.Push(2);
                stack.Push(33);
                //鏈表的頭插法,0表明移除最後一個插入的值
                var result = 0;
                //flag爲true,表示移除成功,而且返回被移除的值
                var flag = stack.TryPop(out result);

                Console.WriteLine("移除的值爲:{0}", result);
            }
            #endregion

            03-ConcurrentQueue
            {
                Console.WriteLine("---------------- 03-ConcurrentQueue ---------------------");
                ConcurrentQueue<int> queue = new ConcurrentQueue<int>();
                queue.Enqueue(1);
                queue.Enqueue(2);
                queue.Enqueue(33);
                //隊列的模式,先進先出,0表明第一個插入的值
                var result = 0;
                //flag爲true,表示移除成功,而且返回被移除的值
                var flag = queue.TryDequeue(out result);

                Console.WriteLine("移除的值爲:{0}", result);
            }
            #endregion

             04-ConcurrentDictionary
            {
                Console.WriteLine("---------------- 04-ConcurrentDictionary ---------------------");
                ConcurrentDictionary<int, int> dic = new ConcurrentDictionary<int, int>();
                dic.TryAdd(1, 10);
                dic.TryAdd(2, 11);
                dic.TryAdd(3, 12);
                dic.ContainsKey(3);
                //下面是輸出字典中的全部值
                foreach (var item in dic)
                {
                    Console.WriteLine(item.Key + item.Value);
                }
            }
            #endregion
複製代碼

代碼結果:

 

二. 隊列的綜合案例

   上面介紹了四大安全線程集合類和與其對應的不安全的線程集合類,可能你會比較疑惑,到底怎麼安全了,那些不安全的集合類怎麼能變成安全呢,下面以隊列爲例,來解決這些疑惑。

  1. 測試Queue隊列併發狀況下是不安全的(存在資源競用的問題),ConcurrentQueue隊列在併發狀況下是安全的。

  2. 利用Lock鎖+Queue隊列,實現多線程併發狀況下的安全問題,即等同於ConcurrentQueue隊列的效果。

    經典案例測試:開啓100個線程進行入隊操做,正常全部的線程執行結束後,隊列中的個數應該爲100.

    ①. Queue不加鎖的狀況:結果出現9九、9八、100,顯然是出問題了。

複製代碼
{
                Queue queue = new Queue();
                object o = new object();
                int count = 0;
                List<Task> taskList = new List<Task>();
                for (int i = 0; i < 100; i++)
                {
                    var task = Task.Run(() =>
                      {
                          queue.Enqueue(count++);
                      });
                    taskList.Add(task);
                }

                Task.WaitAll(taskList.ToArray());
                //發現隊列個數在不加鎖的狀況下 居然不一樣 有100,有99
                Console.WriteLine("Queue不加鎖的狀況隊列個數" + queue.Count);
            }
複製代碼

    ②. Queue加鎖的狀況:結果全是100,顯然是正確的。

複製代碼
1             {
 2                 Queue queue = new Queue();
 3                 object o = new object();
 4                 int count = 0;
 5                 List<Task> taskList = new List<Task>();
 6                 for (int i = 0; i < 100; i++)
 7                 {
 8                     var task = Task.Run(() =>
 9                     {
10                         lock (o)
11                         {
12                             queue.Enqueue(count++);
13                         }
14                     });
15                     taskList.Add(task);
16                 }
17 
18                 Task.WaitAll(taskList.ToArray());
19                 //發現隊列個數在全是100
20                 Console.WriteLine("Queue加鎖的狀況隊列個數" + queue.Count);
21             }
複製代碼

    ③. ConcurrentQueue不加鎖的狀況:結果全是100,顯然是正確,同時證實ConcurrentQueue隊列自己就是線程安全的。

複製代碼
1             {
 2                 ConcurrentQueue<int> queue = new ConcurrentQueue<int>();
 3                 object o = new object();
 4                 int count = 0;
 5                 List<Task> taskList = new List<Task>();
 6 
 7                 for (int i = 0; i < 100; i++)
 8                 {
 9                     var task = Task.Run(() =>
10                     {
11                         queue.Enqueue(count++);
12                     });
13                     taskList.Add(task);
14                 }
15                 Task.WaitAll(taskList.ToArray());
16                 //發現隊列個數不加鎖的情形=也全是100,證實ConcurrentQueue是線程安全的
17                 Console.WriteLine("ConcurrentQueue不加鎖的狀況下隊列個數" + queue.Count);
18             }
複製代碼

  3. 在實際項目中,若是使用隊列來實現一個業務,該隊列須要是全局的,這個時候就須要使用單例(ps:單例是不容許被實例化的,能夠經過單例類中的屬性或者方法的形式來獲取這個類),同時,隊列的入隊和出隊操做,若是使用Queue隊列,須要配合lock鎖,來解決多線程下資源的競用問題。

  經典案例:開啓100個線程對其進行入隊操做,而後主線程輸入隊列的個數,而且將隊列中的內容輸出.

  結果:隊列的個數爲100,輸出內容1-100依次輸出。

複製代碼
1 /// <summary>
 2     /// 單例類
 3     /// </summary>
 4     public class QueueUtils
 5     {
 6         /// <summary>
 7         /// 靜態變量:由CLR保證,在程序第一次使用該類以前被調用,並且只調用一次
 8         /// </summary>
 9         private static readonly QueueUtils _QueueUtils = new QueueUtils();
10 
11         /// <summary>
12         /// 聲明爲private類型的構造函數,禁止外部實例化
13         /// </summary>
14         private QueueUtils()
15         {
16 
17         }
18         /// <summary>
19         /// 聲明屬性,供外部調用,此處也能夠聲明成方法
20         /// </summary>
21         public static QueueUtils instanse
22         {
23             get
24             {
25                 return _QueueUtils;
26             }
27         }
28 
29 
30         //下面是隊列相關的
31         Queue queue = new Queue();
32 
33         private static object o = new object();
34 
35         public  int getCount()
36         {
37             return queue.Count;
38         }
39 
40         /// <summary>
41         /// 入隊方法
42         /// </summary>
43         /// <param name="myObject"></param>
44         public void Enqueue(object myObject)
45         {
46             lock (o)
47             {
48                 queue.Enqueue(myObject);
49             }
50         }
51         /// <summary>
52         /// 出隊操做
53         /// </summary>
54         /// <returns></returns>
55         public object Dequeue()
56         {
57             lock (o)
58             {
59                 if (queue.Count > 0)
60                 {
61                     return queue.Dequeue();
62                 }
63             }
64             return null;
65         }
66 
67     }
複製代碼
複製代碼
1   {
 2                 int count = 1;
 3                 List<Task> taskList = new List<Task>();
 4                 for (int i = 0; i < 100; i++)
 5                 {
 6                     var task = Task.Run(() =>
 7                     {
 8                         QueueUtils.instanse.Enqueue(count++);
 9                     });
10                     taskList.Add(task);
11                 }
12 
13                 Task.WaitAll(taskList.ToArray());
14                 //發現隊列個數在全是100
15                 Console.WriteLine("單例模式下隊列個數" + QueueUtils.instanse.getCount());
16 
17                 //下面是出隊相關的業務
18                 while (QueueUtils.instanse.getCount() > 0)
19                 {
20                     Console.WriteLine("出隊:" + QueueUtils.instanse.Dequeue());
21                 }
22             }
複製代碼

。。。。。。。。。。。

三. 常見的幾類性能調優

PS: 

1. 常見的一級事件:CPU佔用太高、死鎖問題、內存爆滿
  a. CPU太高:查看是否while(true)中的業務過於複雜,致使cpu一直在高負荷運行。
  b. 死鎖問題:亂用lock,千萬不要lock中再加lock,多個lock重疊
  c. 內存爆滿:字符串的無限增加,全局的靜態變量過多。
2. 補充幾個經常使用的性能調優的方式
  a. 使用字典類型Dictionary<T,T>,代替只有兩個屬性的對象或匿名對象。
  b. 使用數組代替只有兩個屬性的對象或匿名對象。
  好比:
    index:存放id
    value:存放數量或其餘屬性
3. 返璞歸真,使用最原始的代碼代替簡潔漂亮的代碼。
4. 合理的使用多線程,業務複雜的儘量的併發執行(或者異步)。
5. 運用設計模式,使代碼簡潔、易於擴展。

 

 

第十三節:實際開發中使用最多的監視鎖Monitor、lock語法糖的擴展、混合鎖的使用(ManualResetEvent、SemaphoreSlim、ReaderWriterLockSlim)

 

一. 監視鎖(Monitor和lock)

1. Monitor類,限定線程個數的一把鎖,兩個核心方法:

  Enter:鎖住某個資源。

  Exit:退出某一個資源。

測試案例:開啓5個線程同時對一個變量進行自增操做,結果變量有序的輸出,說明該鎖同時只容許一個線程訪問。

可是寫法很麻煩,每次都要try-catch-finally,還要聲明bool變量。這個時候lock語法糖就很好的解決了這個問題。

 代碼實踐:

複製代碼
1 static object lockMe = new object();
 2         {
 3                 for (int i = 0; i < 5; i++)
 4                 {
 5                     Task.Factory.StartNew(() =>
 6                     {
 7                         for (int j = 0; j < 100; j++)
 8                         {
 9                             var b = false;
10                             try
11                             {
12                                 Monitor.Enter(lockMe, ref b);
13                                 Console.WriteLine(num++);
14                             }
15                             catch (Exception)
16                             {
17 
18                                 throw;
19                             }
20                             finally
21                             {
22                                 if (b)
23                                 {
24                                     Monitor.Exit(lockMe);
25                                 }
26                             }
27 
28                         }
29 
30                     });
31                 }
32             }
複製代碼

2. lock語法糖

  使用很簡單,聲明一個靜態的object類型變量,調用lock語法糖,將共享變量放入其中,便可保證lock內同時只能一個線程訪問。

 代碼實踐:

複製代碼
1             {
 2                 for (int i = 0; i < 5; i++)
 3                 {
 4                     Task.Factory.StartNew(() =>
 5                     {
 6                         for (int j = 0; j < 100; j++)
 7                         {
 8                             lock (lockMe)
 9                             {
10                                 Console.WriteLine(num++);
11                             }
12                         }
13                     });
14                 }
15             }
複製代碼

 

二. 混合鎖

1. 簡介:混合鎖=用戶模式鎖+內核模式鎖,先在用戶模式下內旋,若是超過必定的閾值,會切換到內核鎖,在內旋模式下,咱們會看到大量的Sleep(0),Sleep(1),Yield等語法。

  Thread.Sleep(1) 讓線程休眠1ms

  Thread.Sleep(0) 讓線程放棄當前的時間片,讓本線程更高或者同等線程獲得時間片運行。

  Thread.Yield() 讓線程當即放棄當前的時間片,可讓更低級別的線程獲得運行,當其餘thread時間片用完,本thread再度喚醒。

 混合鎖包括如下三種:ManualResetEventSlim、SemaphoreSlim、ReaderWriterLockSlim,這三種混合鎖,要比他們對應的內核模式鎖 (ManualResetEvent、Semaphore、ReaderWriterLock),的性能高的多。

2. ManualResetEventSlim

  構造函數默認爲false,可使用Wait方法替代WaitOne方法,支持任務取消. (詳細的代碼同內核版本相似,這裏不作測試了)

3. SemaphoreSlim

  用法和內核版本相似,使用Wait方法代替WaitOne方法,Release方法不變。(詳細的代碼同內核版本相似,這裏不作測試了)

4. ReaderWriterLockSlim

  用法和內核版本相似,可是四個核心方法換成了:

  鎖讀的兩個核心方法:EnterReadLock、ExitReadLock。

  鎖寫的兩個核心方法:EnterWriteLock、ExitWriteLock。

  (詳細的代碼同內核版本相似,這裏不作測試了)

 

 

 

第十二節:深究內核模式鎖的使用場景(自動事件鎖、手動事件鎖、信號量、互斥鎖、讀寫鎖、動態鎖)

 

一. 總體介紹

舒適提示:內核模式鎖,在不到萬不得已的狀況下,不要使用它,由於代價太大了,有不少種替代方案。

  內核模式鎖包括:

    ①:事件鎖

    ②:信號量

    ③:互斥鎖

    ④:讀寫鎖

    ⑤:動態鎖

 

二. 事件鎖

 事件鎖包括:

A. 自動事件鎖(AutoResetEvent)

  使用場景:能夠用此鎖實現多線程環境下某個變量的自增.

  現實場景: 進站火車閘機,咱們用火車票來實現進站操做.

  true: 表示終止狀態,閘機中沒有火車票

  false: 表示費終止狀態,閘機中此時有一張火車票

B.手動事件鎖(ManualResetEvent)

  現實場景:有人看守的鐵道柵欄(和自動事件鎖不同,不能混用)

  true: 柵欄沒有合圍,沒有阻止行人經過鐵路

  false:柵欄合圍了, 阻止行人經過

* 下面案例發現,鎖不住,自增仍然是無序的輸出了.

* 核心方法:WaitOne和Set

 代碼實踐-自動事件鎖:

複製代碼
1  static AutoResetEvent autoResetLock1 = new AutoResetEvent(true);
 2  static AutoResetEvent autoResetLock2 = new AutoResetEvent(false);
 3  static int num2 = 0;
 4  {
 5                 //1. 能輸出
 6                 {
 7                     autoResetLock1.WaitOne();
 8                     Console.WriteLine("autoResetLock1檢驗經過,能夠通行");
 9                     autoResetLock1.Set();
10                 }
11 
12                 //2. 不能輸出
13                 {
14                     autoResetLock2.WaitOne();
15                     Console.WriteLine("autoResetLock2檢驗經過,能夠通行");
16                     autoResetLock2.Set();
17                 } 
18 
19                 //3.下面代碼的結果:num從0-249,有序的發現能夠鎖住。
20                 {
21                     for (int i = 0; i < 5; i++)
22                     {
23                         Task.Factory.StartNew(() =>
24                         {
25                             for (int j = 0; j < 50; j++)
26                             {
27                                 try
28                                 {
29                                     autoResetLock1.WaitOne();
30                                     Console.WriteLine(num2++);
31                                     autoResetLock1.Set();
32                                 }
33                                 catch (Exception ex)
34                                 {
35                                     Console.WriteLine(ex.Message);
36                                 }
37 
38                             }
39                         });
40                     }
41                 }
42             }
複製代碼

代碼實踐-手動事件鎖:

複製代碼
1          static int num2 = 0;
 2          static ManualResetEvent mreLock = new ManualResetEvent(true);
 3          //下面代碼鎖不住,仍然是無序的輸出了
 4                 {
 5                     for (int i = 0; i < 5; i++)
 6                     {
 7                         Task.Factory.StartNew(() =>
 8                         {
 9                             for (int j = 0; j < 50; j++)
10                             {
11                                 try
12                                 {
13                                     mreLock.WaitOne();
14                                     Console.WriteLine(num2++);
15                                     mreLock.Set();
16                                 }
17                                 catch (Exception ex)
18                                 {
19                                     Console.WriteLine(ex.Message);
20                                 }
21 
22                             }
23                         });
24                     }
25                 }
複製代碼

 

三. 信號量

信號量:

  * 核心類:Semaphore,經過int數值來控制線程個數。

  * 經過觀察構造函數 public Semaphore(int initialCount, int maximumCount);:

  * initialCount: 能夠同時授予的信號量的初始請求數。

  * maximumCount: 能夠同時授予的信號量的最大請求數。

  * static Semaphore seLock = new Semaphore(1, 1); //表示只容許一個線程經過

* 下面的案例能夠有序的輸出。

* 核心方法:WaitOne和Release

 代碼實踐:

複製代碼
1              static Semaphore seLock = new Semaphore(1, 1); //只容許一個線程經過 
2 //下面代碼鎖住了,能夠有序的輸出 3 { 4 for (int i = 0; i < 5; i++) 5 { 6 Task.Factory.StartNew(() => 7 { 8 for (int j = 0; j < 50; j++) 9 { 10 try 11 { 12 seLock.WaitOne(); 13 Console.WriteLine(num2++); 14 seLock.Release(); 15 } 16 catch (Exception ex) 17 { 18 Console.WriteLine(ex.Message); 19 } 20 21 } 22 }); 23 } 24 }
複製代碼

 

四. 互斥鎖

互斥鎖:

  核心方法:WaitOne和ReleaseMutex

  下面案例能夠鎖住,有序輸出

總結以上三種類型的鎖,都有一個WaitOne方法,觀察源碼可知,都繼承於WaitHandle類。

 代碼實踐:

複製代碼
1       static Mutex mutex = new Mutex();
 2         //下面代碼鎖住了,能夠有序的輸出
 3                 {
 4                     for (int i = 0; i < 5; i++)
 5                     {
 6                         Task.Factory.StartNew(() =>
 7                         {
 8                             for (int j = 0; j < 50; j++)
 9                             {
10                                 try
11                                 {
12                                     mutex.WaitOne();
13                                     Console.WriteLine(num2++);
14                                     mutex.ReleaseMutex();
15                                 }
16                                 catch (Exception ex)
17                                 {
18                                     Console.WriteLine(ex.Message);
19                                 }
20 
21                             }
22                         });
23                     }
24                 }
複製代碼

 

五. 讀寫鎖

  讀寫鎖(ReaderWriterLock):

  背景:多個線程讀,一個線程寫,若是寫入的時間過久,此時讀的線程會被卡死,這個時候就要用到讀寫鎖了。

  鎖讀的兩個核心方法:AcquireReaderLock和ReleaseReaderLock。

  鎖寫的兩個核心方法:AcquireWriterLock和ReleaseWriterLock。

 代碼實踐:

複製代碼
1        static ReaderWriterLock rwlock = new ReaderWriterLock();
 2         private void button24_Click(object sender, EventArgs e)
 3         {
 4             #region 01-讀寫鎖
 5             {
 6                 //開啓5個線程執行讀操做
 7                 for (int i = 0; i < 5; i++)
 8                 {
 9                     Task.Run(() =>
10                     {
11                         Read();
12                     });
13                 }
14                 //開啓1個線程執行寫操做
15                 Task.Factory.StartNew(() =>
16                 {
17                     Write();
18                 });
19             }
20             #endregion
21 
22         }
23         /// <summary>
24         /// 線程讀
25         /// </summary>
26         static void Read()
27         {
28             while (true)
29             {
30                 Thread.Sleep(10);
31                 rwlock.AcquireReaderLock(int.MaxValue);
32                 Console.WriteLine("當前 t={0} 進行讀取 {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now);
33                 rwlock.ReleaseReaderLock();
34             }
35         }
36         /// <summary>
37         /// 線程寫
38         /// </summary>
39         static void Write()
40         {
41             while (true)
42             {
43                 Thread.Sleep(300);
44                 rwlock.AcquireWriterLock(int.MaxValue);
45                 Console.WriteLine("當前 t={0} 進行寫入 {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now);
46                 rwlock.ReleaseWriterLock();
47             }
48         }
複製代碼

 

六. 動態鎖

動態鎖(CountdownEvent):

  * 做用:限制線程數的一個機制。

  * 業務場景:有Orders、Products、Users表,咱們須要多個線程從某一張表中讀取數據。

  * 好比:Order表10w,10個線程讀取。(每一個線程讀1w)

       Product表5w,5個線程讀取。(每一個線程讀1w)

         User表2w,2個線程讀取。(每一個線程讀1w)

三個核心方法:

  ①.Reset方法:重置當前的線程數量上限。(初始化的時候,默認設置一個上限)

  ②.Signal方法:將當前的線程數量執行減1操做。(使用一個thread,這個線程數量就會減1操做,直到爲0後,繼續下一步)

  ③.Wait方法:至關於咱們的Task.WaitAll方法。

代碼實踐:

複製代碼
1  //初始化線程數量上限爲10.
 2         static CountdownEvent cdLock = new CountdownEvent(10);
 3         private void button25_Click(object sender, EventArgs e)
 4         {
 5             //加載Orders搞定
 6             cdLock.Reset(10);
 7             for (int i = 0; i < 10; i++)
 8             {
 9                 Task.Factory.StartNew(() =>
10                 {
11                     LoadOrder();
12                 });
13             }
14             cdLock.Wait();
15             Console.WriteLine("全部的Orders都加載完畢。。。。。。。。。。。。。。。。。。。。。");
16 
17             //加載Product搞定
18             cdLock.Reset(5);
19             for (int i = 0; i < 5; i++)
20             {
21                 Task.Run(() =>
22                 {
23                     LoadProduct();
24                 });
25             }
26             cdLock.Wait();
27             Console.WriteLine("全部的Products都加載完畢。。。。。。。。。。。。。。。。。。。。。");
28 
29             //加載Users搞定
30             cdLock.Reset(2);
31             for (int i = 0; i < 2; i++)
32             {
33                 Task.Factory.StartNew(() =>
34                 {
35                     LoadUser();
36                 });
37             }
38             cdLock.Wait();
39             Console.WriteLine("全部的Users都加載完畢。。。。。。。。。。。。。。。。。。。。。");
40 
41             Console.WriteLine("全部的表數據都執行結束了。。。恭喜恭喜。。。。");
42             Console.Read();
43         }
44         static void LoadOrder()
45         {
46             //書寫具體的業務邏輯
47             Console.WriteLine("當前LoadOrder正在加載中。。。{0}", Thread.CurrentThread.ManagedThreadId);
48             //線程數量減1
49             cdLock.Signal();
50 
51         }
52         static void LoadProduct()
53         {
54             //書寫具體的業務邏輯
55             Console.WriteLine("當前LoadProduct正在加載中。。。{0}", Thread.CurrentThread.ManagedThreadId);
56             //線程數量減1
57             cdLock.Signal();
58         }
59         static void LoadUser()
60         {
61             //書寫具體的業務邏輯
62             Console.WriteLine("當前LoadUser正在加載中。。。{0}", Thread.CurrentThread.ManagedThreadId);
63             //線程數量減1
64             cdLock.Signal();
65         }
複製代碼

 

 

 

 

第十一節:深究用戶模式鎖的使用場景(異變結構、互鎖、旋轉鎖)

 

一. 鎖機制的背景介紹

  本章節,將結合多線程來介紹鎖機制, 那麼問題來了,什麼是鎖呢? 爲何須要鎖? 爲何要結合多線程來介紹鎖呢?鎖的使用場景又是什麼呢? DotNet中又有哪些鎖呢?

  在接下來的幾個章節中,將陸續解答這些問題。

PS:

  多個線程對一個共享資源進行使用的時候,會出問題, 好比實際的業務場景,入庫和出庫操做同時進行,庫存量就會存在併發問題。因此鎖就是用來解決多線程資源競用的問題。

  Net領域中,鎖機制很是多,好比:時間鎖、信號量、互斥鎖、讀寫鎖、互鎖、異變結構,主要咱們能夠把他們劃分爲三大類:

    ①.用戶模式鎖:就是經過一些cpu指令或者一個死循環,來達到達到線程的等待和休眠。
    ②.內核模式鎖:就是調用win32底層的代碼,來實現thread的各類操做。
    ③.混合鎖:用戶模式+內核模式

  其中用戶模式鎖又分爲這麼幾類:異變結構、互鎖和旋轉鎖。

二. 異變結構

背景:一個線程讀,一個線程寫,在release模式下會出現bug,致使主線程沒法執行,緣由在前面章節已經介紹過了。

  方式一:利用MemoryBarrier方法進行處理 。(前面章節已介紹

  方式二:利用VolatileRead/Write方法進行處理。 (前面章節已介紹)

  方式三:volatile關鍵字進行處理,個人read和write都是從memrory中讀取,讀取的都是最新的。(下面的案例使用volatile關鍵字後,主線程能夠執行)

 代碼實踐:

複製代碼
1             public static volatile bool isStop = false;
2 //使用Volatile關鍵字處理 3 var t = new Thread(() => 4 { 5 var isSuccess = false; 6 while (!isStop) 7 { 8 isSuccess = !isSuccess; 9 } 10 }); 11 t.Start(); 12 Thread.Sleep(1000); 13 isStop = true; 14 t.Join(); 15 Console.WriteLine("主線程執行結束!"); 16 Console.ReadLine();
複製代碼

代碼結論:使用volatile關鍵字進行修飾,解決共享資源的競用問題。

三. 互鎖

  互鎖結構(Interlocked類),經常使用的方法有:

    * Increment:自增操做

    * Decrement:自減操做

    * Add: 增長指定的值

    * Exchange: 賦值

    * CompareExchange: 比較賦值

 代碼實踐:

複製代碼
1             {
 2                 //1. 自增
 3                 {
 4                     int a = 1;
 5                     Interlocked.Increment(ref a);
 6                     Console.WriteLine("自增後的數據爲:{0}", a);
 7                 }
 8                 //2. 自減
 9                 {
10                     int b = 2;
11                     Interlocked.Decrement(ref b);
12                     Console.WriteLine("自減後的數據爲:{0}", b);
13                 }
14                 //3. 增長操做
15                 {
16                     int c = 3;
17                     Interlocked.Add(ref c, 4);
18                     Console.WriteLine("增長後的數據爲:{0}", c);
19 
20                 }
21                 //4. 賦值操做
22                 {
23                     int d = 4;
24                     Interlocked.Exchange(ref d, 55);
25                     Console.WriteLine("賦值後的數據爲:{0}", d);
26 
27                 }
28                 //5. 比較賦值
29                 {
30                     //Interlocked.CompareExchange(ref num1, sum, num2);  // num1==num2 ; num1=sum;
31                     int ee = 5;
32                     Interlocked.CompareExchange(ref ee, 15, 5);
33                     Console.WriteLine("比較賦值後的數據爲:{0}", ee);
34 
35                     Interlocked.CompareExchange(ref ee, 100, 15);
36                     Console.WriteLine("比較賦值後的數據爲:{0}", ee);
37 
38                 }
39 
40             }
複製代碼

代碼結果:

 

四. 旋轉鎖

  旋轉鎖(SpinLock), 特殊的業務邏輯讓thread在用戶模式下進行自選,欺騙cpu當前thread正在運行中。

   SpinLock類有兩個核心方法,分別是:Enter和Exit方法。

 代碼實踐:

複製代碼
1             {
 2                 //下面代碼的結果:num從0-249,且是有序的。
 3                 //若是把旋轉鎖去掉,num將沒有任何順序
 4                 for (int i = 0; i < 5; i++)
 5                 {
 6                     Task.Factory.StartNew(() =>
 7                     {
 8                         for (int j = 0; j < 50; j++)
 9                         {
10                             try
11                             {
12                                 var b = false;
13                                 sl.Enter(ref b);
14                                 Console.WriteLine(num++);
15                             }
16                             catch (Exception ex)
17                             {
18                                 Console.WriteLine(ex.Message);
19                             }
20                             finally
21                             {
22                                 sl.Exit();
23                             }
24                         }
25                     });
26                 }
27             }
複製代碼

代碼結果:下面代碼的結果:num從0-249,且是有序的;若是將旋轉鎖的代碼去掉,num的輸出將沒有任何順序可言。

 

 

第十節:利用async和await簡化異步編程模式的幾種寫法

 

一. async和await簡介

PS:簡介

1. async和await這兩個關鍵字是爲了簡化異步編程模型而誕生的,使的異步編程跟簡潔,它自己並不建立新線程,但在該方法內部開啓多線程,則另算。

2. 這兩個關鍵字適用於處理一些文件IO操做。

3. 好處:代碼簡介,把異步的代碼寫成了同步的形式,提升了開發效率。

 壞處:若是使用同步思惟去理解,容易出問題,返回值對不上。

 

二. 幾種用法

狀況1:當只有async,沒有await時,方法會有個警告,和普通的多線程方法沒有什麼區別,不存在線程等待的問題。

代碼實踐:

複製代碼
1  private static async void Test1()
 2         {
 3             //主線程執行
 4             Console.WriteLine("主線程{0}開始:", Thread.CurrentThread.ManagedThreadId);
 5             //啓動新線程完成任務
 6             Task task = Task.Run(() =>
 7             {
 8                 Console.WriteLine("子線程{0}開始:", Thread.CurrentThread.ManagedThreadId);
 9                 Thread.Sleep(3000);
10                 Console.WriteLine("子線程{0}結束:", Thread.CurrentThread.ManagedThreadId);
11             });
12             //主線程執行
13             Console.WriteLine("主線程{0}結束:", Thread.CurrentThread.ManagedThreadId);
14         }
複製代碼

代碼結果:

狀況2:不推薦void返回值,使用Task來代替Task和Task<T>可以使用await, Task.WhenAny, Task.WhenAll等方式組合使用,async Void 不行。

代碼實踐:

複製代碼
1         /// <summary>
 2         /// 不推薦void返回值,使用Task來代替
 3         /// Task和Task<T>可以使用await, Task.WhenAny, Task.WhenAll等方式組合使用。async Void 不行
 4         /// </summary>
 5         private static async void Test2()
 6         {
 7             //主線程執行
 8             Console.WriteLine("主線程{0}開始:", Thread.CurrentThread.ManagedThreadId);
 9             //啓動新線程完成任務
10             Task task = Task.Run(() =>
11             {
12                 Console.WriteLine("子線程{0}開始:", Thread.CurrentThread.ManagedThreadId);
13                 Thread.Sleep(3000);
14                 Console.WriteLine("子線程{0}結束:", Thread.CurrentThread.ManagedThreadId);
15             });
16             await task;   //等待子線程執行完畢,方可執行後面的語句
17             Console.WriteLine("主線程{0}結束:", Thread.CurrentThread.ManagedThreadId);
18         }
複製代碼

代碼結果:

 

狀況3:async Task == async void。 區別:Task和Task<T>可以使用await, Task.WhenAny, Task.WhenAll等方式組合使用,async Void 不行。

代碼實踐:

複製代碼
1          /// <summary>
 2         /// 無返回值  async Task == async void
 3         /// Task和Task<T>可以使用await, Task.WhenAny, Task.WhenAll等方式組合使用,async Void 不行
 4         /// </summary>
 5         private static async Task Test3()
 6         {
 7             //主線程執行
 8             Console.WriteLine("主線程{0}開始:", Thread.CurrentThread.ManagedThreadId);
 9             //啓動新線程完成任務
10             Task task = Task.Run(() =>
11             {
12                 Console.WriteLine("子線程{0}開始:", Thread.CurrentThread.ManagedThreadId);
13                 Thread.Sleep(3000);
14                 Console.WriteLine("子線程{0}結束:", Thread.CurrentThread.ManagedThreadId);
15             });
16             await task;   //等待子線程執行完畢,方可執行後面的語句
17             Console.WriteLine("主線程{0}結束:", Thread.CurrentThread.ManagedThreadId);
18         }
複製代碼

 

代碼結果:

 

狀況4和狀況5:說明要使用子線程中的變量,必定要等子線程執行結束後再使用。

代碼實踐:

 

複製代碼
1         /// <summary>
 2         /// 帶返回值的Task,要使用返回值,必定要等子線程計算完畢才行
 3         /// </summary>
 4         /// <returns></returns>
 5         private static async Task<long> Test4()
 6         {
 7             //主線程執行
 8             Console.WriteLine("主線程{0}開始:", Thread.CurrentThread.ManagedThreadId);
 9             long result = 0;
10             //啓動新線程完成任務
11             Task task = Task.Run(() =>
12             {
13                 for (long i = 0; i < 100; i++)
14                 {
15                     result += i;
16                 }
17             });
18             await task;   //等待子線程執行完畢,方可執行後面的語句
19             Console.WriteLine("主線程{0}結束:", Thread.CurrentThread.ManagedThreadId);
20             Console.WriteLine("result:{0}", result);
21             return result;
22         }
複製代碼

 

複製代碼
1         /// <summary>
 2         /// 帶返回值的Task,要使用返回值,必定要等子線程計算完畢才行
 3         /// 與狀況四造成對比,沒有等待,最終結果不許確
 4         /// </summary>
 5         /// <returns></returns>
 6         private static Task<long> Test5()
 7         {
 8             //主線程執行
 9             Console.WriteLine("主線程{0}開始:", Thread.CurrentThread.ManagedThreadId);
10             long result = 0;
11             //啓動新線程完成任務
12             TaskFactory taskFactory = new TaskFactory();
13             Task<long> task = taskFactory.StartNew<long>(() =>
14             {
15                 for (long i = 0; i < 100; i++)
16                 {
17                     result += i;
18                 }
19                 return 1;
20             });
21             Console.WriteLine("主線程{0}結束:", Thread.CurrentThread.ManagedThreadId);
22             Console.WriteLine("result:{0}", result);
23             return task;
24         }
複製代碼

代碼結果:

   以上兩種狀況,第一種狀況含有線程等待的結果爲4950,第二個狀況麼有線程等待,結果不許確(即共享變量競用問題)。

相關文章
相關標籤/搜索