关于静态List的高并发读写的线程安全问题

时间:2022-03-15 15:31:26
大家好,关于List<T>的线程安全论坛里讨论了很多,基本上都是针对怎么应用读写锁的操作发表的看法。我仔细思考了一下,有个特殊情况下的疑问想得到明确的答案,希望有大牛可以赐教。

     假设在一个Web项目中有百万级别的用户,这些用户的登录账号和登录密码我们存储在一个静态的 List<UserInfo> 中,用来提供用户登录的快速响应。

     那么,在高并发的情境下(假设用户频繁读取账号密码,修改密码)都会对该List作大量读写,但是这里每个用户读写的都是对应自己的那个UserInfo。按我的理解,List中保存的是该UserInfo的内存地址,只要我们不new一个新的UserInfo来替代当前对象,我们对这个UserInfo进行数据的修改都不影响List的本身结构,所以这样的读写是线程安全的。在这种情形下是不用对List进行读写锁定的。

      我这样理解对吗???求赐教,最好有实例说明。
   

20 个解决方案

#1


刚才做了测试,这种静态List<T>中保存百万条记录,遍历匹配起来CPU瞬间达到峰值,所以还是改用K/V方式,但是这并不影响上面的问题本质,请大家帮忙一起探讨!

#2


实际应用中只读不写的这类应用很多,一旦涉及到写操作,还是建议适当放弃性能提高安全

#3


这个可以不加锁,应为修改密码跟登录不在同一时间发生,在一个,你这个list并没有新添加数据或者删除数据,所以可以不加。

#4


关于线程安全的集合使用,请考虑使用System.Collections.Concurrent.ConcurrentBag<T>来代替List<T>

#5


用户会不会增加么?
用户信息会不会修改?
内存快撑爆了的时候,会不会删除不活跃的用户?

既然如你所说用户数无变化,用数组按id递增排放不是更好?

这种东西乱拍脑袋可不行

#6


引用 5 楼 iceheart 的回复:
用户会不会增加么?
用户信息会不会修改?
内存快撑爆了的时候,会不会删除不活跃的用户?

既然如你所说用户数无变化,用数组按id递增排放不是更好?

这种东西乱拍脑袋可不行

我提出的只是某个环节中的一个假设而已,关键问题在于"对List<T>内部元素分别进行访问是否需要对List<T>加锁"
之于构架上来说,用户当然会增加,用户密码当然也会修改,这个时候肯定对List<T>加锁进行增改,但是修改用户信息在我的业务流中比例是偏小的,我的高并发来自于读,所以我是采用读写锁的。
现在改用Dictionary<string,string> 来存储用户的账号/密钥对,三百万条记录占用800M内存,对于高并发的用户登录行为来说,多购买1G服务器内存的成本非常之低。
对于你说数组按ID递增,用户登录的时候,只给出账号和密码,我靠什么去取ID呢?
这些都是他话,各位有识之士请帮忙我回答一下"对List<T>内部元素分别进行访问是否需要对List<T>加锁"的问题。谢谢iceheart拍砖,回复就是力量,感谢。

#7


补充一下上面说的,对于用户的新增和修改,我会采用队列方式来控制,因为注册和修改在我业务流中允许有一定的等待时间。

#8


1、id: 可以唯一识别用户的,就可以做id,所以username做id毫无问题;
2、锁: 对于这种需求的服务器,账户信息的查询需求远大于更新需求,所以多线程用读写锁就够用了;
3、数据结构: 这种服务器大量的用户信息查询需求,必然要求查询时更低时间复杂度的数据结构。链表的复杂度是O(n),上百万的用户数,每个用户登录都遍历百万次链表,显然查询时间复杂度不满足要求;而红黑树的增删查效率都是O(logn),hashmap的效率是O(1),所以用以红黑树为基础的容器,或者hashmap为基础实现的容器存储用户信息,以username做key,是比较合理的选择。

#9


我觉得可以进行二分查找

#10


引用 8 楼 iceheart 的回复:
1、id: 可以唯一识别用户的,就可以做id,所以username做id毫无问题;
2、锁: 对于这种需求的服务器,账户信息的查询需求远大于更新需求,所以多线程用读写锁就够用了;
3、数据结构: 这种服务器大量的用户信息查询需求,必然要求查询时更低时间复杂度的数据结构。链表的复杂度是O(n),上百万的用户数,每个用户登录都遍历百万次链表,显然查询时间复杂度不满足要求;而红黑树的增删查效率都是O……

谢谢iceheart的建议,我现在用的是dictionary<string,string>,目前使用200线程*每秒1000次访问,cpu占用都仅在10%以下,内存占用800mb,我的服务器是xeon e5645 2.4 六核处理器,4G内存。
这个是读的效率,同时读写的效率还没测试,测试过了也会放数据上来。

#11


引用 9 楼 yao752915708 的回复:
我觉得可以进行二分查找

谢谢回复,请说明具体方法

#12


请问iceheart,如果读写都是高并发,怎么做比较好,用读写锁会产生性能问题吗?其实注册和登录事务,在同一个用户身上来说不可能同时写的时候同时读,所以是否采用写锁,而不是读写锁?仅对dictionary锁定写,写的同时允许多线程读取,会产生什么问题吗?

#13


C#我不熟,不过dictionary这种key-value的容器应该是以红黑树或avl树实现的吧。
如果读访问和写访问处于同一数量并且都很频繁,可以考虑用单一线程处理dictionary的读写访问,通过无锁队列(如果有的话)接收请求。即这个线程提供用户信息的访问服务。

#14


谢谢 iceheart  ,dictionary是基于红黑树的,收益匪浅。

#15


其实线程安全(线程同步)与并发量的高低几乎是没有关系的。即时只有两个并发修改共享变量,如果出现的非预期的结果,那么就不能算是并发安全的。

问题在于:
首先是什么情况下会出现并发安全问题
   1、多个线程
   2、存在共享数据
   3、存在读写操作(修改:添加删除都可以认为是修改)

单线程之所以是线程安全的是因为不存在对某种资源(比如某个共享变量)的竞争。

所以,多线程安全问题就落在了如何保证存在竞争资源的安全问题,所谓的安全问题就是说在不确定次数操作后的结果是否符合编写程序的预期结果。
如果符合,则可以说是线程安全。

例如List<Userinfo>这个数据结构。
要做到线程安全,那就需要确定是否是多线程。
如果确定是多线程,那么再确定是否存在共享资源的竞争条件。

下面基于List<T>是非线程安全情况。
例如:
   你的web访问List<Userinfo>这个数据结构。
   因为list<userinfo>只有一个,而且所有web线程都在访问,所以他就是个共享的资源。
   但是还不能确定他是否存在竞争。
   因为没有确定是否有多个线程对他做修改操作。(比如一个在查询/迭代,一个在删除,一个在增加)

   如果都只做迭代,因为他不修改list<userinfo>实例的本身,所以就不存在线程安全问题。(比如你说不添加新的userinfo)

   一旦有其他线程在修改list<userinfo>实例本身,那么迭代就会出现线程安全的问题
   这个时候就应该考虑用锁或者是线程安全的List来代替不安全的List

至于Userinfo这个自定义的结构,和List分析一样。
是否存在多个线程操作同一个实例,如果有,则确定是否存在竞争。
因为你上面没有说多个web端操作同一个用户,所以,userinfo就不存在竞争条件。
但是,如果多个线程操作同一个用户,并且存在修改,那么就不得不考虑线程安全问题。

userinfo 里面的每个成员变量/属性都会因userinfo存在竞争条件而可能影响线程的安全性。

---------------
至于你百万条记录访问速度问题与线程安全的关系是不大的,除非你极端的每次只允许一个线程访问。

map结构访问速度一般来说是1,但是添加速度就没得lsit快。
用map是对的,但是需要考虑map是不是多线程的竞争资源。



   

#16


用map key  userInfo  不知道有没有情况  会出现同时修改一个人的用户信息    若没有的话 理想情况下线程安全都不用考虑

#17


引用 11 楼 boois 的回复:
引用 9 楼 yao752915708 的回复:我觉得可以进行二分查找
谢谢回复,请说明具体方法

Arrays类不是有个静态方法么 用二分查找发实现的  直接调用就行了!!!

#18


领教了,哇哈哈~

#19


存在内存里总感觉很奇怪,如果服务挂了,重启后加载这几百万的数据又需要多久??

建议使用memcahced

#20


该回复于2013-03-12 09:19:41被管理员删除

#1


刚才做了测试,这种静态List<T>中保存百万条记录,遍历匹配起来CPU瞬间达到峰值,所以还是改用K/V方式,但是这并不影响上面的问题本质,请大家帮忙一起探讨!

#2


实际应用中只读不写的这类应用很多,一旦涉及到写操作,还是建议适当放弃性能提高安全

#3


这个可以不加锁,应为修改密码跟登录不在同一时间发生,在一个,你这个list并没有新添加数据或者删除数据,所以可以不加。

#4


关于线程安全的集合使用,请考虑使用System.Collections.Concurrent.ConcurrentBag<T>来代替List<T>

#5


用户会不会增加么?
用户信息会不会修改?
内存快撑爆了的时候,会不会删除不活跃的用户?

既然如你所说用户数无变化,用数组按id递增排放不是更好?

这种东西乱拍脑袋可不行

#6


引用 5 楼 iceheart 的回复:
用户会不会增加么?
用户信息会不会修改?
内存快撑爆了的时候,会不会删除不活跃的用户?

既然如你所说用户数无变化,用数组按id递增排放不是更好?

这种东西乱拍脑袋可不行

我提出的只是某个环节中的一个假设而已,关键问题在于"对List<T>内部元素分别进行访问是否需要对List<T>加锁"
之于构架上来说,用户当然会增加,用户密码当然也会修改,这个时候肯定对List<T>加锁进行增改,但是修改用户信息在我的业务流中比例是偏小的,我的高并发来自于读,所以我是采用读写锁的。
现在改用Dictionary<string,string> 来存储用户的账号/密钥对,三百万条记录占用800M内存,对于高并发的用户登录行为来说,多购买1G服务器内存的成本非常之低。
对于你说数组按ID递增,用户登录的时候,只给出账号和密码,我靠什么去取ID呢?
这些都是他话,各位有识之士请帮忙我回答一下"对List<T>内部元素分别进行访问是否需要对List<T>加锁"的问题。谢谢iceheart拍砖,回复就是力量,感谢。

#7


补充一下上面说的,对于用户的新增和修改,我会采用队列方式来控制,因为注册和修改在我业务流中允许有一定的等待时间。

#8


1、id: 可以唯一识别用户的,就可以做id,所以username做id毫无问题;
2、锁: 对于这种需求的服务器,账户信息的查询需求远大于更新需求,所以多线程用读写锁就够用了;
3、数据结构: 这种服务器大量的用户信息查询需求,必然要求查询时更低时间复杂度的数据结构。链表的复杂度是O(n),上百万的用户数,每个用户登录都遍历百万次链表,显然查询时间复杂度不满足要求;而红黑树的增删查效率都是O(logn),hashmap的效率是O(1),所以用以红黑树为基础的容器,或者hashmap为基础实现的容器存储用户信息,以username做key,是比较合理的选择。

#9


我觉得可以进行二分查找

#10


引用 8 楼 iceheart 的回复:
1、id: 可以唯一识别用户的,就可以做id,所以username做id毫无问题;
2、锁: 对于这种需求的服务器,账户信息的查询需求远大于更新需求,所以多线程用读写锁就够用了;
3、数据结构: 这种服务器大量的用户信息查询需求,必然要求查询时更低时间复杂度的数据结构。链表的复杂度是O(n),上百万的用户数,每个用户登录都遍历百万次链表,显然查询时间复杂度不满足要求;而红黑树的增删查效率都是O……

谢谢iceheart的建议,我现在用的是dictionary<string,string>,目前使用200线程*每秒1000次访问,cpu占用都仅在10%以下,内存占用800mb,我的服务器是xeon e5645 2.4 六核处理器,4G内存。
这个是读的效率,同时读写的效率还没测试,测试过了也会放数据上来。

#11


引用 9 楼 yao752915708 的回复:
我觉得可以进行二分查找

谢谢回复,请说明具体方法

#12


请问iceheart,如果读写都是高并发,怎么做比较好,用读写锁会产生性能问题吗?其实注册和登录事务,在同一个用户身上来说不可能同时写的时候同时读,所以是否采用写锁,而不是读写锁?仅对dictionary锁定写,写的同时允许多线程读取,会产生什么问题吗?

#13


C#我不熟,不过dictionary这种key-value的容器应该是以红黑树或avl树实现的吧。
如果读访问和写访问处于同一数量并且都很频繁,可以考虑用单一线程处理dictionary的读写访问,通过无锁队列(如果有的话)接收请求。即这个线程提供用户信息的访问服务。

#14


谢谢 iceheart  ,dictionary是基于红黑树的,收益匪浅。

#15


其实线程安全(线程同步)与并发量的高低几乎是没有关系的。即时只有两个并发修改共享变量,如果出现的非预期的结果,那么就不能算是并发安全的。

问题在于:
首先是什么情况下会出现并发安全问题
   1、多个线程
   2、存在共享数据
   3、存在读写操作(修改:添加删除都可以认为是修改)

单线程之所以是线程安全的是因为不存在对某种资源(比如某个共享变量)的竞争。

所以,多线程安全问题就落在了如何保证存在竞争资源的安全问题,所谓的安全问题就是说在不确定次数操作后的结果是否符合编写程序的预期结果。
如果符合,则可以说是线程安全。

例如List<Userinfo>这个数据结构。
要做到线程安全,那就需要确定是否是多线程。
如果确定是多线程,那么再确定是否存在共享资源的竞争条件。

下面基于List<T>是非线程安全情况。
例如:
   你的web访问List<Userinfo>这个数据结构。
   因为list<userinfo>只有一个,而且所有web线程都在访问,所以他就是个共享的资源。
   但是还不能确定他是否存在竞争。
   因为没有确定是否有多个线程对他做修改操作。(比如一个在查询/迭代,一个在删除,一个在增加)

   如果都只做迭代,因为他不修改list<userinfo>实例的本身,所以就不存在线程安全问题。(比如你说不添加新的userinfo)

   一旦有其他线程在修改list<userinfo>实例本身,那么迭代就会出现线程安全的问题
   这个时候就应该考虑用锁或者是线程安全的List来代替不安全的List

至于Userinfo这个自定义的结构,和List分析一样。
是否存在多个线程操作同一个实例,如果有,则确定是否存在竞争。
因为你上面没有说多个web端操作同一个用户,所以,userinfo就不存在竞争条件。
但是,如果多个线程操作同一个用户,并且存在修改,那么就不得不考虑线程安全问题。

userinfo 里面的每个成员变量/属性都会因userinfo存在竞争条件而可能影响线程的安全性。

---------------
至于你百万条记录访问速度问题与线程安全的关系是不大的,除非你极端的每次只允许一个线程访问。

map结构访问速度一般来说是1,但是添加速度就没得lsit快。
用map是对的,但是需要考虑map是不是多线程的竞争资源。



   

#16


用map key  userInfo  不知道有没有情况  会出现同时修改一个人的用户信息    若没有的话 理想情况下线程安全都不用考虑

#17


引用 11 楼 boois 的回复:
引用 9 楼 yao752915708 的回复:我觉得可以进行二分查找
谢谢回复,请说明具体方法

Arrays类不是有个静态方法么 用二分查找发实现的  直接调用就行了!!!

#18


领教了,哇哈哈~

#19


存在内存里总感觉很奇怪,如果服务挂了,重启后加载这几百万的数据又需要多久??

建议使用memcahced

#20


该回复于2013-03-12 09:19:41被管理员删除

#21