Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Jake Scott
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Jake Scott

Deeply Nested CFThread Tags Are Permitted In Lucee 5.3.2.77

By Ben Nadel on
Tags: ColdFusion

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!



Reader 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','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 :)

Reply to this Comment

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

Reply to this Comment

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

Reply to this Comment

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

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.