对于多线程的线程安全最简单的做法就是使用锁,对于锁的应用,其实能把锁的数量降到最低甚至是不用一定是效率最高的,但是多线程操作真的是个复杂的东西,稍不留神就可能会出现问题,不只是数据出错,还可能出现死锁.以前的时候就在这方面出了很多问题,不过都是很久以前的事了,以至于都快忘记了,最近正好在写一些C#底层的东西,又遇到了些问题,觉得有必要写下来作为备注.
为了效率问题,很多年前写C++底层的时候就开始用原子数来实现原子锁了,一般就是对数字做CAS操作,即比较并赋值同时返回原来的值来判断这个值是不是我们想要的,如果是进行操作,如果不是继续等待或者退出,原子锁很多时候是为了把多步操作打包处理成原子操作来用的,原子操作就是在外界看来,这个操作就像一个操作,不会被任何多线程操作插入或打断.
原子锁只是实行方式不太一样,但是用法和普通锁是没有什么区别的.不过上面提到了,为了效率,总会想些办法来减少锁的使用,对于一些特殊情况,比如一个队列同时只有一个线程读一个线程写,就可以简化成无锁操作,又比如对一个函数的多次调用只希望同时只有一个起作用,就可以使用原子数的判断来实现,也可以省去加锁.但是对于同一个对象的多线程的操作,就要小心了,最近自己出的问题就在这里.自己觉得只要通过原子数的校验就省去锁了,而且在后面写的unittest里也没有出现问题,实际使用中也暂时没出现问题,但是在准备睡觉时突然想到,虽然原子数校验和对象操作两个语句是紧挨着的,但是毕竟是两个操作,不是原子操作,即便他们之间的处理时间间隔很短,在多线程操作时都可能被其他操作插入,没有发生只是时间问题,就像是墨菲定律说的那样,会出错的事情一定会出错.所以对于这种不太确定的操作最好还是加上锁来处理.
一个简单的标准,对于同一个对象的多线程处理,不管什么情况,最好都做加锁处理,为了效率可以考虑使用原子锁.
对于多线容器,如果有的话最好选择语言自带的原子容器,如C# System.Collections.Concurrent内的容器,为了实现操作的原子化,他的很多接口都会看着比较特殊的,比如ConcurrentDictionary,他的AddOrUpdate和GetOrAdd,就是为了实现原子化的操作,因为你在add或者get的同时如果不加锁的话容器可能就已经在改变了,所以他提供了参数让你在操作的时候把其他可能的操作作为原子操作传到接口里去,当作原子操作一起执行,从而在使用中避免了锁的使用,但是他的内部实现里有没有用锁就不太确定了,没有去看代码.
其实除了一些底层的实现,在实际的逻辑代码中,我一直的理念是尽量避免使用多线程,可以使用异步或者并发来代替.异步的话思路主要就是事件触发,并发的话就是把任务post到不同线程去处理,对于需要线程安全的对象,把对他的操作全部post到同一个线程去处理,从而避免了线程安全问题.所以我写底层一般最开始实现的就是基于多线程的任务队列.
- 本文固定链接: http://www.wy182000.com/2018/04/12/关于线程安全和原子操作原子数/
- 转载请注明: wy182000 于 Studio 发表