At our applications have jobs that are made up of many different tasks run asychronously in many different way -- thread pools, messages queued onto message brokers, remote calls -- and we needed a centralized way to report out all the tasks for a given job had completed.  With this, we could kick off other work.  

We developed a system of batching that could work for all the different ways we kick off jobs.  At the beginning of a job, the initiator registers itself as a batch and sets the actions to execute when the batch completes, then each individual task registers itself.  On completion of each task, a message is sent to a queue where a listener looks up the task in the registry and determines if there are any other tasks left to be completed.  If not, the batch is considered complete, and a list of actions are executed.