(Take #2) ThreadRecycler: Recycle and reuse threads! | v0.3.0

ThreadRecycler

A thread pool module strictly written in Luau.


GitHub Repo | Documentation & Tutorial | Releases | Last updated: 2/6/25

:tada: v0.3.0 is released! Improvements, API changes, and more!

View benchmarks before and after the release of v0.3.0

Important change from v0.2.x; the module now works as it should now, recycling threads properly.
This gives a huge performance improvement, now that threads are being recycled and reused properly. However, I have added breaking API changes that may not be backwards compatible with v0.2.x. More changes are stated in GitHub releases.

This is based solely on calling the function. Both functions were called 10000 times.

Before (v0.2.2)

After (v0.3.0)

As you can see from this histogram, v0.3.0 performs way better than v0.2.x.

Additionally, I made a post about this module before (which is why I have this post labled as Take #2), but unlisted it due to the recent problem with v0.2.x.
[MOVED] ThreadRecycler: Recycle and reuse threads! | v0.2.2

Performance

ThreadRecycler optimizes thread usage by recycling and reusing it in a thread pool, which reduces resource consumption. Fewer resources are wasted on unnecessary threads leading to faster execution.

Limitations

  • Must replace task.wait() with ThreadPool.wait() to prevent issues in the task library (more context in the documentation. However, if you only use ThreadPool.wrap() in a thread pool (& don’t use any other functions), this step might not be necessary.
  • Using ThreadPool.unsafedefer() and ThreadPool.unsafedelay() should be avoided due to task library issues
    • (Possible warning if used) “task.defer should not be called on a thread that is already ‘deferred’ in the task library”
  • May have type errors on the new type solver?
    • Unfortunately, the beta feature is bugged on my side so I’m unsure whether there’s type errors or not.
  • As this is my first open-source and public module, it may require significant improvements.

Benchmarks

(For Take #2, I have provided better benchmarks).

Figure 1: task.spawn vs. recycler.spawn

Histogram based on calls and how much time it took to run the function in ms. This was created using Benchmarker.
This is based solely on calling the function. Both functions were called 10000 times.
If you have the Benchmarker plugin, you can try this out yourself by going into the GitHub repo and downloading the bench file. (root/benchmarks)

Figure 2: coroutine.wrap vs recycler.wrap

Histogram based on calls and how much time it took to run the function in ms. This was created using Benchmarker.

This is based solely on calling the function. Both functions were called 10000 times.

Note

If you have the Benchmarker plugin, you can try this out yourself by going into the GitHub repo and downloading the bench file. (root/benchmarks)

Only spawn and wrap are compared, as using task.defer is considered “unsafe” in this module. delay cannot be benchmarked as there cannot be yielding.

Installation (via GitHub Releases)

Head over to releases and download the .rbxm file from the latest version. Then, insert the .rbxm file into Roblox Studio.

Require the module:

local ThreadRecycler = require(game:GetService("ReplicatedStorage").ThreadRecycler) 
local ThreadPool = ThreadRecycler.erect({ -- erect basically means to build or create
	["InitialThreadCount"] = 60, -- Initial thread count 
	["CachedLifetime"] = 60,  -- Cached lifetime 
	["EnableStatRecording"] = true, -- Enables recording of stats 
	["Logger"] = warn, -- Log method; doesn't do anything right now. Coming in the future... 
	["Debug"] = false -- Debug configuration. Doesn't do anything right now. Coming in the future... 
})

I strongly recommend that you use the documentation for more in-depth details.

License

ThreadRecycler is fully open-source and free to use with a MIT license.

Contributions

All contributions are welcome! :)

Thanks to:
@RailTypes for giving feedback and recommendations
@2jammers for recommending the Benchmarker plugin


Usage Example
local ThreadRecycler = require(game:GetService("ReplicatedStorage").ThreadRecycler)
local PoolConfig = {
	["InitialThreadCount"] = 10,
	["CachedLifetime"] = false,
	["EnableStatRecording"] = false,
	["Logger"] = warn,
	["Debug"] = false
}
local CoroutineThreadPool = ThreadRecycler.erect(PoolConfig)
local TaskThreadPool = ThreadRecycler.erect(PoolConfig) -- it would be best to keep them both separated

local function test(thread, x)
    print("Hello!")
    task.wait(1) -- should not cause any issues since wrap isn't in the task library
    print("Hello world after 1 second!")
    -- do some stuff; lets say that it may yield forever or never actually 'end'.
    CoroutineThreadPool.recycle(CoroutineThreadPool, thread) -- if so, then this would be needed to properly recycle the thread
end

TaskThreadPool.spawn(TaskThreadPool, function(thread)
    print("Hello world!")
    TaskThreadPool.wait(5) -- must be used instead of task.wait() to prevent task library issues
    print("Hello again after 5 seconds!")
end)

CoroutineThreadPool.wrap(CoroutineThreadPool, test, 1) 

--// Example of autocompatible
TaskThreadPool.autocompatible(TaskThreadPool, test, 1) -- ->  ThreadRecycler: This function supports auto-recycling!
-- If that statment never shows up, then you must manually recycle. 

(just realized fastspawn exists and works like this except with less features :sweat:)

5 Likes

still what is the benefit of using an entire library vs a portable fast spawn module

1 Like