Swift 3 GCD锁定变量和block_and_release错误

时间:2017-08-11 17:35:49

标签: ios swift multithreading concurrency grand-central-dispatch

我正在使用Swift 3 GCD在我的代码中执行某些操作。但我经常遇到 _dispatch_call_block_and_release 错误。我想这个错误背后的原因是因为不同的线程修改了同一个变量,但我不确定如何修复问题。这是我的代码和解释:

我有一个变量可以在不同的线程中访问和修改:

var queueMsgSent: Dictionary<Date,BTCommand>? = nil


func lock(obj: AnyObject, blk:() -> ()) {
    objc_sync_enter(obj)
    blk()
    objc_sync_exit(obj)
}

func addMsgSentToQueue(msg: BTCommands) {

    if queueMsgSent == nil {
        queueMsgSent = Dictionary.init()
    }
    let currentDate = Date()
    lock(obj: queueMsgSent as AnyObject) {
        queueMsgSent?.updateValue(msg, forKey: currentDate)
    }
}

func deleteMsgSentWithId(id: Int) {

    if queueMsgSent == nil { return }

    for (date, msg) in queueMsgSent! {


        if msg.isAck() == false && msg.getId()! == id {
            lock(obj: queueMsgSent as AnyObject) {
                queueMsgSent?.removeValue(forKey: date)
            }
        }
   }
}

func runSent() -> Void {


    while(true) {
        if queueMsgSent == nil { continue }

        for (date, msg) in queueMsgSent! {

            if msg.isSent() == false {
                 mainSearchView?.btCom?.write(str: msg.getCommand()!)
                 msg.setSent(val: true)
                lastMsgSent = Date()
                continue
            }

            if msg.isAck() == true {
                lock(obj: queueMsgSent as AnyObject) {
                    queueMsgSent?.removeValue(forKey: date)
                }
                continue
            }



        }
   }

}

我将runSent方法启动为:

  DispatchQueue.global().async(execute: runSent)

我需要runSent不断检查queueMsgSent中的某些条件,并在主线程id中调用其他函数addMsgSentToQueueue和deleteMsgSentWithId。我正在使用一些锁定机制,但它无法正常工作

2 个答案:

答案 0 :(得分:3)

我强烈建议您使用 Grand Central Dispatch 提供的DispatchQueue(s),这使得多线程管理变得更加容易。

命令

让我们从你的命令类开始

class Command {
    let id: String
    var isAck = false
    var isSent = false

    init(id:String) {
        self.id = id
    }
}

队列

现在我们可以构建我们的Queue类,它将提供以下功能

  

这是我们的课不应该与DispatchQueue的概念混淆!

  1. Command推入队列
  2. 从队列中删除Command
  3. 将所有元素的处理启动到队列中
  4. 现在代码:

    class Queue {
        typealias Element = (date:Date, command:Command)
        private var storage: [Element] = []
        private let serialQueue = DispatchQueue(label: "serialQueue")
    
        func push(command:Command) {
            serialQueue.async {
                let newElement = (Date(), command)
                self.storage.append(newElement)
            }
        }
    
        func delete(by id: String) {
            serialQueue.async {
                guard let index = self.storage.index(where: { $0.command.id == id }) else { return }
                self.storage.remove(at: index)
            }
        }
    
        func startProcessing() {
            Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in
                self.processElements()
            }
        }
    
        private func processElements() {
            serialQueue.async {
                // send messages where isSent == false
                let shouldBeSent = self.storage.filter { !$0.command.isSent }
                for elm in shouldBeSent {
                    // TODO: add here code to send message
                    elm.command.isSent = true
                }
    
                // remove from storage message where isAck == true
                self.storage = self.storage.filter { !$0.command.isAck }
            }
        }
    }
    

    它是如何工作的?

    正如您所看到的,storage属性是一个包含元组列表的数组,每个元组都有2个组件:DateCommand

    由于storage数组是由多个线程访问的,我们需要确保以线程安全的方式访问它。

    因此,每次我们访问storage时,我们都会将代码包装到此

    serialQueue.async {
        // access self.storage safely
    }
    

    我们写入上面显示的闭包中的每个代码都会添加到串行调度队列中。

    Serial Queue当时处理1个闭包。这就是为什么我们的存储属性是以线程安全的方式访问的!

    enter image description here

    最后的考虑

    以下代码块是邪恶的

    while true {
        ...
    }
    

    它确实使用了所有可用的CPU时间,它确实冻结了UI(在主线程上执行时)并为电池放电。

    正如您所看到的,我用

    替换了它
    Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in
        self.processElements()
    }
    

    每隔10秒调用self.processElements(),留出足够的时间让CPU处理其他线程。

    当然,您需要更改秒数以更好地适应您的方案。

答案 1 :(得分:0)

如果您对objc机制感到不舒服,可以查看here。使用它,您可以为要协调的特定同步创建PThreadMutex,然后使用mutex.fastsync{ *your code* }来隔离访问。它是一种使用操作系统级别调用的简单,非常轻量级的机制,但您必须注意创建死锁。

您提供的示例取决于对象始终是同一物理实体,因为objc锁使用该地址作为同步内容的ID。因为你似乎必须在任何地方检查queueMsgSent的存在,我想知道更新值例程正在做什么 - 如果它删除了字典,期望它稍后创建,你将有潜力因为不同的线程可以看到不同的同步器。

另外,你在runSent中的循环是一个旋转循环 - 如果没有什么可做的,它只是要烧掉CPU而不是等待工作。也许你可以考虑修改它来使用信号量或一些更合适的机制,让工人在无事可做的情况下阻止?