V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
iOS 开发实用技术导航
NSHipster 中文版
http://nshipster.cn/
cocos2d 开源 2D 游戏引擎
http://www.cocos2d-iphone.org/
CocoaPods
http://cocoapods.org/
Google Analytics for Mobile 统计解决方案
http://code.google.com/mobile/analytics/
WWDC
https://developer.apple.com/wwdc/
Design Guides and Resources
https://developer.apple.com/design/
Transcripts of WWDC sessions
http://asciiwwdc.com
Cocoa with Love
http://cocoawithlove.com/
Cocoa Dev Central
http://cocoadevcentral.com/
NSHipster
http://nshipster.com/
Style Guides
Google Objective-C Style Guide
NYTimes Objective-C Style Guide
Useful Tools and Services
Charles Web Debugging Proxy
Smore
zioc
V2EX  ›  iDev

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

  •  
  •   zioc · 2016-09-08 10:58:03 +08:00 · 3062 次点击
    这是一个创建于 3030 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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
    }

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

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

    @kitalphaj 感谢回答,但具体不好操作,@hitmanx 已经阐述了
    mofet
        13
    mofet  
       2016-09-09 14:06:42 +08:00
    这样的需求建议加层啊…… tickets 数组单独封装管理,对外不可见,所有操作统一调接口方法。 transaction 可以考虑用 block 做,不用管调用者有多少 ops 。
    kitalphaj
        14
    kitalphaj  
       2016-09-09 15:41:54 +08:00
    @hitmanx
    @zioc
    嗯,具体实现肯定是就事论事。比如说数据库操作,简单点的 transaction 比如 removeLastIfExists 就可以封装 getCount 和 remove 两个操作。复杂一点的这样肯定就不行。但是既然这个程序是你在写,那么哪些常见的 transaction 应该提供就可以大致罗列出来。而那些无法预测的,就单独提供一个加锁解锁功能,如果操作很复杂,那你多写几行加锁解锁操作也是可以接受的吧,@mofet 提到的 block 法就可以。架构这个东西不可能做到完美,抽象往往跟不上需求,所以肯定会有妥协。但是这样做肯定比不做好,不做的话更难维护。当然这些都是个人观点, transaction 相关的可以看看 Distributed Algorithms 这一类的书,在分发式系统里面这种问题挺常见的。
    zioc
        15
    zioc  
    OP
       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
        16
    xi_lin  
       2016-09-09 18:46:50 +08:00
    @hitmanx 我理解错问题了。我以为要处理的就是 index 失效的问题
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1012 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 21:30 · PVG 05:30 · LAX 13:30 · JFK 16:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.