有一个实例里的数组存在线程安全问题,有没有好的处理办法?

2016-09-08 10:58:03 +08:00
 zioc
Storage 类里面有 MutableArray tickets
多个 vc 里面会更新、使用 tickets ,如执行类似如下代码块,存在线程安全问题

NSInteger index = self.tickets.count - 1;
if (index >= 0) { //sell a ticket
Ticket *ticket = [self.tickets objectAtIndex:index];
[self.tickets removeObjectAtIndex:index];
}else{
//sold out
}

如果在每个使用的地方去加锁肯定可以避免线程安全问题,有没有更好的方法?比如在类里面就处理了?
2917 次点击
所在节点    iDev
16 条回复
GoForce5500
2016-09-08 11:22:48 +08:00
绝对不考虑所有地方加锁的方式来“避免”线程安全问题,这只会掩盖问题,让将来的 Debug 更难定位问题。
计算机领域通行的做法是额外加一层,代理对它的操作,在代理层保障线程安全。
zioc
2016-09-08 11:31:31 +08:00
@GoForce5500 但是每个地方的使用场景不一样,不一定是简单的 addObject 、 removeObject 几种操作。
一个场景可能是多种操作,如果每种操作独立上锁,还是会出现线程安全问题。比如取索引后,用索引去删除数组某项时,索引已经越界了
SlipStupig
2016-09-08 12:08:30 +08:00
可以参考一下 mysql 的做法,做一个资源锁去控制访问颗粒度, mysql 当写入数据或者事务提交的时候全部锁住,也就是数据在这个时候是不能读的,直到完成所有的任务的时候才能进行读取(这个可以考虑设置一个最大超时值),当读取操作的时候, update 类操作就不能进行了,思想就是将操作分类避免冲突
xi_lin
2016-09-08 12:48:14 +08:00
你封装一下 Storage 类的 ticket 调用方法不要直接暴露呗。。内部保证线程安全就好
zioc
2016-09-08 13:38:50 +08:00
@xi_lin 保证不了

比如通过 getCount 取最后一项的索引,再执行 removeObjectAtIndex 就闪退了。 分别在 getCount 和 removeObjectAtIndex 里做上锁解锁并不是线程安全的。
GoForce5500
2016-09-08 14:19:22 +08:00
@zioc 继续封装上层操作(如 putIfAbsent),这种场景只要是非原子操作就必须通过内部加锁完成,依赖外部锁极其依赖程序员的自觉,完全不可靠。
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 里面的。其实就是你自己说的到处加锁只是封装一下就不用写那么多重复代码而已。
xi_lin
2016-09-09 12:42:08 +08:00
@zioc 内部封装不是只在方法层级上的封装啊,你要在 Storage 类内部确保
xi_lin
2016-09-09 12:44:15 +08:00
@kitalphaj lz 的问题里 getCount 和 remove 是两个 transaction 吧。只是写锁要排斥读锁就是了。
hitmanx
2016-09-09 13:29:45 +08:00
@kitalphaj 关于第二点有个疑问,不知道我是不是理解错了。作为 storage 类的作者怎么能预先知道(穷举)使用者有哪些可能的 transaction ,就像你说的, transaction 可能是多个 ops 组成的,它的组合方式可能很多,所以对于 storage 类是没法提供全部可能的接口的。最后还是只有调用者自己知道哪些 ops 是应该属于单个 transaction 的,而哪些 ops 是可以组成不同的 transaction 的。这样的话其实与到处加锁也差距不远?
hitmanx
2016-09-09 13:32:08 +08:00
@xi_lin 这个不一定的吧,如果按照 index remove 的话,两者就是有关联的。当 remove 时,前面获取的 count 可能已经失效了,除非放在同一个 transaction 内
zioc
2016-09-09 13:40:20 +08:00
@xi_lin
@hitmanx 是的 getCount 和 remove 应该是一个 transaction ,否则 index 就失效了

@kitalphaj 感谢回答,但具体不好操作,@hitmanx 已经阐述了
mofet
2016-09-09 14:06:42 +08:00
这样的需求建议加层啊…… tickets 数组单独封装管理,对外不可见,所有操作统一调接口方法。 transaction 可以考虑用 block 做,不用管调用者有多少 ops 。
kitalphaj
2016-09-09 15:41:54 +08:00
@hitmanx
@zioc
嗯,具体实现肯定是就事论事。比如说数据库操作,简单点的 transaction 比如 removeLastIfExists 就可以封装 getCount 和 remove 两个操作。复杂一点的这样肯定就不行。但是既然这个程序是你在写,那么哪些常见的 transaction 应该提供就可以大致罗列出来。而那些无法预测的,就单独提供一个加锁解锁功能,如果操作很复杂,那你多写几行加锁解锁操作也是可以接受的吧,@mofet 提到的 block 法就可以。架构这个东西不可能做到完美,抽象往往跟不上需求,所以肯定会有妥协。但是这样做肯定比不做好,不做的话更难维护。当然这些都是个人观点, transaction 相关的可以看看 Distributed Algorithms 这一类的书,在分发式系统里面这种问题挺常见的。
zioc
2016-09-09 15:54:10 +08:00
@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 ,没看到它的定义和实现,这个具体实现的代码大概是?
xi_lin
2016-09-09 18:46:50 +08:00
@hitmanx 我理解错问题了。我以为要处理的就是 index 失效的问题

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/304756

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX