Hone logo
Hone
Problems

Concurrent Counter and Aggregator

This challenge focuses on building thread-safe data structures in Rust. You will create a concurrent counter that can be incremented by multiple threads simultaneously and a concurrent aggregator that sums up values from different threads. This is crucial for scenarios where multiple parts of an application need to update shared state without data races.

Problem Description

You are tasked with implementing two key concurrent data structures in Rust:

  1. ConcurrentCounter: A structure that holds an integer and allows multiple threads to increment its value safely.
  2. ConcurrentAggregator: A structure that holds a collection of integers and allows multiple threads to add new integers to it safely.

Key Requirements:

  • ConcurrentCounter:
    • Must be initialized with a starting value (defaulting to 0).
    • Must provide a increment() method that atomically increases the counter's value by 1.
    • Must provide a get() method to retrieve the current value of the counter.
  • ConcurrentAggregator:
    • Must be initialized as an empty collection.
    • Must provide an add(value: i32) method that safely adds an integer to the collection.
    • Must provide a sum() method that returns the sum of all integers currently in the collection.

Expected Behavior:

When multiple threads call increment() on a ConcurrentCounter or add() on a ConcurrentAggregator, the operations should be performed atomically, preventing race conditions and ensuring the final state is consistent. The get() and sum() methods should reflect the most up-to-date state.

Edge Cases to Consider:

  • What happens if increment() or add() are called an extremely large number of times?
  • What if sum() is called while other threads are actively adding values?

Examples

Example 1: Concurrent Counter

// Assume we have a ConcurrentCounter initialized to 0.
// Ten threads are spawned, and each thread calls increment() 1000 times.

let counter = ConcurrentCounter::new(); // Starts at 0

// ... (code to spawn 10 threads, each incrementing 1000 times) ...

// After all threads complete, the get() method should return 10000.

Output:

10000

Explanation:

Each of the 10 threads increments the counter 1000 times. Without proper synchronization, there would be lost updates. With a ConcurrentCounter, all 10,000 increments are accounted for, resulting in a final value of 10,000.

Example 2: Concurrent Aggregator

// Assume we have a ConcurrentAggregator initialized empty.
// Three threads are spawned.
// Thread 1 adds numbers 1 to 10.
// Thread 2 adds numbers 11 to 20.
// Thread 3 adds numbers 21 to 30.

let aggregator = ConcurrentAggregator::new();

// ... (code to spawn 3 threads, each adding their respective ranges) ...

// After all threads complete, the sum() method should return the sum of 1 to 30.
let total_sum: i32 = (1..=30).sum(); // 465

// The sum() method should return 465.

Output:

465

Explanation:

Each thread adds a distinct set of numbers to the aggregator. The add() method ensures that no values are lost or duplicated during concurrent access. The sum() method then correctly calculates the total sum of all numbers added by all threads.

Example 3: Mixed Operations

// ConcurrentCounter starts at 0.
// Thread 1 increments 500 times.
// Thread 2 calls get() and prints the value (likely 0 or 500, depending on timing).
// Thread 3 increments 500 times.
// Thread 4 calls get() and prints the value (likely close to 1000).

let counter = Arc::new(Mutex::new(ConcurrentCounter::new())); // For demonstration purposes

// ... (code to spawn threads with mixed increment and get calls) ...

// After all threads complete, the counter should be 1000.

Output:

(Interleaved output depending on thread scheduling, but final value is 1000)

0
500
...
1000

Explanation:

This example highlights the importance of thread-safe access. Even if get() is called mid-operation, it retrieves the current, consistent value. The final value after all increments demonstrates the correctness of the concurrent counter.

Constraints

  • The counter value and aggregated numbers will fit within i32.
  • You will need to spawn at least 10 threads for testing purposes to adequately demonstrate concurrency.
  • Your solution should use standard Rust concurrency primitives (std::thread, std::sync::Arc, std::sync::Mutex, or other appropriate tools).
  • Avoid using external crates for synchronization unless specifically allowed.

Notes

  • Consider how to share mutable state across threads safely. Arc (Atomic Reference Counting) and Mutex are common tools for this.
  • Think about the critical sections of your code where shared data is accessed and modified. These sections need to be protected.
  • The get() method of ConcurrentCounter should be able to be called concurrently with increment() without causing data races.
  • Similarly, sum() should be callable concurrently with add().
  • For the aggregator, consider what underlying data structure you'll use to store the numbers and how to make its operations thread-safe.
Loading editor...
rust