Block the main thread until all threads finish work.

void Main()
{
    List<Thread> workerThreads = new List<Thread>();
    List<int> results = new List<int>();

    for (int i = 0; i < 5; i++)
    {
        Thread thread = new Thread(() =>
        {
            Thread.Sleep(10); // simulate work
            lock (results)
            {
                // insert some random values
                results.Add(new Random().Next(1, 10));
            }
        });

        workerThreads.Add(thread);
        thread.Start();
    }

    // Wait for all the threads to finish so that the results list is populated.
    // If a thread is already finished when Join is called, Join will return immediately.
    foreach (Thread thread in workerThreads)
    {
        thread.Join();
    }

    Debug.WriteLine("Sum of results: " + results.Sum());
}

//
// Option 2:  Using a WaitHandle (http://stackoverflow.com/a/2569478)
void Main()
{
    const int numThreads = 10;

    var resetEvent = new ManualResetEvent(false);
    int toProcess = numThreads;

    // Start workers
    for (int i = 0; i < numThreads; i++)
    {
        new Thread(() =>
        {
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId);

            // If we're the last thread, signal
            if (Interlocked.Decrement(ref toProcess) == 0)
            {
                resetEvent.Set();
            }
        }).Start();
    }

    // Wait for workers.
    resetEvent.WaitOne();

    Console.WriteLine("Finished.");
}