I am using Swift 3 GCD in order to perform some operations in my code. But I'm getting _dispatch_call_block_and_release error often. I suppose the reason behind this error is because different threads modify same variable, but I'm not sure how to fix problem. Here is my code and explanations:
I have one variable which is accessed and modified in different threads:
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
}
}
}
}
I start runSent method as:
DispatchQueue.global().async(execute: runSent)
I need that runSent continuously check some conditions withinn queueMsgSent, and other functions addMsgSentToQueueue and deleteMsgSentWithId are called in main thread id necessary. I am using some locking mechanism but its not working properly
I strongly suggest you to use the DispatchQueue(s)
provided by Grand Central Dispatch, they makes multithreading management much easier.
Command
Let's start with your command class
class Command {
let id: String
var isAck = false
var isSent = false
init(id:String) {
self.id = id
}
}
Queue
Now we can build our Queue
class, it will provide the following functionalities
This is our class should not be confused with the concept of DispatchQueue!
- push a
Command
into the queue - delete a
Command
from the queue - start the processing of all the elements into the queue
And now the code:
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 }
}
}
}
How does it work?
As you can see the storage
property is an array holding a list of tuples, each tuple has 2 components: Date
and Command
.
Since the storage
array is accesses by multiple threads we need to make sure it is accessed in a thread safe way.
So each time we access storage
we wrap our code into this
serialQueue.async {
// access self.storage safely
}
Each code we write into the closure 👆👆👆 shown above is added to our Serial Dispatch Queue.
The Serial Queue does process 1 closure at the time. That's why our storage property is accessed in a thread safe way!
Final consideration
The following block of code is evil
while true {
...
}
It does use all the available CPU time, it does freeze the UI (when executed on the main thread) and discharge the battery.
As you can see I replaced it with
Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in
self.processElements()
}
which calls self.processElements()
every 10 seconds leaving plenty of time to the CPU to process other threads.
Of course it's up to you changing the number of seconds to better fit your scenario.
If you're uncomfortable with the objc mechanisms, you might take a look here. Using that, you create a PThreadMutex for the specific synchronizations you want to coordinate, then use mutex.fastsync{ *your code* }
to segregate accesses. It's a simple, very lightweight mechanism using OS-level calls, but you'll have to watch out for creating deadlocks.
The example you provide depends on the object always being the same physical entity, because the objc lock uses the address as the ID of what's being synchronized. Because you seem to have to check everywhere for the existence of queueMsgSent, I'm wondering what the update value routine is doing - if it ever deletes the dictionary, expecting it to be created later, you'll have a potential race as different threads can be looking at different synchronizers.
Separately, your loop in runSent is a spin loop - if there's nothing to do, it's just going to burn CPU rather than waiting for work. Perhaps you could consider revising this to use semaphores or some more appropriate mechanism that would allow the workers to block when there's nothing to do?
来源:https://stackoverflow.com/questions/45640868/swift-3-gcd-lock-variable-and-block-and-release-error