Weak Reference Race Condition

  • Thread starter Thread starter Chris Mullins
  • Start date Start date
C

Chris Mullins

If I have an object held only by a weak reference, and I write code that
looks like:

dim strongRef as Object
if wr.IsAlive() then
strongRef=wr.Target
else
strongRef=new MyObject()
end if

There is a fairly signifigant race condition - between the "wr.IsAlive" call
and the wr.Target call, the object could be garbage collected. To the best
that I can find, there is no mechanism by which I can lock the Weak
Reference to prevent a GC between my IsAlive check, and the rooting of the
object.

To make this code legit, and work in all cases it needs to look like:

dim strongRef as Object
if wr.IsAlive() then
strongRef=wr.Target
if strongRef is nothing then strongRef = new MyObject()
else
strongRef=new MyObject()
end if

It seems like there should be a way to avoid that extra check for the
"IsAlive" case.

What am I missing?
 
Chris said:
If I have an object held only by a weak reference, and I write code that
looks like:

dim strongRef as Object
if wr.IsAlive() then
strongRef=wr.Target
else
strongRef=new MyObject()
end if

There is a fairly signifigant race condition - between the "wr.IsAlive" call
and the wr.Target call, the object could be garbage collected. To the best
that I can find, there is no mechanism by which I can lock the Weak
Reference to prevent a GC between my IsAlive check, and the rooting of the
object.

To make this code legit, and work in all cases it needs to look like:

dim strongRef as Object
if wr.IsAlive() then
strongRef=wr.Target
if strongRef is nothing then strongRef = new MyObject()
else
strongRef=new MyObject()
end if

It seems like there should be a way to avoid that extra check for the
"IsAlive" case.

What am I missing?

Why not just skip the IsAlive check?

dim strongRef as Object = Nothing

try
strongRef=wr.Target
catch e as InvalidOperationException
' do nothing
end try

if strongRef is nothing then
strongRef = new MyObject()
end if

If the target object type has a finalizer, you should catch the
exception - it's possible for it to be thrown if the finalizer has been
run, but the object has not yet been collected (at least that's my
reading of the docs).
 
mikeb said:
Why not just skip the IsAlive check?

dim strongRef as Object = Nothing

try
strongRef=wr.Target
catch e as InvalidOperationException
' do nothing
end try

if strongRef is nothing then
strongRef = new MyObject()
end if

I suspect this would work - it just seems.... inelegant. Which is usually a
sign that there's a better way.

I keep looking for somthing akin to a SyncLock that would block the GC from
doing a collect while the lock is heald. I *know* there's nothing like this,
but I keep looking anway...
 
dim strongRef as Object = wr.Target
If strongRef is Nothing Then
' It's alive and now will not be collect since you have strongref
Endif

That's all you need. No try/catch is needed.
 
dim strongRef as Object = Nothing
try
strongRef=wr.Target
catch e as InvalidOperationException
' do nothing
end try

If the target object type has a finalizer, you should catch the
exception - it's possible for it to be thrown if the finalizer has been
run, but the object has not yet been collected (at least that's my
reading of the docs).

The InvalidOperationException is thrown when the WeakReference has been
finalized and you access the Target or IsAlive properties, not the target
itself.

Niall
 
Can you provide a sample.
Because there must something else wrong with your code.
I cannot cause Target to throw an InvalidOperationException
 
Dmitriy said:
Can you provide a sample.
Because there must something else wrong with your code.
I cannot cause Target to throw an InvalidOperationException

On browsing the code for WeakReference in Reflector, it looks like the
InvalidOperationException can be thrown when there's some problem with
the GC allocating an internal handle for the weak reference
(GCHandle.InternalAlloc() returns 0). But, the exception would not get
thrown until you tried to access the Target or IsAlive properties.

So there is code that throws the exception in those properties.
However, it's unclear to me exactly what conditions will cause the
exception to be thrown.
 
Dmitriy, Mike

If you read the code for WeakReference.Finalize, you will see it sets the
handle to 0 and frees the GCHandle object it wraps. As I said in my last
post, you will get an InvalidOperationException if you try to access the
Target or IsAlive once the WeakReference has been finalized. This is because
upon finalization, the WR has cleaned up its GCHandle, so the WR can no
longer tell you anything about what the Target is/was and whether it is
alive or not.

Niall
 
Niall said:
Dmitriy, Mike

If you read the code for WeakReference.Finalize, you will see it sets the
handle to 0 and frees the GCHandle object it wraps. As I said in my last
post, you will get an InvalidOperationException if you try to access the
Target or IsAlive once the WeakReference has been finalized. This is because
upon finalization, the WR has cleaned up its GCHandle, so the WR can no
longer tell you anything about what the Target is/was and whether it is
alive or not.

If that's the only way that the GCHandle can become 0, then it seems to
me that you'd only get the exception if you're accessing the
WeakReference via another WeakReference (otherwise it would not be able
to be finalized). Or am I missing something?

That possibility had not crossed my mind - the idea of using
WeakReferences to WeakReferences makes my head hurt.
 
It can happen if you're using WRs from classes with Finalizers. Imagine an
object of Class A has a Finalizer, and also has the only reference to a WR
x. Something else has a WR to object A. A becomes unreachable, so x becomes
unreachable. The order of finalization isn't guaranteed, so it is possible
to have the WR finalized before A is finalized. Then you can have all kinds
of race conditions, etc, in order to get at the finalized WR.

Granted, it's bizarre. I didn't write the stupid code in our system that had
this problem, and there's not enough painkillers in the world to get me
through fixing it (apparently deleting it isn't an option), so I learned
about when the InvalidOperationException could happen and dealt with it :P

The m_handle is internal in the class. So it is possible that it could be
set externally, somewhere in mscorlib. However, it could just be for reading
purposes, who knows.

Niall
 
Niall said:
It can happen if you're using WRs from classes with Finalizers. Imagine an
object of Class A has a Finalizer, and also has the only reference to a WR
x. Something else has a WR to object A. A becomes unreachable, so x becomes
unreachable. The order of finalization isn't guaranteed, so it is possible
to have the WR finalized before A is finalized. Then you can have all kinds
of race conditions, etc, in order to get at the finalized WR.

Granted, it's bizarre.

I figured that the conditions for the exception would be bizarre - I've
never seen the exception myself, but I try to code for it since the docs
claim it can happen. Apparently you have direct experience with it.

As an aside - I wonder why they throw an exception instead of simply
returning null, since the net effect is that you can't a reference to
the target. In fact, I use a static method to access the target that
simply catches the exception and returns null in that case (not that
that code path has ever been hit as far as I know).
 
mikeb said:
I figured that the conditions for the exception would be bizarre - I've
never seen the exception myself, but I try to code for it since the docs
claim it can happen. Apparently you have direct experience with it.

I wish I hadn't :P

As an aside - I wonder why they throw an exception instead of simply
returning null, since the net effect is that you can't a reference to
the target. In fact, I use a static method to access the target that
simply catches the exception and returns null in that case (not that
that code path has ever been hit as far as I know).

I think the difference is that returning null for the target may lead to the
wrong impression. If you return null, you can't tell if the WR ever had a
real target in the first place. Also, there's no correct return value for
the IsAlive property because the WR has been disconnected from its target,
so the target may still be alive, or it may be dead as well. So I think from
a correctness point of view, the exception is the right way to go because
the WR no longer can tell anything about the target it once had. Obviously,
the net effect of the exception for most cases is that the WR is now useless
and you can't get to your target any more, so sooner or later you'll end up
with a null.

Niall
 
Well,
In whidbey your wishes came true and it just returns null instead of
throwing exception
 
Niall said:
I wish I hadn't :P





I think the difference is that returning null for the target may lead to the
wrong impression. If you return null, you can't tell if the WR ever had a
real target in the first place. Also, there's no correct return value for
the IsAlive property because the WR has been disconnected from its target,
so the target may still be alive, or it may be dead as well.

I hadn't thought of the situation where the WR might be gone (or
finalizing) while the target is still around...

My scenarios for using WRs have been simple - a poor man's cache usually
- so I don't think I would have run into any of these complexities. But
the education is well worthwhile.
 
mikeb said:
I hadn't thought of the situation where the WR might be gone (or
finalizing) while the target is still around...

My scenarios for using WRs have been simple - a poor man's cache usually
- so I don't think I would have run into any of these complexities. But
the education is well worthwhile.

They can be very handy for lots of things. Apart from caching, we also use
them for testing our application for memory leaks - take a WR to an object,
do something, get to a point when the object should be dead (you may need to
nudge the GC) and check the WR to see if all has gone as expected. This can
be useful to provide a general level of protection against shipping
something that has a leak in the architecture, but it's obviously not going
to catch every possible leak. Sometimes we run into issues where the GC is
not behaving as we expected, but at the end of the day, the GC will do what
it wants and we just have to live with it :P

Niall
 
Does this exception not have anything to do with whether the weakreference
has been created as a longweakreference (i.e., track after finalization set
to true) or a shortweakreference?
 
Sorry, only just saw this post now.

I doubt it has anything to do with long or short references. We have no use
for long references at the moment, so all of ours are short. It's been a
long time since I saw the InvalidOperationException, but I remember being
completely baffled as to how the code came to that state at the time, and I
don't think I'm much the wiser now :P

Niall
 
Back
Top