1
GoForce5500 2016-09-08 11:22:48 +08:00
绝对不考虑所有地方加锁的方式来“避免”线程安全问题,这只会掩盖问题,让将来的 Debug 更难定位问题。
计算机领域通行的做法是额外加一层,代理对它的操作,在代理层保障线程安全。 |
2
zioc OP @GoForce5500 但是每个地方的使用场景不一样,不一定是简单的 addObject 、 removeObject 几种操作。
一个场景可能是多种操作,如果每种操作独立上锁,还是会出现线程安全问题。比如取索引后,用索引去删除数组某项时,索引已经越界了 |
3
SlipStupig 2016-09-08 12:08:30 +08:00
可以参考一下 mysql 的做法,做一个资源锁去控制访问颗粒度, mysql 当写入数据或者事务提交的时候全部锁住,也就是数据在这个时候是不能读的,直到完成所有的任务的时候才能进行读取(这个可以考虑设置一个最大超时值),当读取操作的时候, update 类操作就不能进行了,思想就是将操作分类避免冲突
|
4
xi_lin 2016-09-08 12:48:14 +08:00
你封装一下 Storage 类的 ticket 调用方法不要直接暴露呗。。内部保证线程安全就好
|
5
zioc OP @xi_lin 保证不了
比如通过 getCount 取最后一项的索引,再执行 removeObjectAtIndex 就闪退了。 分别在 getCount 和 removeObjectAtIndex 里做上锁解锁并不是线程安全的。 |
6
GoForce5500 2016-09-08 14:19:22 +08:00
@zioc 继续封装上层操作(如 putIfAbsent),这种场景只要是非原子操作就必须通过内部加锁完成,依赖外部锁极其依赖程序员的自觉,完全不可靠。
|
7
kitalphaj 2016-09-09 08:31:43 +08:00
这种应该是典型的多线程 Transaction 问题,一个 transaction 是一系列的操作,然后最后一起 commit 。
两种思路: 1. 操作本地 copy ,提交的时候再决定如何 merge 。 Git 就是其中一个例子,你本地有一个 copy ,不管是 remove 还是 add 还是 getIndex 都是对本地 copy 的操作,不影响真正的远端代码。等你最后 commit 的时候,如果没冲突就原子操作写到远端代码里,如果有你就要手动解决冲突。 Realm 也是这样保证多线程访问的。 2. transaction 加锁 这种就是楼上各位讲的封装,每次 transaction 的时候加锁,然后操作完成了解锁。注意,一个 transaction 是由很多操作组成, getCount 和 remoteObjectAtIndex 是一个 transaction 里面的。其实就是你自己说的到处加锁只是封装一下就不用写那么多重复代码而已。 |
9
xi_lin 2016-09-09 12:44:15 +08:00
@kitalphaj lz 的问题里 getCount 和 remove 是两个 transaction 吧。只是写锁要排斥读锁就是了。
|
10
hitmanx 2016-09-09 13:29:45 +08:00
@kitalphaj 关于第二点有个疑问,不知道我是不是理解错了。作为 storage 类的作者怎么能预先知道(穷举)使用者有哪些可能的 transaction ,就像你说的, transaction 可能是多个 ops 组成的,它的组合方式可能很多,所以对于 storage 类是没法提供全部可能的接口的。最后还是只有调用者自己知道哪些 ops 是应该属于单个 transaction 的,而哪些 ops 是可以组成不同的 transaction 的。这样的话其实与到处加锁也差距不远?
|
11
hitmanx 2016-09-09 13:32:08 +08:00
@xi_lin 这个不一定的吧,如果按照 index remove 的话,两者就是有关联的。当 remove 时,前面获取的 count 可能已经失效了,除非放在同一个 transaction 内
|
12
zioc OP |
13
mofet 2016-09-09 14:06:42 +08:00
这样的需求建议加层啊…… tickets 数组单独封装管理,对外不可见,所有操作统一调接口方法。 transaction 可以考虑用 block 做,不用管调用者有多少 ops 。
|
14
kitalphaj 2016-09-09 15:41:54 +08:00
@hitmanx
@zioc 嗯,具体实现肯定是就事论事。比如说数据库操作,简单点的 transaction 比如 removeLastIfExists 就可以封装 getCount 和 remove 两个操作。复杂一点的这样肯定就不行。但是既然这个程序是你在写,那么哪些常见的 transaction 应该提供就可以大致罗列出来。而那些无法预测的,就单独提供一个加锁解锁功能,如果操作很复杂,那你多写几行加锁解锁操作也是可以接受的吧,@mofet 提到的 block 法就可以。架构这个东西不可能做到完美,抽象往往跟不上需求,所以肯定会有妥协。但是这样做肯定比不做好,不做的话更难维护。当然这些都是个人观点, transaction 相关的可以看看 Distributed Algorithms 这一类的书,在分发式系统里面这种问题挺常见的。 |
15
zioc OP @mofet 这个办法很好,谢谢
@kitalphaj 最后采用的是 @mofet 说的方法,传 block , block 执行前加锁,完成后解锁。非常感谢你的回复:) @interface SafeMutableArray<__covariant ObjectType> : NSObject typedef void(^TransactionBlock)(NSMutableArray<ObjectType> * _Nullable mutableArray); - (void)transactUsingBlock:(nonnull TransactionBlock)transBlock; @end 另外请教一下在 NSArray 里面有个协议 ObjectType ,没看到它的定义和实现,这个具体实现的代码大概是? |