c#多线程同步之lock
一提起lock,想必大家都很熟悉,因为它易用,顾名思义,就是一把锁,常用于多线程的同步,一次只允许一个线程进入。最近遇到一个很诡异的bug。
1 private static readonly object lock4 = new object(); 2 3 private static void LoadResolvers(string name) 4 { 5 if (resolvesCache.Count == 0) 6 { 7 8 #if DEBUG 9 Console.WriteLine(name+",进入第一层判断,当前解析器数:" + resolvesCache.Count + ",时间:" + DateTime.Now.ToShortTimeString()); 10 #endif 11 12 lock (lock4) 13 { 14 15 if (resolvesCache.Count == 0) 16 { 17 18 #if DEBUG 19 Console.WriteLine(name + ",进入第二层判断,当前解析器数:" + resolvesCache.Count + ",时间:" + DateTime.Now.ToShortTimeString()); 20 #endif 21 22 23 List<Resolvers> listResolvers = ResolversBLL.GetAllResolvers(); 24 25 26 #if DEBUG 27 Console.WriteLine(name + ",解析器查询完,准备遍历,查询出的解析器数:" + listResolvers.Count+ ",当前解析器数:" + resolvesCache.Count + ",时间:" + DateTime.Now.ToShortTimeString()); 28 #endif 29 30 foreach (Resolvers resolver in listResolvers) 31 { 32 List<ResolverConfigures> listConfigures = ResolverConfiguresBLL.GetResolverConfiguresByResolverId(resolver.ResolverID); 33 34 LoginInfo loginInfo = LoginInfoDAL.SelectItemByItemId(resolver.ResolverID); 35 36 NameValueCollection valueCollection = new NameValueCollection(); 37 38 if (loginInfo != null) 39 { 40 valueCollection.Add("username", loginInfo.UserName); 41 valueCollection.Add("password", loginInfo.Password); 42 } 43 44 foreach (ResolverConfigures configures in listConfigures) 45 { 46 if (!string.IsNullOrEmpty(configures.Key)) 47 valueCollection.Add(configures.Key, configures.Value); 48 } 49 50 if (!resolvesCache.ContainsKey(resolver)) 51 { 52 resolvesCache.Add(resolver, valueCollection); 53 } 54 } 55 56 #if DEBUG 57 Console.WriteLine(name + ",遍历完解析器并添加完成,当前解析器数:" + resolvesCache.Count + ",时间:" + DateTime.Now.ToShortTimeString()); 58 #endif 59 60 } 61 } 62 } 63 }
这段代码的大意:从数据库中查询出解析器(23行)加入到解析器缓存中(52行)。这个牵扯到多线程,因此,第12行加了把锁。本来数据库中只有13条数据,但是软件启动后,缓存中添加了26条数据,这是为什么呢?明明double if判断,lock每次只允许一个线程进入。
图1
为了搞清楚事情的真相,我写了个控制台代码:
1 private static Dictionary<string, string> resolvesCache = new Dictionary<string, string>(); 2 public static Dictionary<string, string> ResolvesCache 3 { 4 get 5 { 6 if (resolvesCache.Count == 0) 7 { 8 LoadResolvers(); 9 } 10 11 return resolvesCache; 12 } 13 set 14 { 15 resolvesCache = value; 16 } 17 } 18 19 private static void LoadResolvers() 20 { 21 if (resolvesCache.Count == 0) 22 { 23 lock (resolvesCache) 24 { 25 Thread.Sleep(new Random().Next(1000, 3000)); 26 27 for (int i = 0; i < 13; i++) 28 { 29 string key = i + DateTime.Now.Millisecond.ToString(); 30 31 if (!resolvesCache.ContainsKey(key)) 32 { 33 resolvesCache.Add(key, "wbq"); 34 } 35 } 36 } 37 } 38 } 39 40 41 42 static void Main(string[] args) 43 { 44 45 Thread thread1 = new Thread(new ThreadStart(() => 46 { 47 Console.WriteLine("线程1:" + ResolvesCache.Count.ToString()); 48 49 })); 50 51 thread1.Start(); 52 53 54 Thread thread2 = new Thread(new ThreadStart(() => 55 { 56 57 LoadResolvers(); 58 Console.WriteLine("线程2:" + ResolvesCache.Count.ToString()); 59 60 })); 61 62 thread2.Start(); 63 64 65 Thread thread3 = new Thread(new ThreadStart(() => 66 { 67 LoadResolvers(); 68 Console.WriteLine("线程3:" + ResolvesCache.Count.ToString()); 69 70 })); 71 72 thread3.Start();
运行结果:
每次一个线程访问下缓存,缓存数据加倍变化,这是为什么呢?哦,别忘了double if判断。因为程序刚运行,三个线程几乎同时到达22行,过了第一个if。这好比,很多人在公司外面等着面试,大家赶时间点几乎同时到,但是面试是一对一进行,这时候需要等待。在24行之后,再加一个if判断:
运行结果:
这下跟正式代码一样了吧。都是double if 判断,测试代码达到要求了,为什么正式代码有问题呢?为了研究,在正式的代码上加上了好多debug,让它输出当前线程名称,记录相关日志。
从图1的日志上可以看出,这是同一个线程所为。为什么会执行两遍呢?
再看看23行代码:
List<Resolvers> listResolvers = ResolversBLL.GetAllResolvers();
跟进到 ResolversBLL类中,发现了一句代码:
private static OfficialMetadataResolveManager resolverManager = new OfficialMetadataResolveManager(BibliographyAutoUpdateProcess.ResolvesCache);
静态对象,类加载的时候,首先访问。 这不是解析器缓存的访问器吗?看看它的实现:
1 private static Dictionary<Resolvers, NameValueCollection> resolvesCache = new Dictionary<Resolvers, NameValueCollection>(); 2 3 public static Dictionary<Resolvers, NameValueCollection> ResolvesCache 4 { 5 get 6 { 7 if (resolvesCache.Count == 0) 8 { 9 LoadResolvers(Thread.CurrentThread.Name); 10 } 11 12 return resolvesCache; 13 } 14 set 15 { 16 resolvesCache = value; 17 } 18 }
第9行调用了 LoadResolvers,当前线程正在执行LoadResolvers方法,中途调用解析器缓存访问器,结果解析器缓存访问器又调用了此访问。所以这段代码执行了两次,因此,数据翻倍。终于真相大白了。
要修改其实也很简单,把第二个if判断,放到数据库查询解析器之后即可。这样的话,等于数据库查询了两次,但是缓存中只缓存一份数据。