CFThread Attributes Are Passed By Reference - Not By Deep Copy - In Lucee 5.3.2.77
When Adobe ColdFusion first introduced the cfthread
tag, a lot of us stumbled over the fact that attributes are passed by deep-copy into the cfthread
body. This was done by design in order to help prevent developers from having to worry about accessing and mutating shared state. Which, to be fair, is a very valid concern (though, one that we need to side-step all the time). I just assumed that Lucee exhibited the same behavior. But, this morning, I woke up to find a Tweet from Brad Wood stating that Lucee passed cfthread
attributes by reference. Given the hard departure from the previous behavior, I wanted to see this for myself.
Testing this cfthread
behavior is quite easy. All we have to do is:
- Create an Struct.
- Pass the struct into a
cfthread
tag via an attribute. - Mutate the struct inside the
cfthread
body. - Check to see if the mutation applied to the original struct.
NOTE: We have to use a Struct for this experiment - not an Array - because Arrays are always passed by value in Adobe ColdFusion. As such, the
cfthread
tag will always receive a "local copy" of the top-level Array structure in Adobe ColdFusion.
If we can see the mutation in the original struct, it means that the cfthread
body received the struct by reference; and that it acted directly upon the original struct. And, if we can't see the mutation, it means that the cfthread
body received its own, isolated, local copy of the struct:
<cfscript>
public void function testThread() {
var localValues = { localKey: "from local" };
// In both Adobe ColdFusion and Lucee CFML, the CFThread body cannot reference
// the "local" scope of the parent context. As such, we have to pass the local
// reference into the CFThread body as an Attribute. However, in Adobe
// ColdFusion, this reference is passed by DEEP COPY. And, in Lucee, this
// reference is passed BY REFERENCE.
thread
name = "ThreadyMcThreadFace"
values = localValues
{
// In Lucee, this is mutating the SHARED struct.
// In Adobe, this is mutating a THREAD-LOCAL copy.
values.threadKey = "from thread";
}
threadJoin();
// Let's see how / if the function-local copy was mutated.
if ( server.keyExists( "lucee" ) ) {
writeDump(
label = "Lucee #server.lucee.version#",
var = localValues
);
} else {
writeDump(
label = "Adobe #server.coldfusion.productVersion#",
var = localValues
);
}
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
testThread();
</cfscript>
As you can see, in the Function body, we are declaring an Struct with one key. Then, we pass the struct into the cfthread
tag and attempt to set a second key. We then block-and-join the thread (in order to make sure it executed) and inspect the struct contents.
If we run this ColdFusion code in both Adobe ColdFusion 2018 and Lucee CFML 5.3, we get the following output (shows both results):
As you can see, in Adobe ColdFusion, the struct is passed by value, giving the cfthread
tag its own local copy work with. And, in Lucee CFML, the struct is passed by reference, which means that the Lucee cfthread
works directly on the original struct reference.
This is great from a performance standpoint since Lucee doesn't have to deep-copy the data-structures going into a cfthread
. But, to be clear, it does put the burden of managing shared memory space on the developer. This is a non-trivial problem, which is why Adobe went the route that they did.
That said, with the number of asynchronous processing options that we now have available in Lucee CFML, it makes sense that the controls around shared memory access are defined explicitly by the developer. The language itself can't solve most of the asynchronous access problems that we're bound to run into. As such, it is crucial that we [engineers] develop a strong sense of what is and is not OK to do with asynchronous control-flows in Lucee CFML.
Want to use code from this post? Check out the license.
Reader Comments
This kind of defeats the point of having thread attributes. I mean I thought the whole point of these attributes, was to sandbox each thread and protect it from interference from the outside environment.
Anyway. Definitely good to know, although, it seems that gap between ACF & Lucee, in terms of compatibility, keeps widening. Not good news, especially when many of my projects require cross compatibility.
@Charles,
It's a really good point. If nothing else, there's the "breaking change" aspect of it that I haven't seem really documented. At least, not that I can remember coming across.
My assumption is that since things like
runAsync()
and all the parallel iteration features (ex,array.each( closure, true )
) don't provide any sandboxing, then at least this direction is "more consistent" across all the async features.But, you are 100% correct -- you do lose that "sandbox", at least to some degree.
Ben. Yes. I can see why you might have it in other 'asynchronous' routines, but traditionally, cfthread has had sandbox integrity.
I would almost say this is bordering on a 'bug', but because it has been done deliberately to match the other asynchronous features, then, clearly it isn't a 'bug'.
It would be interesting to know, if Lucee, displayed this kind of behavior, for cfthread, in previous versions?
I might put in a feature request for an option to retain cfthread's sandbox behaviour, using something like:
By default, it could be set to 'false' to maintain Lucee's current behaviour.
@Charles,
I mean, that's a really good idea. With other things, I'm pretty sure that Lucee has a setting where you can "enable" the Adobe ColdFusion compatible behavior in the
Application.cfc
. Also, I was just looking at the "syntax" differences, and I do see that you can do something like you are saying for Function-arguments:Here, the
passby="value"
converts Array-passing to be like Adobe ColdFusion. So, there is a precedence for "turning on" older behaviors.That's really interesting. It's good to know there is an option to set a 'passBy' attribute for arguments to bridge the compatibility gap.
I didn't see anything similar for 'cfthread' in the Lucee Docs, but I remember you said, in a previous post, that 'cfthread' attributes act just like function arguments, so maybe the attribute works on 'cfthread'.
As I said though, the Lucee Docs don't mention anything about this.