Understanding the purpose of CERs in this example

318 views Asked by At

I'm reading through Constrained Execution Regions and other errata [Brian Grunkemeyer] in an attempt to understand constrained execution regions, however I'm having some problems understanding the following sample:

RuntimeHelpers.PrepareConstrainedRegions();
try {
    // Prepare my backout code
    MethodInfo m = _list.GetType().GetMethod("RemoveAt", new Type[] { typeof(int) });
    RuntimeHelpers.PrepareMethod(m.MethodHandle);

    IEnumerator en = c.GetEnumerator();
    while(en.MoveNext()) {
        _list.Insert(index++, en.Current);
        // Assuming that these lines aren't reordered.
        numAdded++;
    }
    _version++;
}
catch(Exception) {
    // Reliable backout code
    while(numAdded > 0) {
        _list.RemoveAt(index--);
        numAdded--;
    }
    throw;
}

My understanding is that the try block is not constrained, only the finally and catch blocks are constrained. This means that during the try block an asynchronous exception (e.g. ThreadAbortException) can be thrown at any time, in particular it could be thrown before numAdded++ but after _list.Insert. In this case the backout code would remove one item too few from _list.

Given this I'm struggling to understand the purpose of the constrained execution region in this example.

Is my understanding of this correct or have I missed something?

1

There are 1 answers

0
Brian Gideon On

The documentation and the actual behavior of CERs do not match exactly based on what I observe. The issue you describe where a ThreadAbortException gets injected between Insert and numAdded++ is not possible with any of the .NET Framework versions I have tested. There are two possible reasons for this.

  • PrepareConstrainedRegions does, despite what the documentation says, have an observable effect on the try block. It will delay certain abort injections; specifically those that do not come while the thread is in an alertable state.
  • Even in the absence of the PrepareConstrainedRegions call the abort still will not get injected into that location. Based on the SSCLI code the abort will be injected at the backward jump to spin the while loop.

I figured some of this out while answering my own related question here and then attempting to answer a question about how Thread.Abort actually works here.

Point #2 is not legit. It is an implementation detail of the SSCLI that may not carry over to the official distributions (though I suspect it actually does). Furthermore, it ignores the possibility of having the abort injected at some point during the execution of Insert. I suppose it is possible that the crucial bits of Insert could use a CER internally though.

Point #1 may be the one that matters, but that begs the questions why did Microsoft not document it and why did the article you cited not mention it either. Surely the author of the article knew of this fact. Otherwise, I too am not understanding how the code presented could possibly be safe. In other words, it seems safe only by accident right now.

If I had to take a guess as to what PrepareConstrainedRegions is doing behind the scenes I would say that it sets a flag in the JIT engine that tells it not to inject the GC poll hook that gets placed strategically at backward branch jumps for code inside a CER try block. This GC poll hook is where the asynchronous abort would typically be injected (in addition to its main purpose related to garbage collection).