Skip to main content
Ben Nadel at CFinNC 2009 (Raleigh, North Carolina) with: Larry Lyons
Ben Nadel at CFinNC 2009 (Raleigh, North Carolina) with: Larry Lyons

Deeply Nested CFThread Tags Are Permitted In Lucee 5.3.2.77

By on
Tags:

When Adobe ColdFusion introduced the CFThread tag for asynchronous processing, they decided to prevent developers from nested it. That is, they explicitly prevent one CFThread tag from spawning a child CFThread tag. The rationale (as best I can remember) being that a developer could accidentally spawn an excessive number of threads and exhaust the server's thread pool. Whether or not this rationale is, well, rational, I am excited to see that this limitation does not appear to apply to Lucee. In fact, I can deeply nest CFThread tags in Lucee 5.3.2.77.

To see this in action, I'm going to spawn a series of nested CFThread tags. And then, block and wait for each nested CFThread tag to complete so that I can aggregate a series of nested results:

<cfscript>

	thread name = "a" {

		// NOTE: Adobe ColdFusion 2018 throws error: "Child threads are not supported."
		thread name = "b" {

			thread name = "c" {

				thread name = "d" {

					thread name = "e" {

						thread.result = "from-e";

					}

					threadJoin( "e" );
					thread.result = "from-d (#cfthread.e.result#)";

				}

				threadJoin( "d" );
				thread.result = "from-c (#cfthread.d.result#)";

			}

			threadJoin( "c" );
			thread.result = "from-b (#cfthread.c.result#)";

		}

		threadJoin( "b" );
		thread.result = "from-a (#cfthread.b.result#)";

	}

	threadJoin();
	echo( "Result: #cfthread.a.result#" );

</cfscript>

As you can see, each CFThread tag both spawns a child CFThread and then assigns a .result property based on the given context and the results of the child CFThread.

Now, if we were to run this CFML code in Adobe ColdFusion 2018 (syntax differences aside), we'd get the following error:

Thread B cannot be created. Child threads are not supported.

However, when we run this ColdFusion code in Lucee CFML 5.3.2.77, we get the following output:

Result: from-a (from-b (from-c (from-d (from-e))))

As you can see, Lucee has absolutely no problem spawning and joining deeply-nested CFThread tags!

This is actually a really exciting feature of the Lucee language because it is more forgiving of some architectural choices. I know for a fact that I have processes in my ColdFusion application that I would love to put inside a CFThread tag; but, haven't been able to in the past because some really deep method call somewhere also uses a CFThread tag and breaks in Adobe ColdFusion. With Lucee, however, I can now make more processes asynchronous without having to worry about deeply-nested CFThread tags. This also means that adding a CFThread tag deeply within a Lucee application, will never accidentally break a consuming context! Outstanding work!

Want to use code from this post? Check out the license.

Reader Comments

7 Comments

One thing you might also like is the implementation of the each() functions for structs, arrays and queries. When you call the each function, you pass on a closure to handle with the individual element. If you now want to work on those elements in parallel, you just add two arguments to the each() function. First one will say whether or not to execute the closure in parallel and the second one, how many simultaneous threads you want to start. Here's an example:

  • cfscript -
    aURLs =['http://www.google.com','http://www.apple.com','http://www.amazon.com','http://www.bennadel.com','http://www.rasia.ch'];
    timer label="total time" type="outline" {
    aResult = [];
    aURLs.each(function(sURL, iPosition, aSource) {
    http url=sURL method="GET" result="local.stResult";
    aResult.append(sURL & ": " & stResult.statusCode);
    });
    }
    dump(aResult);
    timer label="total time" type="outline" {
    aResult = [];
    aURLs.each(function(sURL, iPosition, aSource) {
    http url=sURL method="GET" result="local.stResult";
    aResult.append(sURL & ": " & stResult.statusCode);
    }, true, 5);
    }
    dump(aResult);
  • /cfscript -

As you may see in the second result, the order has changed. This is just because the threads finish at different times.

Hope you like it :)

426 Comments

@Gert, Hi Gert/Ben.

RE: each()

What happens if you leave out the 'thread number' parameter? Does it attempt to run all of them in parallel? And, if you have 8 items in the array and you add 4 as the second argument, does the process add 2 items to each thread?

7 Comments

@Charles,

the default is 10. But you can put in there any number.
If you have 8 elements in the array and you pass 4 as the third argument, there will be 4 threads running in parallel until all 8 are handled off.
So for example say you cfhttp a website and one of them takes 30s to answer, the others only 3s, then three threads will handle the 7 array elements while one is blocked with the 30s cfhttp. Once ALL have finished, the code after the .each() will be executed. Just like a thread action="join".

One other thing. Lucee only needs the thread name if you need to join named threads. Otherwise you can open threads as you see fit and then just use thread action="join" with no other attribute.

15,663 Comments

@Gert,

The parallel thread stuff has been really interesting to play with. I actually have an experiment in production in which 50% of requests to a particular workflow are using Struct-based parallel iteration around database access.

The concept is really exciting; but, I am discovering that the problem is a lot more nuanced. I'm tracking the Request Duration of the page in New Relic; and, what I'm seeing is that the overall duration of the request is more-or-less the same whether or not I am using parallel iteration or blocking calls to the database.

I suspect that the small cost of having to spawn and join threads is outweighing the on-average small code of the database calls?

Or perhaps there is one large bottleneck on the page, and it will always be the limiting factor. For example, I am seeing that generating S3-signed-urls takes like 40% of the processing time. So, even when running that in parallel, I still have to block and wait for it to finish.

Still playing around this stuff and trying to understand where the sweet spot is.

Are cfthread, parallel-iteration, and runAsync() all using the same mechanics under the hood (more or less)? Or are they suited for different types of processing?

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel