I have a problem that has been plaguing me for awhile now, I have come up with a solution which I will detail below, and although it seems to be working well I'm not super enthusiastic about it from a design point of view and I'm curious if anyone would have any recommendations about a better way to do this.
Basically, I have a shared resource, lets just say it's a directory of files. I have a single object that manages this resource (we'll call it an instance of class BossOfEverthing). BossOfEverthing handles adding, deleting, modifying and retrieving data from the files within that directory. When another object wants to access the shared resource it does so through the BossOfEverthing instance. BossOfEverthing uses locks internally since its client objects can and do exist on separate threads. BossOfEverthing also maintains an array of references to client objects that observe the BossOfEverthingClient protocol. When BossOfEverthing is about to change anything about the shared resource (perhaps due to a request from one of its clients) it notifies all of the clients ahead of time by calling an appropriate selector for each client so that they can all have a chance to respond first. BossOfEverthing is in fact the Boss, ie. the clients have no say so as to whether they approve of the shared resource being changed, but they are given the chance to perform any requisite cleanup activities first. The way I look at it, it's as if BossOfEverthing has many 'delegates'. The difference between what I need to do and the normal delegation pattern is that:
- There are many delegates/clients
- Delegates/clients are created and destroyed many times throughout the lifetime of the BossOfEverthing instance (which in fact exists throughout the lifetime of the entire application).
When an object wants to be a client of the BossOfEverthing instance it calls [BossOfEverthing addMeToYourClientsList:] (usually from its init method) and when an object wants to stop being a client of BossOfEverthing it calls [BossOfEverthing removeMeFromYourClientList:] (from its dealloc method). This way BossOfEverthing knows who to notify (and on what thread) when the shared resource changes.
Normally I would have used notifications or KVO to message the clients, but the hitch is that all of the clients need to have a chance to respond appropriately BEFORE the resource actually changes (like in a normal delegation pattern). Neither notifications or KVO block while the receivers are responding.
OK, that all seems great, but take this scenario:
- BossOfEverthing is about to change the shared resource, eg. [BossOfEverthing changeSomething] is called by some object on thread 1
- [BossOfEverthing changeSomething] acquires the lock associated with the client array
- [BossOfEverthing changeSomething] begins iterating through the client array and calling each client's somethingIsAboutToBeChanged method on the appropriate thread for each client
- In the meantime one of the clients (clientX) is about to go out of existence, so it calls [BossOfEverthing removeMeFromYourClientList:] on thread 2 from within its dealloc method
- [BossOfEverthing removeMeFromYourClientList:] attempts to acquire the lock associated with the client array on thread 2 so that it can remove clientX
What happens here is that I end up in a deadlock because:
- [BossOfEverthing changeSomething] (who has acquired the lock on thread 1) is waiting for [clientX somethingIsAboutToBeChanged] to return from thread2
- Thread 2 is stuck waiting to acquire the lock which is currently owned by [BossOfEverthing changeSomething] on thread 1 and is unable to respond to its somethingIsAboutToBeChanged method
Here's what I have done to remedy this:
- Rather than clientX calling [BossOfEverthing removeMeFromYourClientList:] from its dealloc method, it calls it from its release method (only when retainCount==1)
- [BossOfEverthing removeMeFromYourClientList:] rather than waiting on the lock just ATTEMPTS to acquire it and if it can't it returns NO.
- If [BossOfEverthing removeMeFromYourClientList:] returns NO to [clientX release] then [clientX release] calls [self performSelector:@selector(release) withObject:nil afterDelay:0.1], otherwise it just calls [super release]
This way clientX has a chance to respond to any messages that [BossOfEverthing changeSomething] might be sending it and allow [BossOfEverthing changeSomething] to finish up it's business and re开发者_运维百科lease the lock, while another call to [clientX release] is queued up in the delay.
The problem that I have with this is that:
- Client objects are of a variety of classes and so I have to copy paste my overriden release method to each of them. There's something that irks me about copying and pasting the same code to multiple classes. If objective-c allowed multiple inheritance then I could perhaps create another class 'BossOfEverthingClient' whose only defined method would be an overriden release.
- This whole procedure seems a bit convoluted.
Anyway, Thanks so much for reading my long winded post and I'll look forward to any input that anyone has. Thanks again!
The overloading of -release
is a good sign that you're in trouble....
The real problem IMO is that you take a long-lived lock in step 2 in order to iterate over the list. It's hard to keep this lock atomic because of all the other steps that can happen (some of which re-entrant).
A solution to this problem is to loosen up your hold on the data. First, every client should have a unique identifier (it's memory location is fine if you don't have anything more convenient). Create a dictionary of identifier->client. Now, in step two, lock the dictionary and take a snapshot of the identifiers. Then immediately unlock the dictionary. When callers wish to remove themselves, lock the dictionary, remove them, and and unlock.
Now, if someone vanishes while you're iterating over asking questions, that's ok. You'll take your identifier, go look in the dictionary, and find that there is no such object. Toss the identifier (it's just a string) and move onto the next one. If someone shows up in the middle of an iteration, you won't ask them that round, but hopefully that's acceptable in the first iteration (of course there are solutions if it isn't).
EDIT: A simpler solution to all this may be to simply let BossOfEverthing -retain
all of its clients before it begins asking them questions, and then -release
them when it's done. This will ensure that none of them dealloc during the question asking. This is a good solution if it's important that they all actually answer the question.
When you're about to iterate over the client array, I'd do it like so:
- Acquire the lock
- Make a shallow copy of the client array
- Release the lock
- Iterate over the array copy
By copying the array, a retain is put on every client object which will stop them from being deallocated during iteration. Also, the lock is released before the iteration begins which will avoid the deadlock. Also, it allows clients to call removeMeFromYourClientList:
during iteration, which may or may not be beneficial.
精彩评论