Creating A Closure-Based Tunnel Between A Thread And A Function In ColdFusion
When ColdFusion 10 introduced the concept of Closures to ColdFusion, I did a number of exploratory blog posts. But, I didn't really do much with them after that. A big part of this hesitation was because I was still running ColdFusion 9 in production (until only recently) and just didn't have the practical need for it. But, now that I am finally running ColdFusion 10 in production, closures are something that I'd like to do a little more thinking on. And, while I did do an extensive post on closures and CFThread a few years ago, I wanted to look specifically at using a closure to create a tunnel between a parent function and its spawned threads.
By default, ColdFusion CFThreads don't have access to the calling context. They do share the Variables scope of the page object; but, they don't have access the local scope or the arguments scope of any housing function. As such, data often has to be passed into threads as attributes, which are passed by deep-copy. Threads can then be joined back to the page and their public attributes can be queries for "response values."
A closure, however, is lexically bound and, if defined within a function, will have access to the parent function's local variables and arguments. As such, we could theoretically use a closure to create an accessor tunnel between a spawned CFThread and the parent function.
To explore this idea, I put together a quick demo in which we are given an array of names. We then spawn a thread-per-name and perform name reversal in-place using an asynchronous operation. We're using a closure to access and mutate the names array so that we don't have to pass the array, or its values, into the thread.
<cfscript>
normalNames = [ "Sarah", "Kit", "Tig", "Joanna" ];
writeDump( var = reverseNames( normalNames ), format = "text" );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I take an array of names and reverse each string value.
*
* @names I am the collection of names.
* @output false
*/
public array function reverseNames( required array names ) {
// Let's create a closure that is lexically bound to the current function. This
// will give the closure access to the local variables as well as the ARGUMENTS
// which will allow us to access and mutate the names array without having to
// pass it around by value / copy.
var accessor = function( required numeric index, string value ) {
// If both arguments are present, mutate the names array.
if ( structKeyExists( arguments, "value" ) ) {
names[ index ] = value;
// If only one argument is present, access the names array.
} else {
return( names[ index ] );
}
};
// We're going to perform the reversal using asynchronous operations.
for ( var i = 1 ; i <= arrayLen( names ) ; i++ ) {
// By default, the thread won't have access to the names collection since
// it only shares the Variables scope of the parent page object, not the
// local or arguments scope. By passing in the "accessor closure", however,
// we can provide it with a tunnel into the local function context that
// doesn't require the names collection to be passed into the thread by
// deep-copy.
thread
name = "async-operator-#i#"
accessor = accessor
index = i
{
// Access the ARGUMENTS collection in the parent function.
var name = accessor( index );
// Mutate the ARGUMENTS collection in the parent function.
accessor( index, reverse( name ) );
}
}
// Make sure all threads have had a chance to execute and rejoin the page.
thread action = "join";
// Return the original names collection (which has been mutated by the threads).
return( names );
}
</cfscript>
As you can see, each CFThread instance receives the array index it needs to change and the closure being used to access the data. And, when run the above code, we get the following output:
array - Top 4 of 4 rows
1) haraS
2) tiK
3) giT
4) annaoJ
As you can see, the values of the array were all reversed. The CFThread was able to use the passed-in closure to access and mutate the values in the parent function context.
Honestly, it's been a few years since I've really even thought about closures in ColdFusion. So, more than anything, this was just an experiment to dust off that part of the machinery. I don't think this is really anything that I haven't covered in previous posts. But, its' been a few years.
Want to use code from this post? Check out the license.
Reader Comments
One thing to be aware of, which has bitten me a few times with closures. A closure will not work when executed outside the request context of the original request in which is was recreated.
This makes it impossible to store a closure in something like the Application scope or store it in variable that is cached.
My guess is if you removed the thread join (and added some delays to your code), it would start to fail once the request finished running.
Just something to watch out for.
@Dan G. Switzer,,
Oooh, super interesting. I had never even considered that that would be an issue. Thanks for the heads-up. I wonder what must be going on behind the scenes for that to happen.
@Dan,
I did a little more digging on the use of cached closures in ColdFusion:
www.bennadel.com/blog/2953-cached-closures-and-user-defined-functions-udfs-in-coldfusion.htm
From what I can see, it seems to be tied specifically to the use of cached closures to invoke other user defined functions. On its own, caching and consumed a closure seems to work. And, using a closure to invoke a user defined function seems to work. But, when both of those things are combined, things seem to break.
Funky stuff! Thanks for the heads-up.