Problem

In WL we have Tasks that can runs on parallel kernels using LocalSubmit. I can create the task like this:
In[]:=
task1=LocalSubmit[1+1,HandlerFunctions-><|​​"TaskFinished"->Function[result=#]​​|>]
Out[]=
TaskObject
Task UUID: 58f92758-ff5e-424c-871b-d62f1f9c699a
Task environment: Local
Task type: Asynchronous
Evaluation expression: 1+1

And after waiting small timeout I cat extract the result of the computation:
In[]:=
result
Out[]=
EvaluationExpressionHold[1+1],EvaluationResult2,FailureMissing[NotAvailable],EventNameTaskFinished,MessageOutputMissing[NotAvailable],PrintOutputMissing[NotAvailable],TaskTaskObject
Task UUID: 58f92758-ff5e-424c-871b-d62f1f9c699a
Task environment: Local
Task type: Asynchronous
Evaluation expression: 1+1
,TaskStatusFinished,TaskTypeImmediate,TaskUUID58f92758-ff5e-424c-871b-d62f1f9c699a
I can gues how it works based powered by WSTP + ParallelKernels and some absract task manager. I know that this implementation works and is stable. But for the generationg a task needs a lot of time:
In[]:=
AbsoluteTiming[task1=LocalSubmit[1+1,HandlerFunctions-><|​​"TaskFinished"->Function[result=#]​​|>]]
Out[]=
0.929984,TaskObject
Task UUID: 653339aa-80d6-4249-aca9-ba35a7f91e1a
Task environment: Local
Task type: Asynchronous
Evaluation expression: 1+1

My goal - speed up creating of tasks as it possible.

Installation

I created the package with some internal functions that could help me in other projects. You can install the package like this:
In[]:=
PacletInstall["KirillBelov/Internal"]
Out[]=
PacletObject
Name: KirillBelov/Internal
Version: 1.0.8

And import package functions:
In[]:=
<<KirillBelov`Internal`

AsyncEvaluate

First I have to launch parallel kernels. But it is not necessary, if kernels are not started, this code will be executed when creating only the first task:
In[]:=
LaunchKernels[]
Out[]=
KernelObject
Local
KernelID: 1
,KernelObject
Local
KernelID: 2
,KernelObject
Local
KernelID: 3
,KernelObject
Local
KernelID: 4
,KernelObject
Local
KernelID: 5
,KernelObject
Local
KernelID: 6
,KernelObject
Local
KernelID: 7
,KernelObject
Local
KernelID: 8
,KernelObject
Local
KernelID: 9
,KernelObject
Local
KernelID: 10
,KernelObject
Local
KernelID: 11
,KernelObject
Local
KernelID: 12
,KernelObject
Local
KernelID: 13
,KernelObject
Local
KernelID: 14
,KernelObject
Local
KernelID: 15
,KernelObject
Local
KernelID: 16
,KernelObject
Local
KernelID: 17
,KernelObject
Local
KernelID: 18
,KernelObject
Local
KernelID: 19
,KernelObject
Local
KernelID: 20
,KernelObject
Local
KernelID: 21
,KernelObject
Local
KernelID: 22
,KernelObject
Local
KernelID: 23
,KernelObject
Local
KernelID: 24

And let’s try to evaluate some code in async mode:
In[]:=
task2=AsyncEvaluate[1+1,(result2=#)&]
Out[]=
AsynchronousTaskObject[,27,<>]
And the result is:
In[]:=
result2
Out[]=
2
Let’s check how fast is creationg of the task:
In[]:=
AbsoluteTiming[AsyncEvaluate[1+2,(result2=#)&]]
Out[]=
{0.000232,AsynchronousTaskObject[,28,<>]}
4 000 times!!! It means that whean I can create several thousand async tasks per secnd when standard built-in function Localubmit creating only one task. I have 24 physical cores and let’s compare how fast is creating of 24 tasks:
In[]:=
(*Built-inway*)​​AbsoluteTiming[Do[​​With[{$i=i},​​LocalSubmit[$i+1,HandlerFunctions-><|"TaskFinished"->((results1[$i]=#)&)|>]​​],​​{i,1,24}​​]]
Out[]=
{26.9374,Null}
In[]:=
Table[results1[i]["EvaluationResult"],{i,1,24}]
Out[]=
{2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25}
In[]:=
(*customfunction*)​​AbsoluteTiming[Do[​​With[{$i=i},​​AsyncEvaluate[$i+1,(results2[$i]=#)&]​​],​​{i,1,24}​​]]
Out[]=
{0.0379738,Null}
In[]:=
Array[results2,24]
Out[]=
{2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25}

Distribute definitions

For functions that was defined in extarnal packages or that are using function from external packages you can share definitions betwen kernels before the calculation will be start:
In[]:=
my`extrnal`package`func[x_]:=x+$KernelID
In[]:=
my`extrnal`package`func[0]
Out[]=
0
In[]:=
AsyncEvaluate[my`extrnal`package`func[0],(result3=#)&]
Out[]=
AsynchronousTaskObject[,320,<>]
0 is id of the main kernel:
In[]:=
result3
Out[]=
0
Result was released on main kernel but I can use optional argument for sharing full definition of the function:
In[]:=
AsyncEvaluate[my`extrnal`package`func[0],(result3=#)&,​​"DistributeDefinitions"->{my`extrnal`package`func}​​]
Out[]=
AsynchronousTaskObject[,295,<>]
In[]:=
result3
Out[]=
1
In[]:=
results3={};​​Do[AsyncEvaluate[my`extrnal`package`func[0],(AppendTo[results3,#])&,​​"DistributeDefinitions"->{my`extrnal`package`func}​​],{i,1,24}​​]
In[]:=
Sort@results3
Out[]=
{1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3,3,3}
That’s all I wanted to show you today, thanks for your attention! Ask any questions in the comments, as well as I am open to suggestions - if you have any problems or you want to implement something using this package, but do not know how - I will be happy to tell you about it!

CITE THIS NOTEBOOK

AsyncEvaluate: fast asynchronous tasks​
by Kirill Belov​
Wolfram Community, STAFF PICKS, December 24, 2024
​https://community.wolfram.com/groups/-/m/t/3344075