An Experiment In Passing Variables Into A CFThread Tag By Reference
The CFThread tag, in ColdFusion, is a powerful but mysterious beast. Threads allow us to perform some very efficient, asynchronous tasks; but, at the same time, they can be tricky to interact with if you don't understand how they work. A few years ago, I discovered that values passed into a CFThread tag using the tag attributes were passed by deep-copy. On one level, this makes sense since threads run asynchronously to the spawning context; a deep-copy is a decent way to make values thread-safe. Sometimes, however, we need to pass values into a CFThread tag without a deep-copy; sometimes, we need to ensure that those references remain in tact. Doing so requieres a little finagling.
I'm not against the deep-copy approach. In most cases, I agree that the CFThread tag should be as self-contained as possible. This makes its execution safer. But, it's not always practical. Imagine, if you will, a CFFunction tag that spawns a thread that performs work on a set of data. Now, imagine that for each item in that data set, the thread has to execute a given method on a given ColdFusion component. The pseudo code for this might look something like this:
<cffunction name="doSomething">
<!---
These arguments define the values to be used in the data
processing callback. They are specific to this particular
function invocation.
--->
<cfargument name="component" />
<cfargument name="methodName" />
<!--- Launch thread to process data. --->
<cfthread action="run">
<!--- Get data. --->
<cfset data = getDataFromSomewhere() />
<!--- Iterate over data set. --->
<cfloop index="item" array="#data#">
<!--- Pass each item off to callback. --->
<cfset component[ methodName ]( item ) />
</cfloop>
</cfthread>
<!--- Return out. --->
<cfreturn />
</cffunction>
As you can see, the outer function has the callback objects that are used to process the asynchronous data set. This might look good in pseudo code; but, if you tried to write the actual ColdFusion code, it wouldn't work.
The problem in this case is that the CFThread tag doesn't actually have access to the parent function's arguments (ie. the callback objects). The CFThread tag is super guarded around what it shares with the calling context. In fact, the only non-global scope that the CFThread tag is willing to share is the Variables scope. We can try to get around this by passing the given component and methodName into the CFThread tag using its tag attributes. But, as we discussed earlier, this would pass the component by deep-copy which, in all likelihood, is not what you want (a component is typically meant to maintain state, not to be duplicated).
We could also try to store the component and methodName arguments into some Variables-scoped values; but this is also a poor idea since this CFFunction tag might be invoked multiple times. This could create race conditions on the Variables-scope and put the CFThread variables into an unpredictable state.
Ideally, what we'd want in a situation like this is a way to pass variables by-reference into the CFThread tag. But, before we look at that, let's just establish that this is necessary. In the following ColdFusion code, I'm spawning a CFThread within a CFFunction and examining both the Arguments scope and the Local scope within the various contexts.
<cffunction
name="outerContainer"
access="public"
returntype="void"
output="true"
hint="I am a function that spawns a CFThread (which is, itself, a CFFunction tag under the hood).">
<!--- Define arguments. --->
<cfargument
name="someArgument"
type="any"
required="false"
default="Container"
/>
<!---
Define the local scope.
NOTE: In ColdFusion 9, this line is ignored. Since CF9
creates an implicit local scope with the label "local",
it longer executed this (it skips it).
--->
<cfset var local = {} />
<!--- Set a local value. --->
<cfset local.someLocal = "Container" />
<!--- ------------------------------------------------- --->
<!--- ------------------------------------------------- --->
<!---
Launch a new thread. This will generate a ColdFusion user
defined function under the hood and invoke it. While this
process is hidden from us, it is IMPORTANT to know since
each CFThread has its own arguments and local scope.
--->
<cfthread
name="innerContainer"
action="run">
<!--- Set an unscoped value. --->
<cfset someInnerLocal = "CFThread" />
<!--- Dump out the arguments. --->
<cfdump
var="#arguments#"
label="INNER: Arguments"
/>
<br />
<!--- Dump out the local scope. --->
<cfdump
var="#local#"
label="INNER: Local"
/>
<br />
<!---
Check to see if unscoped values will implicitly travel
up the scope chain in order to find references.
--->
<cfdump
var="INNER: isNull( someLocal ): #isNull( 'someLocal' )#"
/>
</cfthread>
<!--- ------------------------------------------------- --->
<!--- ------------------------------------------------- --->
<!--- Block until the thread re-joins. --->
<cfthread action="join" />
<!--- Dump out outer arguments. --->
<cfdump
var="#arguments#"
label="OUTER: Arguments"
/>
<br />
<!--- Dump out outer local scope. --->
<cfdump
var="#local#"
label="OUTER: Local"
/>
<br />
<hr />
<br />
<!--- Dump out thread contents. --->
#cfthread.innerContainer.output#
<!--- Return out. --->
<cfreturn />
</cffunction>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Invoke the outer container. --->
<cfset outerContainer() />
Since CFThread tags execute as CFFunction tags under the hood, each CFThread tag gets its own Arguments scope and implicit Local scope (labelled "local" as of ColdFusion 9). In this demo, we're simply outputting both scopes in both contexts to illustrate that the CFThread tag does not have access to the corresponding container scopes. And, when we run this code, we get the following output:
As you can see, none of the arguments or locally scoped values from the outer CFFunction tag show up in references made within the CFThread tag. Remember, the only non-global scope that the CFThread tag is willing to share with its calling context is the Variables scope.
Ok, so now that we understand the situation, let's do a little exploration. We know that passing ColdFusion objects into a CFThread tag using the tag attributes will create a deep-copy. But, what if the object passed-in was not a ColdFusion object, per say? What if it was a Java object?
I did some playing around with Java objects and it seems like all the ones I tried (HashMap, Lists, etc.) got passed by deep-copy. But, there was one exception - the Soft Reference. If I wrapped a ColdFusion object in a Java Soft Reference and then passed the soft reference into the CFThread tag, it appears to leave the original reference in tact.
To see this in action, take a look at the following demo. In this code, we're passing the CFFunction tag's local scope into the CFThread tag using a soft reference:
<cffunction
name="outerContainer"
access="public"
returntype="void"
output="true"
hint="I am a function that spawns a CFThread (which is, itself, a CFFunction tag under the hood).">
<!---
Define the local scope.
NOTE: In ColdFusion 9, this line is ignored. Since CF9
creates an implicit local scope with the label "local",
it longer executed this (it skips it).
--->
<cfset var local = {} />
<!--- Set a local value. --->
<cfset local.someLocal = "Container" />
<!--- ------------------------------------------------- --->
<!--- ------------------------------------------------- --->
<!---
Launch a new thread. This will generate a ColdFusion user
defined function under the hood and invoke it. While this
process is hidden from us, it is IMPORTANT to know since
each CFThread has its own arguments and local scope.
--->
<cfthread
name="innerContainer"
action="run"
tunnel="#createObject( 'java', 'java.lang.ref.SoftReference' ).init( local )#"
sanitycheck="#local#">
<!---
Sleep this thread to make sure that the post-CFThread
content will run before this thread body does.
--->
<cfset sleep( 100 ) />
<!--- Get the parent UDF local scope. --->
<cfset superLocal = attributes.tunnel.get() />
<!---
Store the parent value in the THREAD scope so that we
double-check to make sure that the value was definitely
not copied by DEEP COPY.
--->
<cfset thread.someLocal = superLocal.someLocal />
<!---
Also copy the same key out of the "sanity check"
attribute to make sure that that one was copied by
DEEP COPY.
--->
<cfset thread.sanityCheck = attributes.sanityCheck.someLocal />
</cfthread>
<!---
Modify the value to test the deep-copy reference. If
the local scope has been deep-copied into the CFThread
attributes, then this outer-modificiation will not be
present within the thread.
--->
<cfset local.someLocal = "Container - MODIFIED" />
<!--- ------------------------------------------------- --->
<!--- ------------------------------------------------- --->
<!--- Block until the thread re-joins. --->
<cfthread action="join" />
<!--- Output the current local scope. --->
<cfdump
var="#local#"
label="OUTER: Local"
/>
<br />
<!--- Output the thread scope. --->
<cfdump
var="#cfthread.innerContainer#"
label="THREAD"
/>
<!--- Return out. --->
<cfreturn />
</cffunction>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Invoke the outer container. --->
<cfset outerContainer() />
As you can see, I'm passing the local scope into the CFThread tag both by soft reference and by direct reference. The latter, direct reference is there as a sanity check. Once the CFThread tag is spawned, I alter the local scope of the outer CFFunction to see if the alteration is represented within the CFThread itself.
When we run the above code, we get the following output:
As you can see, the Soft Reference left the passed-in variable reference in tact. This gave the CFThread tag access to the true values contained within the parent context.
This is definitely cool; but, the biggest, most blatant problem here is that we're using Soft References to do the exact opposite of what they were intended for. A Soft Reference was designed to allow values to be garbage collected; and yet, we're using them to try and ensure that the given reference is maintained. Clearly, these two intents are in direct opposition. As such, one can only assume that trouble should ensue at some point!
Most of the time, I use CFThread tags outside of ColdFusion components. As such, I can make liberal use of things like the Application scope, Request scope, and the Variables scope when I have to deal with referential integrity. Inside of a ColdFusion component, however, the story becomes quite different. Suddenly we have a stateful Variables scope and a strict sense of object encapsulation. In such cases, it can occasionally be desirable to pass objects by-reference into a CFThread tag. A Soft Reference wrapper will allow for such behavior, even if its not entirely safe.
Want to use code from this post? Check out the license.
Reader Comments
Ben,
I don't think people really understand the significance of what you have just accomplished in relation to html5 and embedded pages. Form what I understand, you are passing a variable with out using the url string or sending the variable to the sever and back before displaying on the embed page. From what I understand you are using a js page between the two html5 pages. The js page has two functions where one has focus at a time. Giving focus to the other pass the variable in it wrapper to it. Thus on click event in the first pages pass the text to the second embed page where it appear not only for the viewer that in inputting the text message but also anyone whom has the web page open at the time. Then they can reverse the process. Apparently any type of html page like php can be embedded into html5.
al
@Allan,
This code is meant to be taking place completely on the server side; I don't think it necessarily applies to any of the HTML tags.
It's possible to pass a struct?
Hi,
This is not a solution for passing the local scope. But i think most often you can make shared data instance data.
But what i often do in javascript when i pass a function to another function to be called later is this (pseudo)
var that = this;
f1(f2{that.whatever=1});
Do let a thread be able to call any function and access any data on his calling cfc, you can do:
<cfcomponent>
<cfset THAT = variables> <!---or THAT = THIS for only function calls--->
<cfset somevariable=1>
<cffunction name="f1">
<cfthread name="x" action="run">
<cfset THAT.f2()>
<cfset THAT.somevariable++>
</cfthread>
</cffunction>
<cffunction name="f2">
do something
</cffunction>
<cfcomponent>
This one works:
<cfset var tunnel = {}>
<cfset tunnel.set = createObject("java","java.util.HashSet").init()>
<cfset tunnel.set.add(local)>
<cfthread
name="somethread"
action="run"
tunnel="#tunnel#">
<cfset superLocal =attributes.tunnel.set.toArray()[1]>
</cfthread>
I decompiled the coldfusion classes, the only things duplicated are:
Array (primitive,coldfusion)
java.util.List
java.util.Map
FastHashTable
XmlNodeList
Date
TemplateProxy/Invokable(NOTDUPED)/UDFMethod(NOTDUPED)
QueryTable / Querycolumn / isQuery??