Go Fan-Out Pattern Challenge
This challenge focuses on implementing the fan-out pattern in Go, a common and powerful concurrency pattern. You will build a system where a single task is distributed among multiple worker goroutines, and their results are aggregated. This pattern is highly useful for parallelizing I/O-bound or CPU-bound operations to improve throughput and reduce latency.
Problem Description
Your task is to create a Go program that simulates processing a list of tasks concurrently using the fan-out pattern. The program should:
- Accept a slice of tasks: Each task can be represented by a simple string or an integer ID.
- Fan-out: Distribute these tasks to a predefined number of worker goroutines.
- Worker processing: Each worker goroutine will simulate processing a task by performing a (simulated) time-consuming operation. For simplicity, this can be done using
time.Sleep. The worker should then produce a result for that task. - Fan-in: Collect all the results from the worker goroutines into a single output channel.
- Output: The main goroutine should receive and print all the processed results.
Key Requirements:
- Use Go's built-in concurrency primitives: goroutines and channels.
- The number of worker goroutines should be configurable.
- Ensure all tasks are processed.
- Ensure no results are lost.
- Gracefully handle the closing of channels.
Expected Behavior:
The program should launch N worker goroutines, where N is the configurable number of workers. A channel will feed tasks to these workers. Each worker will pick up a task, process it, and send its result to another channel. The main goroutine will wait for all results to be collected. The order of results might not necessarily match the order of input tasks due to concurrency.
Edge Cases:
- Empty input: What happens if the input slice of tasks is empty?
- Zero workers: What happens if the number of workers is zero? (This should ideally be handled gracefully or prevented).
Examples
Example 1:
Input tasks: ["task1", "task2", "task3", "task4", "task5"]
Number of workers: 3
Output:
Processing task1... done. Result: Processed_task1
Processing task2... done. Result: Processed_task2
Processing task3... done. Result: Processed_task3
Processing task4... done. Result: Processed_task4
Processing task5... done. Result: Processed_task5
(Note: The order of "Processing taskX... done. Result: Processed_taskX" lines may vary due to concurrency.)
Explanation: The 5 tasks are distributed among 3 workers. For instance, Worker A might take "task1", Worker B "task2", and Worker C "task3". When a worker finishes, it picks up the next available task. All results are collected and printed.
Example 2:
Input tasks: [101, 102, 103, 104, 105, 106, 107, 108]
Number of workers: 4
Output:
Processing task 101... done. Result: Task 101 processed.
Processing task 102... done. Result: Task 102 processed.
Processing task 103... done. Result: Task 103 processed.
Processing task 104... done. Result: Task 104 processed.
Processing task 105... done. Result: Task 105 processed.
Processing task 106... done. Result: Task 106 processed.
Processing task 107... done. Result: Task 107 processed.
Processing task 108... done. Result: Task 108 processed.
(Note: The order of output lines may vary.)
Explanation: Similar to Example 1, tasks are fanned out to 4 workers. The simulation might involve slightly different processing times for each task, leading to a variable output order.
Example 3 (Edge Case - Empty Input):
Input tasks: []
Number of workers: 2
Output:
(No processing messages or results should appear)
Explanation: When there are no tasks, no work is done, and no output is generated.
Constraints
- The number of tasks can range from 0 to 1000.
- The number of worker goroutines can range from 1 to 20.
- The simulated processing time for each task should be between 50ms and 200ms.
- Input tasks will be a slice of integers or strings.
- The solution should be efficient and not block unnecessarily.
Notes
- Consider using a
sync.WaitGroupto ensure all worker goroutines have completed their work before the main goroutine exits. - Think about how to signal to workers that there are no more tasks to process. Closing the task input channel is a common way to do this.
- Similarly, consider how the main goroutine knows when all results have been collected. Closing the results channel after all workers are done is a good practice.
- The
fmt.Printfstatements within the worker goroutines should ideally be protected if they are writing to a shared resource (though for simple printing to stdout, it's usually fine). The key is to correctly collect results on a channel. - Your solution should be encapsulated within a function that takes the tasks and the number of workers as input and returns the collected results.