I finally managed to solve the problem in Python. It's not perfect but it works:

Class User.Timer Extends %RegisteredObject
{

Property Executor [ Private ];

Method Initialize(maxWorkers As %Integer = 4) [ Language = python ]
{
import concurrent.futures
import time
import threading

self.Executor = concurrent.futures.ThreadPoolExecutor(max_workers=maxWorkers)
}

Method Close() [ Language = python ]
{
if self.Executor:
    self.Executor.shutdown()
}

Method Greet(name)
{
    Write "Hello ", name, !
}

Method OnCallback0(methodName As %String) [ Private ]
{
    Do $METHOD(instance, methodName)
}

Method OnCallback1(instance As %RegisteredObject, method As %String, arg1) [ Private ]
{
    Do $METHOD(instance, method, arg1)
}

Method OnCallback2(instance As %RegisteredObject, method As %String, arg1, arg2) [ Private ]
{
    Do $METHOD(instance, method, arg1, arg2)
}

Method OnCallback3(instance As %RegisteredObject, method As %String, arg1, arg2, arg3) [ Private ]
{
    Do $METHOD(instance, method, arg1, arg2, arg3)
}

Method OnCallback4(instance As %RegisteredObject, method As %String, arg1, arg2, arg3, arg4) [ Private ]
{
    Do $METHOD(instance, method, arg1, arg2, arg3, arg4)
}

Method OnCallback5(instance As %RegisteredObject, method As %String, arg1, arg2, arg3, arg4, arg5) [ Private ]
{
    Do $METHOD(instance, method, arg1, arg2, arg3, arg4, arg5)
}

Method InternalRun(delayMs As %Integer, wait As %Boolean, instance As %RegisteredObject, method As %String, args... As %List) [ Internal, Language = python ]
{
import time
import iris

if not self.Executor:
    raise Exception("The 'Initialize' method has not been called.")

def worker_function():
    time.sleep(delayMs / 1000)
    if len(args) == 0:
        self.OnCallback0(instance, method)
    elif len(args) == 1:
        self.OnCallback1(instance, method, args[0])
    elif len(args) == 2:
        self.OnCallback2(instance, method, args[0], args[1])
    elif len(args) == 3:
        self.OnCallback3(instance, method, args[0], args[1], args[2])
    elif len(args) == 4:
        self.OnCallback4(instance, method, args[0], args[1], args[2], args[3])
    elif len(args) == 5:
        self.OnCallback5(instance, method, args[0], args[1], args[2], args[3], args[4])
    else:
        raise Exception("Too many arguments.")
    return 0


future = self.Executor.submit(worker_function)

# wait == 0 means fire-and-forget
try:
    if (wait == 1):
        rv = future.result()

except Exception as e:
    print(f"{e}")
}

/// delayMs  - the parameter specifies the timer delay in milliseconds
/// wait     - if the parameter is false, the process will not wait for the Future result to be returned (fire-and-forget)
/// instance - any object which method should be called with a delay
/// method   - specifies the object's callback method name
/// args     - the callback method arguments (up to 5)
Method Run(delayMs As %Integer, wait As %Boolean, instance As %RegisteredObject, method As %String, args... As %List)
{
    Do ..InternalRun(delayMs, wait, instance, method, args...)
}

ClassMethod Test()
{
    Set obj = ##class(Timer).%New()
    Do obj.Initialize()
    Do obj.Run(1000, 0, obj, "Greet", "John")
    Do obj.Run(2000, 0, obj, "Greet", "Jessica")
    Write "If 'wait == 0' this line will be printed first", !
    Do obj.Close()
}

}

Here it is (implemented as Mixin):

Class MyNamespace.Pooled Extends Ens.Host [ Abstract ]
{

Property PoolIndex As %Integer [ Calculated ];

Method PoolIndexGet() As %Integer
{
    #Dim cn as %String
    Set cn = ..%ConfigName  

    #Dim statement as %SQL.Statement
    Set statement = ##class(%SQL.Statement).%New()
    Set status = statement.%PrepareClassQuery("Ens.Job","Enumerate")
    $$$ThrowOnError(status)

    #Dim rs as %SQL.StatementResult
    Set rs = statement.%Execute()
    #Dim i as %Integer = -1

    While (rs.%Next()) 
    {
        #Dim jobId as %String 
        Set jobId = rs.%Get("Job")
        If (rs.%Get("ConfigName") = cn)
        {
            Set i = i + 1
            If (jobId = $JOB) 
            {
                Kill rs
                Return i
            }
        }
    }
    Kill rs
    Return i
}

Property PoolSize As %Integer [Calculated];

Method PoolSizeGet() As %Integer
{
    #Dim cn as %String
    Set cn = ..%ConfigName
    #Dim statement as %SQL.Statement
    Set statement = ##class(%SQL.Statement).%New()
    Set status = statement.%PrepareClassQuery("Ens.Job","Enumerate")
    $$$ThrowOnError(status)

    #Dim rs as %SQL.StatementResult
    Set rs = statement.%Execute()
    #Dim i as %Integer = 0

    While (rs.%Next()) 
    {
        If (rs.%Get("ConfigName") = cn)
        {
            Set i = i + 1
        }
    }
    Kill rs
    Return i
}

}