There are times when you need Microsoft Dynamics 365 for Finance and Operations to process records as a background process. SysOperation Framework batch jobs were created for this. However, sometimes even one instance of these jobs do not process records fast enough. Learn how to create multithreading in D365 batch jobs and process records in parallel.
Example Use Cases
Most of the time, creating a single SysOperation Framework batch job is sufficient. See my articles on how to write SysOperation Framework jobs in D365.
However, there are some scenarios where a single thread of the processes cannot do the work fast enough. In those cases multithreading in D365 batch jobs can process records even faster.
For example, perhaps you are receiving thousands of orders on your ecommerce website. Then, the order information is being sent into D365 and stored in a staging table. Now, you need to loop through each record, and create sale orders and customers.
Depending on how many orders you receive a minute, a single thread may not be able to do all of the work needed to create that many sales orders.
Again, most of the time, a single thread is quite capable of handling a lot of work. Especially if that work can be done over a longer period of time. Such as throughout an entire day. Or, if the batch job can be scheduled to run at night, when new work is not coming in.
But again, there are some scenarios, where the volume of work to be done is just too great. In those cases multithreading in D365 batch jobs can be great.
References
Before continuing, I wanted to acknowledge there are are some truly excellent articles out there related to this topic. See Batch Parallelism in AX Part I, II, and III. These however, were written over a decade ago for AX 2009 and 2012 on the RunBaseBatch model.
Next, Val wrote an awesome LinkedIn article in 2012 that covered this topic for SysOperationFramework.
Inspired by those, I wanted to write an article focusing on the teaching how to create multithreading in D365 batch jobs. And, focus on what I have found the most common used type to use: Top Picking.
Work Items
Before being able to use multithreading in D365 batch jobs, you need to consider how the work can be split up into autonomous pieces, or ‘work items’. If the entire task cannot be split up into pieces that are autonomous, not dependent on other pieces, this means they cannot run at the same time in parallel.
Using my example from earlier, I can have a batch job process one staging table record and have it create one sales order in parallel. However, I could NOT easily break it down further, and have the batch job create parts of the sales order in parallel, because those parts are dependent on those parts being created in a certain order.
Therefore, in my example, I would identify each record in a my staging table as a ‘work item’ that can be distributed across many processor threads.
Approaches To Multithreading
After you have identified your ‘work item’, there are three common ways to distribute those work items for multithreading in D365 batch jobs.
Because the previous articles I referenced go into detail, I will just provide a brief description here. As well as some of the challenges with each approach.
Individual Task Modeling – Create a separate task, or thread, for each ‘work item’. This assumes you have very few work items. Otherwise, the number of threads created adds a lot of overhead.
Batch Bundling – Identify a group, or bundle, of work items. Then, assign that bundle to one of a set list of tasks to process. A possible downside to this approach is that if the time needed to process each bundle is not the same, then you could have tasks just sitting idle. Consider, even if you create a bundle of 1000 sales orders each, some sales orders may have many more sales lines than others. Or other factors that make the processing time of each ‘work item’ not the same.
Top Picking – Utilize a field to track the status of each ‘work item‘. Then, allow each task, or thread, to process the next ‘work item‘, that has not been processed yet.
In this article, I will show you how to create the multithreading in D365 batch jobs using the Top Picking approach. While this does require a status field, or staging table to work, I have found that the benefits are worth it.
Specifically, having this status field helps when troubleshooting your batch job. Additionally, I really like that with this approach you do not need to worry about uneven workloads.
Top Picking Components
When implementing multithreading in D365 batch jobs, using the top picking approach, we need three components.
First, we need a SysOperation Framework batch job that will create the tasks. This job will create one task for each number of thread we want to be running in parallel. Then, it will be finished. So it likely will only take a few seconds to run. However, we need this to be a batch job, so that we can schedule it to run on a recurrence. We will create a an action menu item so that users can schedule this batch job.
Second, we need another SysOperation Framework batch job that each task runs. This job will handle getting the next record that should be processed from the staging table. It is make sure it does not select the same record as any other task/thread that is running in parallel.
Importantly, this second SysOperation Framework batch job is NOT one that you need a menu item for. A user will NOT schedule it from the menu. It just needs to be a SysOperation Framework batch job, so that the FIRST job can run it as a task.
Thirdly, you need a class that can process ONE ‘work item’. In this example, we have defined a ‘work item’ as a ‘staging table record’. In the second batch job, you need to call the code to process a work item. However, I find it to best to put this code in its own separate class, and call it from within this job. This helps ensure everything done on one ‘work item’ is standalone and autonomous from the other records.
Now that we understand the components, let us build the code.
Create Tasks Batch Job
As a reminder, this first SysOperation Framework batch job is just responsible for creating multiple Task (i.e. a thread). In this example, I have created a parameter to allow users to specify how many threads they want running in parallel.
First, in Visual Studio, create a new class named MultiThreadingCreateTasksContract. This class adds a parameter to the dialog form, allowing the user to specify how many threads to run.
[DataContract]
public class MultiThreadingCreateTasksContract
{
int numberOfThreads;
[DataMember]
[SysOperationLabel(literalStr("Number of threads"))]
public int parmNumberOfThreads(int _numberOfThreads = numberOfThreads)
{
numberOfThreads = _numberOfThreads;
return numberOfThreads;
}
}
Second, create a class named MultiThreadingCreateTasksController. This class sets up needed components, and tells the system which method to run. In this case the ‘process’ method on the MultiThreadingCreateTasksService class.
Public class MultiThreadingCreateTasksController extends SysOperationServiceController
{
protected void new()
{
super(classStr(MultiThreadingCreateTasksService),
methodStr(MultiThreadingCreateTasksService, process), SysOperationExecutionMode::Synchronous);
}
public ClassDescription defaultCaption()
{
return "Create multithreading tasks";
}
public static MultiThreadingCreateTasksController construct(SysOperationExecutionMode _executionMode = SysOperationExecutionMode::Synchronous)
{
MultiThreadingCreateTasksController controller = new MultiThreadingCreateTasksController();
controller.parmExecutionMode(_executionMode);
return controller;
}
public static void main(Args _args)
{
MultiThreadingCreateTasksController multiThreadingCreateTasksController = MultiThreadingCreateTasksController::construct();
multiThreadingCreateTasksController.parmArgs(_args);
multiThreadingCreateTasksController.startOperation();
}
}
Thirdly, create a new class called MultiThreadingCreateTasksService. This code creates multiple threads until they match the number specified by the user on the parameter form.
public class MultiThreadingCreateTasksService extends SysOperationServiceBase
{
public void process(MultiThreadingCreateTasksContract _contract)
{
BatchHeader batchHeader;
SysOperationServiceController controller;
StagingTable stagingTable;
int totalNumberOfTasksNeeded = _contract.parmNumberOfThreads();
int threadCount;
Args args = new Args();
select count(RecId) from stagingTable where
stagingTable.ProcessingStatus == ProcessingStatus::ToBeProcessed;
if (stagingTable.RecId > 0)
{
update_recordset stagingTable
setting ProcessingStatus = ProcessingStatus::InProcessing
where stagingTable.ProcessingStatus == ProcessingStatus::ToBeProcessed;
//batchHeader = BatchHeader::construct();
batchHeader = this.getCurrentBatchHeader();
//Creating tasks
for(threadCount = 1; threadCount <= totalNumberOfTasksNeeded; threadCount++)
{
controller = MultiThreadingGetWorkItemController::construct();
batchHeader.addTask(controller);
}
batchHeader.save();
}
}
}
Fourthly, create an action menu item that calls the *Controller class. And, add that menu item to an existing Menu. See this article for more detailed instructions.
In Processing Status
Before moving on, I wanted to explain an important piece of code in the service class above. It calls this code:
update_recordset stagingTable
setting ProcessingStatus = ProcessingStatus::InProcessing
where stagingTable.ProcessingStatus == ProcessingStatus::ToBeProcessed;
The ProcessingStatus field on the staging table has four possible values.
Typically, you do not need the ‘In Processing’ status. The system would simply loop through each record in a status of ‘To Be Processed‘. Then, when it is finished working on that record, the system would change the status to ‘Processed‘. However, there are some scenarios where this does not work very well.
Example Scenario
For example, imagine you have 5 tasks (or threads) processing 1000 staged records. At first, each of the five threads will likely have some records to process. But then at some point, imagine two of the threads check to see if there are more records to process, only to find none. So, those two tasks stop running.
Then, while the remaining three tasks are finishing their last records, another 1000 records are inserted into the staging table.
The three remaining tasks continue to process the newly created 1000 records. But the two tasks that stopped are done. They are not sitting idle waiting for more records.
This could repeat, and now you only have three out of the original five threads working. As you can see, this causes a reduction in performance.
Fixing Stopped Tasks
Therefore, in order to fix this issue, you need to have an intermediate status value. In this example, the code will move all records currently in the system to an ‘in processing‘ status.
Now, the system will only process the records it has at the start of the job. Once all of those records are processed, all of the tasks will stop. Then, on the next recurrence of the batch job, it will pick up all of the new records.
While this could result in some time where the tasks are not working, as long as the recurrence is set to be frequent, this should not be long. Additionally, this approach will record he start and stop times it takes to process the number of records available to process at the start of the batch job.
Configure Maximum Number Of Batch Threads
It is important to note that users can set the maximum number of batch threads allowed to run at once.
First, go to System adminstration>Setup>Server configuration.
Second, set the Maximum batch threads field to as high as 16. More than 16 can have negative performance consequences. See Microsoft’s documentation here.
Create Get Work Items Batch Job
Next, to continue implementing multithreading in D365 batch jobs, we need a second SysOperation framework class. This class will is called by each task. Specifically, it will handle getting a work item, processing it, then repeating.
First, create a class named MultiThreadingGetWorkItemController. Similar to before, this sets up some components, and tells the system what method to call. In this case, the ‘process’ method on the MultiThreadingGetWorkItemService class.
public class MultiThreadingGetWorkItemController extends SysOperationServiceController
{
protected void new()
{
super(classStr(MultiThreadingGetWorkItemService),
methodStr(MultiThreadingGetWorkItemService, process), SysOperationExecutionMode::Synchronous);
}
public ClassDescription defaultCaption()
{
return "Get work item";
}
public static MultiThreadingGetWorkItemController construct(SysOperationExecutionMode _executionMode = SysOperationExecutionMode::Synchronous)
{
MultiThreadingGetWorkItemController controller = new MultiThreadingGetWorkItemController();
controller.parmExecutionMode(_executionMode);
return controller;
}
public static void main(Args _args)
{
MultiThreadingGetWorkItemController multiThreadingGetWorkItemController = MultiThreadingGetWorkItemController::construct();
MultiThreadingGetWorkItemController.parmArgs(_args);
MultiThreadingGetWorkItemController.startOperation();
}
}
Second, create the class MultiThreadingGetWorkItemService:
public class MultiThreadingGetWorkItemService extends SysOperationServiceBase
{
public void process()
{
StagingTable stagingTable;
stagingTable.readPast(true);
do
{
try
{
ttsBegin;
select pessimisticlock firstOnly stagingTable
where stagingTable.ProcessingStatus == ProcessingStatus::InProcessing;
if (stagingTable)
{
ProcessStagingTable::processStagingTableRecord(stagingTable);
stagingTable.ProcessingStatus = ProcessingStatus::Processed;
stagingTable.update();
}
ttscommit;
}
catch
{
//revert data
ttsabort;
//log errors
}
}
while (stagingTable);
}
}
Pessimistic lock and ReadPast
Next, there are four pieces in the previous that are important to understand. Otherwise, the multithreading will not work.
First, the key to implementing multithreading in D365 batch jobs with top picking is that each task needs to get a new record that the other tasks have not processed yet.
The ‘pessimistic‘ keyword marks the record as locked, and other processes cannot update this record. The ‘firstOnly‘ keyword gets one record from the table.
select pessimisticlock firstOnly stagingTable
where stagingTable.ProcessingStatus == ProcessingStatus::InProcessing;
Second, how does the system ensure each task is selecting a DIFFERENT record from the staging table? Won’t all the tasks just select the same record?
Interestingly, passing true into the readPast method on the table buffer, will cause the system to skip rows that are locked by other processes, when the record is read.
Thirdly, once a staging table record is selected, the record (or work item identifier) is passed to an third class to process the information. While strictly speaking a third class is not necessary, it helps ensure that the process of that work item is standalone and autonomous.
ProcessStagingTable::processStagingTableRecord(stagingTable);
If you cannot write the code needed all in that separate class, this is a good indication you need to redefine what ‘work item’ you are processing.
Fourthly, this batch job needs to update the status on the staging table record to be ‘Processed’. This ensures that the record is not accidentally processed more than once.
Process Record Class
Finally, create a separate class that is able to process a single work item, or in this case staging table record. This class should NOT be a SysOperation framework class. See this class as an example.
public class ProcessStagingTable
{
public static void processStagingTableRecord(StagingTable _stagingTable)
{
//process the data in the record.
info(strfmt("Value is %1", _stagingTable.Value));
sleep(5000);
}
}
This code sleeps for 5 seconds to simulate some time consuming process.
If we see that the time it takes to 100 records takes less than (100 * 5) = 500 seconds, then we know our multithreading in D365 batch jobs is working.
Demonstration
To test the job, write a runnable class (job), or run your own process, to insert records into your staging table. In this example, I inserted 100 records for testing purposes.
class CreateStagingRecords
{
/// <summary>
/// Runs the class with the specified arguments.
/// </summary>
/// <param name = "_args">The specified arguments.</param>
public static void main(Args _args)
{
int i;
StagingTable stagingTable;
for (i=0; i <= 100; i++)
{
stagingTable.ProcessingStatus = ProcessingStatus::ToBeProcessed;
stagingTable.Value = i;
stagingTable.insert();
}
Info("done");
}
}
Second, run the batch job that creates the threads, and specify 8 as the number of threads.
Third, go to the System Administration>Inquiries>Batch jobs form. Then, locate the job with description ‘Get work item’.
Next, click on the job ID blue text to drill in. Notice, there are eight tasks executing.
When there are not more records to process, the tasks will change to the status of ‘Ended‘.
Fourth, look at the start and end date and time of the tasks. Overall, the tasks took 66 seconds between when the first one started, and the last one finished. Obviously, this is much less than the 500 seconds than if the job was running in one thread.
To be specific, if we take 100 records, that each take 5 seconds to process, that is 500 seconds. Now, if we divide that work among the 8 tasks, 500 divided by 8 is 62.5. Therefore, the entire process now taking 66 seconds makes sense and is what is expected.
Conclusion
In many cases, I think people often think they need multithreading in D365 batch jobs without evaluating first. Most of the time a SysOperation framework job can process data with sufficient performance. If there is a need for increased performance, first check if adding an index to a table might speed things up. Or, consider rewriting a ‘while select’ loop to use a set based operation like update_recordset. That said, if you really need the performance of multiple threads, I hope you will find this article useful in showing you how to create a batch job that can utilize more system resources, and run really fast!
Hello Peter,
Great content thank you so much.
i’ve followed it all and it works so fine.
only one thing i’m still in doubt about, is that only one thread is working while all the other threads are just there doing nothing.
i have multiple records on a table that needs to be processed, but the time it takes to process one record is the same as it used to be when i had my work done in normal sysoperations framework (one thread).
another question, should the method be static in processStagingTable ? i wrote my code in normal non static form in an outer class and it workds just fine.
Hi Peter,
here is some Code missing.
[SysOperationLabel(literalStr(“Number of threads”))]
“DataMember,” should before sysOperationLabel.
Thanks!
Hi Peter,
Thanks for the article!!
I have understood that if a user creates more threads then specified on the batch server then the access threads will wait for their turn to execute. In my case all threads start executing in parallel and only first few threads completes data processing leaving other threads to do nothing.
Following is happening:
1. There are 8 threads specified on a batch AOS in a DEV VM.
2. I created 20 threads using code for multi-threading operations. I thought first 8 will be in executing state simultaneously by keeping the other 12 threads in waiting state. However this is not happening.
3. All 20 threads went into executing state at the same time. First 8 threads did the job and went into End state with remaining 12 doing nothing but still went into End state.
Could you please suggest why this is happening? Here is my code in controller class:
while (listEnumerator.moveNext())
{
DataCon = listEnumerator.current();
controller = new DataController(classstr(DataServiceClass),
methodStr(DataServiceClass, run),
SysOperationExecutionMode::Synchronous);
contract = controller.getDataContractInfoObject().dataContractObject() as DataContractClass
taskCnt ++;
taskDesc = strFmt(“@YdmLabel:MultithreadTask”, taskCnt);
controller.parmDialogCaption(taskDesc);
batchHeader.addRuntimeTask(controller, this.parmCurrentBatch().RecId);
controller = null;
contract = null;
}
batchHeader.save();
Can I run a report through multithreading ? If you can share some light on that.
On a custom report which I am running batch I am getting System.ServiceModel.CommunicationException: The socket connection was aborted.
My RDP base class is SRSReportDataProviderPreProcessTempDB. Updated InMemory to TempDB.
It’s in Controller class. Also Output Menu points to the Controller class.
If you are getting that error because the report it taking too long to run the query you would need to find a way to make it run faster. You can’t multi-thread it but you do have some options. You can add more parameters so a smaller set of data is queried. You can run a trace parser to confirm where the slowness is, then look to add indexes to make your query run faster. If you have a lot of logic running to return the results, you could use a batch job to populate a staging table with the final calculated results. Then have your report work directly off of that final staging table. I hope that helps.