import concurrent.futures
import logging
from typing import List, Callable, Any

logger = logging.getLogger(__name__)

class ParallelExecutor:
    def __init__(self, max_concurrency: int = 4):
        self.max_concurrency = max_concurrency

    def execute(self, tasks: List[Callable[[], Any]]) -> List[Any]:
        """Executes a list of tasks in parallel.

        Args:
            tasks: A list of callable functions (tasks) to execute.

        Returns:
            A list of results, in the same order as the input tasks.
        """
        results = []
        with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_concurrency) as executor:
            futures = [executor.submit(task) for task in tasks]
            for future in concurrent.futures.as_completed(futures):
                try:
                    results.append(future.result())
                except Exception as e:
                    logger.exception(f"Task failed with exception: {e}")
                    results.append(None)  # Or raise the exception, depending on desired behavior

        return results


if __name__ == '__main__':
    # Example Usage:
    import time

    def task1():
        time.sleep(1)
        print("Task 1 completed")
        return "Result 1"

    def task2():
        time.sleep(2)
        print("Task 2 completed")
        return "Result 2"

    def task3():
        time.sleep(0.5)
        print("Task 3 completed")
        return "Result 3"

    tasks = [task1, task2, task3]

    executor = ParallelExecutor(max_concurrency=2)
    results = executor.execute(tasks)

    print("Results:", results)