As far as the Ice run time is concerned, the act of destroying an Ice object is to remove the mapping between its proxy and its servant. In other words, an Ice object is destroyed when we remove its entry from the Active Servant Map (ASM). Once the ASM entry is gone, incoming operations for the object raise ObjectNotExistException
, as they should.
On this page:
Object Destruction and Concurrency
Here is the simplest version of destroy
:
void PhoneEntryI::destroy(const Current& c) { try { c.adapter?>remove(c.id); } catch (const Ice::NotRegisteredException&) throw Ice::ObjectNotExistException(__FILE__, __LINE__); } }
The implementation removes the ASM entry for the servant, thereby destroying the Ice object. If the entry does not exist (presumably, because the object was destroyed previously), destroy
throws an ObjectNotExistException
, as you would expect.
The ASM entry is removed as soon as destroy
calls remove
on the object adapter. Assuming that we implement create
as we saw earlier, so no other part of the code retains a smart pointer to the servant, this means that the ASM holds the only smart pointer to the servant, so the servant's reference count is 1.
We use smart pointers in C++, which are analogous to object references in languages such as Java and C#.
Once the ASM entry is removed (and its smart pointer destroyed), the reference count of the servant drops to zero. In C++, this triggers a call to the destructor of the servant, and the heap-allocated servant is deleted just as it should be; in languages such as Java and C#, this makes the servant eligible for garbage collection, so it will be deleted eventually as well.
Things get more interesting if we consider concurrent scenarios. One such scenario involves concurrent calls to create
and destroy
. Suppose we have the following sequence of events:
- Client A creates a phone entry.
- Client A passes the proxy for the entry to client B.
- Client A destroys the entry.
- Client A calls
create
for the same entry (passing the same name, which serves as the object identity) and, concurrently, client B callsdestroy
on the entry.
Clearly, something is strange about this scenario, because it involves two clients asking for conflicting things, with one client trying to create an object that existed previously, while another client tries to destroy the object that — unbeknownst to that client — was destroyed earlier.
Exactly what is seen by client A and client B depends on how the operations are dispatched in the server. In particular, the outcome depends on the order in which the calls on the object adapter to add
(in create
) and remove
(in destroy
) on the servant are executed:
- If the thread processing client A's invocation executes
add
before the thread processing client B's invocation, client A's call toadd
succeeds. Internally, the calls toadd
andremove
are serialized, and client B's call toremove
blocks until client A's call toadd
has completed. The net effect is that both clients see their respective invocations complete successfully. - If the thread processing client B's invocation executes
remove
before the thread processing client A's invocation executesadd
, client B's thread receives aNotRegisteredException
, which results in anObjectNotExistException
in client B. Client A's thread then successfully callsadd
, creating the object and returning its proxy.
This example illustrates that, if life cycle operations interleave in this way, the outcome depends on thread scheduling. However, as far as the Ice run time is concerned, doing this is perfectly safe: concurrent access does not cause problems for memory management or the integrity of data structures.
The preceding scenario allows two clients to attempt to perform conflicting operations. This is possible because clients can control the object identity of each phone entry: if the object identity were hidden from clients and assigned by the server (the server could assign a UUID to each entry, for example), the above scenario would not be possible. We will return to a more detailed discussion of such object identity issues in Object Identity and Uniqueness.
Concurrent Execution of Life Cycle and Non-Life Cycle Operations
This section applies to C++ only.
Another scenario relates to concurrent execution of ordinary (non-life cycle) operations and destroy
:
- Client A holds a proxy to an existing object and passes that proxy to client B.
- Client B calls the
setNumber
operation on the object. - Client A calls
destroy
on the object while Client B's call tosetNumber
is still executing.
The immediate question is what this means with respect to memory management. In particular, client A's thread calls remove
on the object adapter while client B's thread is still executing inside the object. If this call to remove
were to delete the servant immediately, it would delete the servant while client B's thread is still executing inside the servant, with potentially disastrous results.
The answer is that this cannot happen. Whenever the Ice run time dispatches an incoming invocation to a servant, it increments the servant's reference count for the duration of the call, and decrements the reference count again once the call completes. Here is what happens to the servant's reference count for the preceding scenario:
- Initially, the servant is idle, so its reference count is at least 1 because the ASM entry stores a smart pointer to the servant. (The remainder of these steps assumes that the ASM stores the only smart pointer to the servant, so the reference count is exactly 1.)
- Client B's invocation of
setNumber
arrives and the Ice run time increments the reference count to 2 before dispatching the call. - While
setNumber
is still executing, client A's invocation ofdestroy
arrives and the Ice run time increments the reference count to 3 before dispatching the call. - Client A's thread calls
remove
on the object adapter, which destroys the smart pointer in the ASM and so decrements the reference to 2. - Either
setNumber
ordestroy
may complete first. It does not matter which call completes — either way, the Ice run time decrements the reference count as the call completes, so after one of these calls completes, the reference count drops to 1. - Eventually, when the final call (
setNumber
ordestroy
) completes, the Ice run time decrements the reference count once again, which causes the count to drop to zero. In turn, this triggers the call todelete
(which calls the servant's destructor).
The net effect is that, while operations are executing inside a servant, the servant's reference count is always greater than zero. As the invocations complete, the reference count drops until, eventually, it reaches zero. However, that can only happen once no operations are executing, that is, once the servant is idle. This means that the Ice run time guarantees that a servant's destructor runs only once the final operation invocation has drained out of the servant, so it is impossible to "pull memory out from underneath an executing invocation".
For garbage-collected languages, such as C# and Java, the language run time provides the same semantics: while the servant can be reached via any reference in the application or the Ice run time, the servant will not be reclaimed by the garbage collector.