A sequential loop processing 500 employee records one at a time is simple to reason about. Running that same loop across multiple threads can reduce processing time considerably, but it isn't a change to make without understanding what it affects underneath. This article covers what multithreading actually means, how it applies to a real batch-processing scenario, and the specific structural requirement that separates a safe implementation from a dangerous one.

What a Thread Is

A thread is a single sequence of instructions executing in order. Code running normally executes on one thread, with each step happening strictly after the previous one completes. Multithreading introduces more than one such sequence running concurrently, allowing independent pieces of work to proceed at the same time rather than waiting on each other sequentially.

A loop processing 500 records one at a time resembles a single queue with one person serving every customer in sequence. Dividing that work across four threads resembles opening four queues, each handling its own share of customers, completing the overall work in roughly a quarter of the time.

Why Payroll Batches Make This Especially Consequential

A salary batch combines two properties that each deserve attention on their own. It typically runs against real time pressure, processing a large volume of employee records that each require database work, which makes a speed improvement genuinely valuable. At the same time, the outcome has to be correct, and a partial, inconsistent, or duplicated save is a serious problem rather than a minor inconvenience.

This combination is exactly where multithreading requires the most discipline. The speed gain is real and worth pursuing, but it has to rest on a foundation that doesn't weaken the correctness guarantees a single-threaded, single-transaction approach already provides.

The Risk: One Shared Connection and Transaction Across Threads

A naive attempt to parallelize the batch might reuse a single database connection and transaction across every concurrent operation:

csharp

// Risky: one shared connection and transaction across multiple threads
using var connection = new OracleConnection(connectionString);
connection.Open();
using var transaction = connection.BeginTransaction();

Parallel.ForEach(employees, employee =>
{
    ProcessSalary(employee, connection, transaction); // shared across threads
});

transaction.Commit();

This is the same underlying problem examined in an earlier article about Dependency Injection lifetimes: a single shared resource, accessed by multiple concurrent operations, becomes a liability rather than an efficiency. Most database connection objects aren't designed to be used safely from multiple threads at once. Two threads writing through the same connection simultaneously can produce errors, corrupted data, or a transaction left in an undefined state. The mistake is structurally identical to a Singleton service holding shared mutable state across concurrent HTTP requests, simply relocated from request-handling code into batch-processing code.

The Fix: Isolating Each Thread's Connection and Transaction

The necessary structural change is for each thread to operate with its own connection and its own transaction, entirely separate from every other thread's:

csharp

var employeeBatches = SplitIntoBatches(employees, batchCount: 4);

Parallel.ForEach(employeeBatches, batch =>
{
    using var connection = new OracleConnection(connectionString);
    connection.Open();
    using var transaction = connection.BeginTransaction();

    try
    {
        foreach (var employee in batch)
        {
            ProcessSalary(employee, connection, transaction);
        }

        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
});

Under this structure, each thread processes its own slice of employees against an isolated connection and transaction. A rollback triggered by one thread's error affects only the records that thread was handling; employees already committed by other, independently running threads remain unaffected. One thread's failure doesn't undo another thread's completed work, and no two threads ever contend for the same connection or transaction simultaneously.

What Else Deserves Attention

Isolating connections and transactions per thread addresses the most serious failure mode, but a complete implementation needs a few additional considerations:

  • Balanced work distribution. How employees are divided across threads determines whether the workload is evenly spread; an uneven split leaves some threads idle while others remain the bottleneck.

  • Coordinated result reporting. If each thread logs its outcome independently, those results need to be gathered and reconciled into a single overall batch report afterward, rather than written to one shared log object from multiple threads without coordination.

  • A deliberate degree of parallelism. Running too many concurrent threads against the database can exhaust the connection pool or overload the database server, so the thread count should be a considered choice rather than maximized without constraint.

A Question Worth Asking Before Parallelizing Anything

Before converting any existing sequential batch into a multithreaded one, it's worth asking plainly: does every piece of shared state in this code, connections, transactions, counters, logging objects, genuinely belong to a single unit of work, or is something being reused across operations that should be independent and isolated from each other? If that question surfaces a connection or transaction shared across threads, that's the change to make before anything else, regardless of what speed improvement the rest of the parallelization might otherwise deliver.

Takeaway

Multithreading a batch process like salary generation can produce a substantial, genuine improvement in processing time, but the benefit depends entirely on isolating each thread's database work from every other thread's. Sharing a single connection and transaction across concurrent threads reproduces the same shared-state corruption risk found in other contexts, such as a Singleton DI registration holding per-request data. Giving each thread its own connection and its own transaction removes that risk, keeping each thread's success or failure fully independent of the others — exactly the guarantee a process like payroll generation cannot afford to sacrifice for the sake of speed.