views:

268

answers:

2

Hello,
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:

  1. There are many delegates/clients
  2. 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:

  1. BossOfEverthing is about to change the shared resource, eg. [BossOfEverthing changeSomething] is called by some object on thread 1
  2. [BossOfEverthing changeSomething] acquires the lock associated with the client array
  3. [BossOfEverthing changeSomething] begins iterating through the client array and calling each client's somethingIsAboutToBeChanged method on the appropriate thread for each client
  4. 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
  5. [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:

  1. [BossOfEverthing changeSomething] (who has acquired the lock on thread 1) is waiting for [clientX somethingIsAboutToBeChanged] to return from thread2
  2. 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:

  1. Rather than clientX calling [BossOfEverthing removeMeFromYourClientList:] from its dealloc method, it calls it from its release method (only when retainCount==1)
  2. [BossOfEverthing removeMeFromYourClientList:] rather than waiting on the lock just ATTEMPTS to acquire it and if it can't it returns NO.
  3. 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 release the lock, while another call to [clientX release] is queued up in the delay.

The problem that I have with this is that:

  1. 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.
  2. 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!

+1  A: 

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.

Rob Napier
+1  A: 

When you're about to iterate over the client array, I'd do it like so:

  1. Acquire the lock
  2. Make a shallow copy of the client array
  3. Release the lock
  4. 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.

Tom Dalling
This is a clearer way of saying my EDIT, and probably is the best solution for most cases.
Rob Napier
Thanks! It seems to me that the real key to preventing deadlocks is to not hold the lock while BossOfEverthing is sending messages. Unfortunately I still have to figure out an appropriate time for clients to call -removeMeFromYourClientList:. If BossOfEverthing calls -retain on all of its clients in order to ensure their existence then clients need to call -removeMeFromYourClientList: sometime before they reach -dealloc, otherwise the the client list can contain references to objects that have already fallen through their final -release and the -retain that BossOfEverthing calls has no effect.
hyperspasm
If clients call removeMeFromYourClientList from an overridden -release method (only when retainCount ==1) then that gets tricky because I need to expect that BossOfEverthing will also be calling -release on the client from another thread (after it has finished sending it a message). I have to think about this but maybe using a lock within the client's overridden -release method would solve this.
hyperspasm
As you said, the clients are sort of delegates, and delegates aren't usually retained. You'd probably only retain them temporarily while iterating.
Tom Dalling
I should have been clearer in my comment above. What I should have said is:If BossOfEverthing <temporarily> calls -retain on all of its clients in order to ensure their existence <while sending them messages> then clients need to call -removeMeFromYourClientList: sometime before they reach -dealloc, otherwise the the client list can contain references to objects that have already fallen through their final -release and the -retain that BossOfEverthing calls has no effect.
hyperspasm
Basically clients need to be able to call -removeMeFromYourClientList: on one thread while BossOfEverthing is messaging them on another thread without deadlocks or messages being sent to non-existent clients.
hyperspasm
If the retain is temporary, it won't stop the clients from being deallocated. It will just delay the deallocation in some situations.
Tom Dalling