How safe is the use of Block and Internal`InheritedBlock
You are correct about the behavior of computations done from preemptive links. So-called "preemptive evaluations" have been around since version 6. They are a class of evaluations that all work through the same mechanism. When Mathematica checks to see if a user interrupt has been requested, which it does at a high rate most of the time, it also looks to see if a preemptive evaluation is waiting to be serviced. If so, it temporarily interrupts the main evaluation and runs the preemptive evaluation. The kernel sets, and later restores, only a few small things to give the preemptive evaluation a "clean" environment in which to run. These include turning off tracing if it is on, and resetting $MessageList
to {}. No attempt is made to "unblock" the values of symbols, or more complex resetting (like undoing InheritedBlock
), and thus the evaluation sees almost the exact kernel state that the interrupted evaluation was seeing at the moment it was interrupted. It can very reasonably be argued that this behavior is a desirable feature, not a bug.
Off the top of my head, I can think of the following types of preemptive evaluations. I think this might be the complete list:
Evaluations sent on preemptive links. This includes
Dynamic
calls from the front end, and also some computations arriving from Java and .NET programs (J/Link and .NET/Link both have preemptive links back to the kernel). Users can also create their own preemptive links with the advanced (and undocumented) functionMathLink`AddSharingLink[..., MathLink`AllowPreemptive -> True]
.ScheduledTasks
(and also the olderPeriodicals
API in version 7 and earlier).AsynchronousTasks
(e.g., look up guide/AsynchronousTasks). These are part of the WolframLibrary interface, and you can probably expect to see more about them in the future.
One important fact about preemptive evaluations is that they themselves cannot be preempted. Therefore you should avoid doing anything time-consuming in a preemptive evaluation, because things like Dynamic
will hang until your preemptive evaluation completes.
It is easy to envision scenarios where a program breaks horribly because it is preempted by another piece of code that modifies the kernel state in some unexpected way. In practice, though, I think such situations are extremely rare, for two reasons. First, most Mathematica programs don't rely on global state, and are inherently reentrant. Second, most user-level programs don't get called preemptively.
If the question is "How safe is InheritedBlock
?", the answer, of course, depends on what you are doing inside the InheritedBlock
. If you are altering the properties of a fundamental System symbol like Rule, then you have the potential for trouble even in the absence of preemptive computations. Significant parts of Mathematica are written in top-level Mathematica code, and you never know when that sort of code might execute. For example, if your program issues a message, a large number of lines of system-level Mathematica code will execute as the message cell is formatted, the link to the relevant documentation page is checked and constructed, etc. Some of that code might break if you change the behavior of Rule
. There are many other examples of this.
The way to think about InheritedBlock
is just as a means to ensure that changes to values and properties of symbols get undone no matter how the block is exited. The mere fact that the changes are guaranteed to be undone doesn't confer any fundamental safety on the changes you are making, because top-level code that doesn't appear in your own program can execute at unknown times for many reasons.
The larger question is whether the existence of preemptive evaluations means we must start writing all our programs to be preemption-safe (which is almost identical to the principle of thread-safety in languages with multiple threads). The majority of Mathematica programs are already preemption-safe. The majority of ones that aren't don't need to care because they will never be executed preemptively (you're not calling your program from a Dynamic
that might interrupt a shift-enter evaluation of the same program). But for some types of programs, yes, you need to be careful about preemption-safety. J/Link is an example of a component of Mathematica that has many lines of top-level code and that needs to be bulletproof. Furthermore, it is "system-level" code that is not at all unlikely to be called from a preemptive evaluation. It relies on global state in the form of the MathLink to Java. Therefore J/Link uses PreemptProtect
in a few places internally to ensure that it can't clobber itself by being called reentrantly at a sensitive time. Most user programs don't need that level of rigor, but it's available if you want to use it.
Addendum (reply to Albert Retey's comment)
I've been asked to elaborate on why preemptive evaluations inheriting the current execution state of the kernel might be considered a feature rather than a bug. Consider the possible sets of behaviors for preemptive evaluations in that regard. First, each preemptive evaluation might be cordoned off into a completely separate execution state, as if it were done in a fresh kernel. I think people would agree that for most uses this isn't desirable. At the other end of the spectrum, represented by the current behavior, the preemptive evaluation could inherit essentially everything about the current state, including the values of Block
'ed variables. An intermediate type of behavior would have the original evaluation maintain some sort of "execution context" that contained changes to the kernel state that would be considered "local" to the evaluation. A preemptive evaluation that interrupted it would be given its own execution context that did not reflect those local changes. But what sort of things would be localized in the execution context? Perhaps the values of Block
'ed or InheritedBlock
'ed symbols. What about non-Blocked symbols? If my program sets x = 1
, do I want preemptive code to see x
as 1? Almost undoubtedly yes (if not, we are moving toward the cordoned-off extreme for preemptive evaluations that renders them practically useless). If yes, then what is so different about a Block
'ed x? After all, I used Block
instead of Module
in my program precisely because I wanted its value to be non-local to the fragment of code in which it appeared. Let's say that I write some program-monitoring code that runs as a ScheduledTask
, periodically interrupting the kernel to print some information about the state of the computation. I might want to use Block
for some local variables specfically so that this monitoring code could see (and possibly alter) their values. When you call URLFetchAynchronous
you provide a callback function that might execute preemptively. The examples on the tutorial/AsynchronousTasks help page use the global variables done
and progress
to communicate information into and out of this callback function. A programmer might prefer to use Block
for these variables, rather than making them global.
If we want to have a notion of execution context, what other sorts of things do we want localized into it? The Protect
/Unprotect
state of a symbol, perhaps. Now you could start creating a long list of things that might or might not make sense to localize, and for every one there could be an argument about whether it's a useful or undesirable thing. Of course, just because it might be hard to make decisions doesn't mean an idea is a bad one, and I'm not attempting to make a forceful defense that the current behavior is necessarily perfect. I'm just saying that an argument in favor of the current behavior can be made. Personally, I think that this behavior is sensible and, at the very least, simple to understand.
Note that I haven't spoken about the technical feasibility of any of this. The current behavior exists in no small part because it is relatively straightforward to implement.
As for "action at a distance", and "decent code broken by completely unrelated code", I don't think any of that is going on here. In the original question, InheritedBlock
was used to alter the properties of the fundamental System symbol Rule
. One of the points I was making is that this operation is dangerous even without considering preemptive evaluations, because so much of the Mathematica system is written in top-level Mathematica. Every time you hit shift-enter, there is the potential for many, many lines of Mathematica code to execute that don't appear in your program. Lots of things could break if you mess around with built-in symbols, even if you undo your changes at the end. I know that Albert and the other Mathematica pros who hang out in this forum (and are still reading this long-winded exposition) are well aware of this. People can do all sorts of clever and useful things with tricks like this, but no one should be surprised if something breaks.
The one important concern I tried to point out about decent code breaking is not about someone else breaking your own code, but rather about making sure one's own programs are safe from clobbering themselves. It's true that preemptive computations have introduced a new dynamic (pun intended) into Mathematica, the ramifications of which have not been fully digested by most programmers. If your program relies on global state (like non-local variables, streams, MathLinks, etc.), and there is any chance that some part of your program could execute preemptively, you need to make sure that your program won't break if a second instance of it preempts a running instance. Either design things so that this won't break anything, or use PreemptProtect
judiciously to protect only those segments that must be guarded from being preempted.
The vast majority of Mathematica programs are preemption-safe, and you have to put in a little work to think of one that isn't, or at least one that isn't deliberately created to break. A simple example is when a MathLink is in use. For example, if you do Install["addtwo"]
, then Dynamic[AddTwo[3,4], UpdateInterval->0.1]
, and then execute While[True, AddTwo[5,6]]
, things will break within a few seconds. You would need to use PreemptProtect[AddTwo[5,6]]
in the While
loop to prevent a preemptive call from the Dynamic
from trying to use the same link while it was in the middle of being used from a call in the While
loop.
Preliminaries
As Todd has indicated in his answer which has a lot of excellent information, the situation where the described behaviour will actually trigger problems will be very rare. I also read from his answer that WRI doesn't consider this behavior to be a bug and my hopes that this might change in future versions are low or nonexistent. I show several examples in the following which are only meant to demonstrate certain aspects, of course they are oversimplified but I guess everyone to whom this question and answer are relevant will be able to generalize from those and convert them to useful code. To circumvent having to refer to something vague like "the described problem" I introduce the term "block leaking by preemption" and will use it in this answer along with the abbreviation "BLP"...
What I absolutely want to support is Todd's statement that "most user code doesn't need that level of rigor". So if you read this and get afraid I think it is important that you probably only need to care about BLP if you write code:
- for others to use/build on
AND
- that needs to be very robust
AND
- has a reasonable probability to be used in or in combination with preemptive evaluations.
Even then, you only will see problems due to BLP very rarely. I want to emphasize that "rare" also comes in two very different meanings here:
Rare in Theory:
Due to the nature of BLP an extra criterion is whether you expect your code to be used in an evaluation that comes over a preemtive link or while such an evaluation is done. Dynamic
is the most prominent example for such preemptive evaluations, but as Todd has explained is not the only possibility. He gives a probably complete list of such preemptive evaluatons in his answer. If you don't expect your code to be used in combination with any of these functionalities, you probably don't need to read on.
There are many cases, where even in theory BLP can not happen. That includes the case where both the code in the queued evaluation and the code in the preemptive evaluation correctly block the same variable.
Even in theory a potential problem arises only when the preemptive code makes use of a global symbol without blocking it that has temporarily been changed by the queued code (There is no problem the other way round!). That (unfortunately) includes the case when the preemptive code uses InheritedBlock. It also includes the case where both evaluations use the Villegas-Gayley trick for the same symbol (isn't it an interesting coincidence that it is Todd who was the only to answer up to now?). There is of course no problem if the preemptive code doesn't make use of exactly the same symbol. Most code trying to be some sort of robust should and probably will live in an own namespace (Context
) and/or will be properly localized in some way. That only leaves those cases as problematic where system symbols are blocked and of course that is (hopefully) only necessary and done in very rare cases.
One probably should be aware that many other constructs, e.g. Do
and Table
, "effectively" use Block
and in principle are also affected. For the mentioned reasons, I only consider the case where system symbols are blocked to be really problematic and these probably are not very likely used in other constructs than Block
and Internal`InheritedBlock
...
Rare in Practice:
Even in the case that in theory there could exist a problem, it seems very likely you'll (not|never|hardly ever) see it. This is because BLP only manifests when the preemptive evaluation interrupts the queued evaluation exactly while that is in the problematic Block
construct. This to happen is in many real situations very unlikely, in fact you might have noticed that to reproducably trigger BLP for illustrations I need extra tricks like Pause
and UpdateInterval
. While this is somewhat consoling, it is on the other hand also an extra problem: if a problem exists it will typically manifest in (very) rare random malfunctions (probably including crashes) that can't easily be reproduced and are almost impossible to track and debug. A program which is affected will most probably get a reputation for being slightly unstable and stay like that for a long time (and keep the reputation even longer). (every resemblance with recent products by WRI is of course purely incidential and unintentional)...
Here is an example where you can adjust the probability of seeing an influence of the queued evaluation on the preemptive one by adjusting the length of the Pause. 0.007 gave a chance of about 50% on my machine today, the value was different yesterday, probably due to a different load on it, you might need to play with that value on your machine:
Dynamic[f[RandomReal[]]]
Block[{f}, SetAttributes[f, HoldFirst]; f[x_, y_] := 2; Pause[0.008]]
Other interesting "features" are:
- the update of the
Dynamic
is only triggered once, when the blocked symbol is actually changed, but not when its original definitions are restored after theBlock
exits. - setting attributes doesn't trigger the
Dynamic
, that seems to happen only if one changes definitons (==*Values
).
What can be done is an explicit Clear
at the end of the Block
which will trigger the Dynamic
once more, if so desired:
Block[{f}, SetAttributes[f, HoldFirst]; f[x_, y_] := 2; Pause[0.007]; Clear[f]]
"It's a feature"
As Todd has mentioned, there are of course cases where you can make use of the given behavior, e.g. when monitoring the progress of a computation running in the queued kernel, even if that blocks the variable to be monitored:
Dynamic[i]
Block[{i = 1}, While[i < 10, i++; Pause[0.1]]]
This is basically the same as using a Do
loop, which as mentioned also localizes like Block
:
Do[Pause[0.2], {i, 1, 10}]
But still...
On the other hand I think everyone having some experience in writing computer programs will agree that if something can go wrong it will go wrong sooner or later and BLP certainly has the potential to create unexpected problems.
I also slightly disagree with the position that the given situation is not worse than when the evaluation of the body of a Block
might break unknown code that's evaluated in the process of calling system functionality within its body. Of course that can also create problems, but these are local to the evaluation that I've blocked and I'll only break the evaluation of that piece of code. So I can run tests to see whether the code runs as intended and probably eliminate most of the potential pitfalls, failure should to a large intent be deterministic and reproducable and certainly is related to the code I'm calling.
The situation is different with the leaking of such changes into preemptive evaluations: there my blocking in the queued evaluation might break code which is completely unrelated to my code (unrelated in the sense that my code doesn't call it and it doesn't call my code), and I have no possibility to recognize or circumvent the problem with testing (as I don't know which code it is that I might break and if it appears it might be very hard (=impossible) to reproduce). To me that sounds much more frightening.
It also has an issue with responsibility for failure: if I provide some code which runs from a graphical user interface via the preemptive link I can't do anything to protect that from being corrupted by something stupid that just happens to be evaluated in queued mode. Of course it is that codes fault that the preemptive evaluation fails, but it is very hard to a) find what made my code break and b) explain why I can't do anything about it. This doesn't sound like solid ground to build on to me.
Strategies for Premption-Safe Code
This part cares about code which is evaluated over the queued link and might be interrupted by preemptive evaluation. The goal is to write such code in such ways that it won't break the preemptive evaluation (again, this might not be necessary for the overwhelming majority of user code).
Avoid Block (and InheritedBlock)
The most simple strategy is to avoid usage of Block
and InheritedBlock
as much as possible. While there are use cases where you can't do without them, a majority of use cases where I have seen it, it is simply used for elegance or slight performance improvements. Here are some example:
Usage of
Block
instead ofModule
for performance reasons or to avoid memory leaks from Module. Both seem to not justified for code aiming to be preemption safe. There are techniques to work around the memory leaks that can be generated by Module elsewhere on this site.Usage of
Block
to avoid the need to pass arguments because that's inconvenient and less elegant. Alternatives: do pass values nevertheless.Usage of
Block
to ensure that symbols have no values. Alternatives: use combinations ofUnevaluated
,Hold
,ReleaseHold
and friends or use formal symbols.overwrite/add functionality of system symbols: in many cases it is better to define a new symbol for the purpose which delegates to the original system symbol when appropriate instead of adding additional definitions to the system symbol in e.g. an
Internal`InheritedBlock
. Again this is often somewhat less convenient and elegant then changing the system symbol itself, but that seems not be a justification if your goal is preemption safe code.
PreemptProtect
There is PreemptProtect
which seems to exactly solve our problem as it inhibits the interruption of the queued evaluation. If you can guarantee that the body of the Block
will evaluate reasonably fast that might be the best and most simple way to achieve robustness. You should be aware that careless use of it also might cause undesired effects: If you for example press the following button while the Pause
within the PreemptProtect
waits, nothing happens, even though the button was explicitly set up for preemptive evaluation (which of course also would have been the default):
i = 1;
Button["preemptive", Print[i++], Method -> "Preemptive"]
now evaluate the following and click the above button:
PreemptProtect[Pause[3]]
It's probably interesting to see that the button clicks seem to be queued in the preemptive links while PreemptProtect
prevents their immediate evaluation. As Todd explained preemptive evaluations won't interrupt other preemptive evaluations.
Unlike for button presses the preemptive evaluations caused by Dynamic
seem to behave slightly different. In the following, the preemptive evaluations during the PreemptProtect
are either not even triggered or don't queue up but are dismissed (I hope that I interpreted this correctly?)
i = 1;
Dynamic[Print[i++]; "delete me to stop printing...",
UpdateInterval -> 1, TrackedSymbols -> {}]
and in an extra cell:
PreemptProtect[Pause[3]]
Block only where necessary
If you really need to block, only block extactly where necessary. It's tempting to use Block
for other localizations if it's there anyway and include much more as it safes some lines of code and makes things look slightly more simple/cleaner. Example:
Block[{$RecursionLimit = Infinity, x, y},
(* initialization *);
(* actual recursion *);
(* construct result *)
]
Somewhat more preemption-safe (but of course not entirely so):
Module[{x, y},
(* initialization *);
Block[{$RecursionLimit = Infinity},
(* actual recursion *);
];
(* construct result *)
];
When used in combination with PreemptProtect
the final version will provide a very safe and well behaved version:
Module[{x, y},
(* initialization *);
PreemptProtect@Block[{$RecursionLimit = Infinity},
(* actual recursion *);
];
(* construct result *)
];
$DynamicEvaluation
There is one technique which I thought up after asking the question, very similar to the Villegas-Gayley trick. I have not extensively tested it and don't know how well it will work in practice, but I think it is a promissing approach for some use cases. My main concern is preemptive evaluation triggered from dynamic interactivity, where $DynamicEvaluation
provides a documented way to get the information whether we are evaluated via the queued or the preemptive link. There might be similar variables for other preemptive evaluations (e.g. $ScheduledTask
) and I could well imagine that it's possible to find a way to find out whether our code is evaluated in a preemptive or queued way (I would certainly appreciate such information). However we get it, here is how we can make use of such information to avoid the problem:
Dynamic[f[RandomReal[]]]
This will reveal our typical problem:
Block[{f}, f[x_] := 5; Pause[1]; Clear[f]]
This will not:
Block[{f}, f[x_] /; ! $DynamicEvaluation := 5; Pause[1]; Clear[f]]
What it does is to add a definition which only can be matched in the queued evaluation and thus should not affect the dynamic evaluation (except if that would itself change $DynamicEvaluation
). It should be noted that this doesn't prevent the updates of the Dynamic
due to the changes in f
, but it at least will not see the definitions that are only meant to be done for the queued evaluation.
Here is an example which shows that this can even be done for the OwnValues
, although that seems to be quite unusual:
x = 5;Dynamic[Date[] -> x]
This will make the Dynamic
trigger (again the Dynamic
doesn't update when the original value of x is reset!):
Block[{x}, x = 1; Pause[1]; x]
This will not change the value which the preemptive evaluation sees (but still trigger its update):
x = 5;Dynamic[Date[] -> x]
Internal`InheritedBlock[{x},
OwnValues[x] = {HoldPattern[x /; Not[$DynamicEvaluation]] :> 1,
HoldPattern[x /; $DynamicEvaluation] -> x};
Pause[2];
x
]
To write a function which will work reliably in both the queued and the preemptive evaluation mode you'd of course have to explicitly only treat the case where the code is evaluated in the queued mode. Evaluations from the preemptive link can't be interrupted anyway and for them there is no need for any such precautions...
Extra Kernel
It would in principle be possible to run the "offensive" code in an extra kernel. As long as that won't have any preemptive links opened, no BLP should be able to occur. The kernels could be started with LinkOpen
and friends or we could just use the parallel functionality (with e.g. one worker kernel). I think that this is probably causing much more overhead and additional potential problems and thus doesn't think it's a good solution. It could on the other hand be an option if one uses parallel kernels of an extra kernel for other purposes anyway. And of course code that will only run in worker kernels might not be affected by BLP and can probably make use of Block with less concerns about robustness. I'm not sure whether there exist mechanism which also make use of preemptive evaluations on parallel kernels, but would guess that could well be the case.
Strategies to Write Robust Preemptive Code
This part mainly addresses those parts of code that are evaluated over the preemptive link and might interrupt queued code. As my own experience with other ways of preemptive evaluation is limited, the following concentrates on the various uses of the "Dynamic Interactivity Language" parts of Mathematica as the source of preemptive evaluations.
Localize as much as possible
Of course it is only global symbols which are potentially affected by the described problem. The fewer global symbols the dynamic code contains, the less probable it is that it's broken by (temporary) changes to such symbols in the queued evaluation.
Minimal Functionality in Dynamic
As Todd has emphasized, preemptive evaluations are meant for short immediate evaluations. From the point of preemption safe coding, the less these interrupting evaluations actually do, the less can go wrong. You'll also avoid malfunctions due to dynamic timeouts when only what really is necessary is done in the preemptive evaluation. The following is an example how that can be achieved. What we do here is to calculate something and show the result. A naive version would probably look like this:
longCalculation[] := (Pause[1]; Table[{x, Sin[x]}, {x, 0, \[Pi], \[Pi]/20}])
expensiveVisualization[data_List] := (Pause[1]; ListPlot[data])
DynamicModule[{result},
Column[{
Button["Run", result = longCalculation[], Method -> "Queued"],
Dynamic[expensiveVisualization[result]]
}]
]
It seems (not only from this point of view) much better to also do the expensive visualization in the queued evaluation and only do as minimal as possible in the preemptive evaluation, e.g.:
DynamicModule[{plot},
Column[{
Button["Run",
Module[{result}, result = longCalculation[];
plot = expensiveVisualization[result]
],
Method -> "Queued"
],
Dynamic[plot]
}]
]
There is now only almost no code which can be broken by BLP, and you'll also not run into problems when evaluating expensiveVisualization
takes longer than the dynamic timeout allows.
Take Control over when Dynamics Update
Another measure which will generally help to improve the stability of user interfaces considerably, especially when they become reasonably complex is to take as much control about when Dynamic
s actually trigger as possible. That this will also rule out many cases which could be problematic due to BLP is more or less just a side effect of that. Here is again an example:
f[x_, n_] := x^n;
DynamicModule[{n = 1},
Column[{
Slider[Dynamic[n], {1, 10}],
Dynamic[Plot[f[x, n], {x, 0, 1}]]
}]
]
Here is our typical problem, which triggers the above Dynamic
to show something (probably) unintended:
Internal`InheritedBlock[{f},
f[x_, y_] := "whatever";
Pause[1];
]
Of course we actually only wanted the plot to update when n
is changed, so we might want to let Mathematica know about that by using the TrackedSymbols
option:
f[x_, n_] := x^n;
DynamicModule[{n = 1},
Column[{
Slider[Dynamic[n], {1, 10}],
Dynamic[Plot[f[x, n], {x, 0, 1}], TrackedSymbols :> {n}]
}]
]
And now the above Dynamic
is at least not unintendedly triggered by our queued evaluation:
Internal`InheritedBlock[{f},
f[x_, y_] := "whatever";
Pause[1];
]
Experimenting with that mechanisms for system symbols reveals an interesting feature which will again explain why you'll only see such problems very rarely: a Dynamic
will obviously not trigger when system symbols are changed (that is: Dynamic
makes the reasonable expectation that these won't change). Thus our problem for system symbols can only arise when the preemptive evaluations are triggered by accident from other changes:
Here we trigger with UpdateInterval
, thus we can see BLP:
Dynamic[Plot[x, {x, 0, 1}], UpdateInterval -> 0.5]
Block[{Plot}, Pause[2]]
This won't trigger, obviously system symbols (by simple tests one can verify that it's not merely the context that's used to decide about that) are treated different than others in that respect:
Dynamic[Plot[x, {x, 0, 1}]]
Block[{Plot}, Pause[2]]
And even explicitly asking for Plot
to be tracked won't change that (All
or Full
don't change that as well, bug or feature?):
Dynamic[Plot[x, {x, 0, 1}], TrackedSymbols :> {Plot}]
Block[{Plot}, Pause[2]]
A last remark is that for complete control over when Dynamic
s fire you can use a technique I call trigger symbols and that completely circumvents the automatisms (with all problems and benefits this brings). It roughly works like this:
n = 1;
Dynamic[trigger; Plot[x^n, {x, 0, 1}], TrackedSymbols :> {trigger}]
Here it is used to trigger an update even though the change of Plot
would normally not (as we have learned from the above):
Block[{Plot}, Plot = List; trigger = AbsoluteTime[]; Pause[0.5]]
note that again we need a Pause
to ensure that the triggered preemptive evaluation has a chance to interrupt the queued evaluation before it is finished!
Here it is used to only update the Dynamic
once, although we make several changes :
Internal`InheritedBlock[{Plot}, SetOptions[Plot, Background -> LightGray];
n = 10; trigger = AbsoluteTime[]; Pause[0.5]]
Final remarks
Having know been thinking about and experimenting with "block leaking by preemption" several days and with the input of Todd my own resumee about it is:
Appearance of real problems through BLP are probably very very rare, there are already some precautions built in and we have several possibilities to circumvent it. With the mentioned measures it seems to be possible to create very reliable code, even if the default behavior seems to be questionable from that point of view (and I guess/hope we will be able to collect here some even better techniques over time).
There remains a very small chance to introduce problems when making use of
Block
and especiallyInheritedBlock
. So if you are trying to write very robust code, try to avoid them and when necessary, use them with even more extra care as ever before.The most difficult thing remaining for us users is to decide how much effort is adequate to avoid potential problems from this source for a given chunk of code. The majority of user code will probably not need any special care.
Some parts of my question and this answer are somewhat sarcastic and indicate that I'm not exactly happy with the behavior as it is. As Todd has well described, this is a design decision which has been well thought about and has to balance various aspects. For any such design decision there is no right vs. wrong, but only at most good vs. bad, and whether such a decision is to be considered good or bad needs some experience with using it for some time in practice. The outcome of such a rating depends on personal habits, use cases and is also a matter of taste. I'm afraid that my personal usage of Mathematica these days doesn't exactly match how the majority of users are using it and what it was mainly designed for. Thus I'm aware of the fact that my rating is most probably not very representative. I want to also emphasize that overall I'm a happy Mathematica user and appreciate not only the efforts that Todd and others WRI employees put into answering our questions on this site, but also their work on Mathematica which I consider to be of very good quality in general, even though I don't agree with every design decision in every detail and would hope for some parts to be implemented with reliability having a higher priority.
I probably also should add that while such problems are very unlikely to happen I have seen them in real life applications. One of those instances has cost me several days of frustrating debugging sessions and a lot of very unpleasant communication with disappointed and very unhappy users. The good news is that with the mentioned techniques I could solve the problems, the bad news is that I don't feel that I can guarantee this will not happen again (but hopefully I will need less time to find the source of error). That probably explains my motivation for the question and this lengthy answer.