Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the jQuery Conference 2011 (Cambridge, MA) with: Niall Kader
Ben Nadel at the jQuery Conference 2011 (Cambridge, MA) with: Niall Kader

CFThread Attributes Are Passed By Reference - Not By Deep Copy - In Lucee 5.3.2.77

By Ben Nadel on
Tags: ColdFusion

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:

  1. Create an Struct.
  2. Pass the struct into a cfthread tag via an attribute.
  3. Mutate the struct inside the cfthread body.
  4. 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):

CFThread attributes are passed by value in Adobe ColdFusion and by reference in Lucee CFML.

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.



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.

Reply to this Comment

@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.

Reply to this Comment

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?

Reply to this Comment

I might put in a feature request for an option to retain cfthread's sandbox behaviour, using something like:

<cfthread deepCopy="true">...

By default, it could be set to 'false' to maintain Lucee's current behaviour.

Reply to this Comment

@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:

<cfargument name="x" type="array" passby="value">

Here, the passby="value" converts Array-passing to be like Adobe ColdFusion. So, there is a precedence for "turning on" older behaviors.

Reply to this Comment

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.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.