Process bunches of records in seperate threads and wait for them all to finish I feel like there is a better way of writing this code which waits for multiple threads to finish their work (i.e. without using two loops), but my colleagues and I are drawing a blank.

private static void ProcessRecords(long[] recordIds)
{
    var threads = new List&lt;Thread&gt;();
    var recordsPerThread = recordIds.Length / Settings.Default.WorkerThreads + 1;

    for (var i = 0; i &lt; Settings.Default.WorkerThreads; i++)
    {
        var t = new Thread(new ParameterizedThreadStart(Program.SetOwner));
        threads.Add(t);
        t.Start(recordIds.Skip(i * recordsPerThread).Take(recordsPerThread));
    }

    // Wait until all threads have completed
    foreach (var t in threads)
    {
        t.Join();
    }
}

private static void SetOwner(object o)
{
    var recordIds = o as IEnumerable&lt;long&gt;;

    using (Database d = Program.GetDatabase())
    {
        Account owner = Program.GetOwner(d);

        foreach (long id in recordIds)
        {
            try
            {
                var record = d.GetRecordById(id);
                record.Owner = owner;
                record.Save();
            }
            catch (Exception ex)
            {
                // code to log error
            }
        }
    }
}
So your original approach looked something like this:

recordIds.AsParallel()
    .WithDegreeOfParallelism(...)
    .ForAll(id =&gt; SetOwner(id));


which involved a separate call to SetOwner for each record. Because SetOwner has a lot of overhead per call, it's indeed much better to let it operate on batches rather than individual items. But you don't need to use threads for that directly:

recordIds
    .SplitIntoBatches(Settings.Default.WorkerThreads)
    .AsParallel()
    .WithDegreeOfParallelism(Settings.Default.WorkerThreads)
    .ForAll(batch =&gt; SetOwner(batch));


public static IEnumerable&lt;IEnumerable&lt;T&gt;&gt; SplitIntoBatches&lt;T&gt;(this IEnumerable&lt;T&gt; items, int numberOfBatches)
{
    var batchSize = (items.Count() + numberOfBatches - 1) / numberOfBatches;
    for (int i = 0; i &lt; numberOfBatches; i++)
        yield return items.Skip(i * batchSize).Take(batchSize);
}