Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 68

Building a Thread-Safe Connection Pool

Recommended resources

Hello! Welcome to the next lesson in our capstone module on Concurrent Design Applications.

In our previous lesson, we dove deep into task-based parallelism by designing and implementing a lock-free work-stealing queue. We saw how a specialized data structure, combined with atomic operations, could create a highly efficient, low-contention system for dynamic load balancing.

Today, we shift our focus from managing ephemeral tasks to managing persistent, expensive resources. We will address the learning outcome: Build a thread-safe connection pool to manage a finite number of resources (e.g., database connections).

This pattern is fundamental to almost any high-performance server application. Creating resources like database connections, network sockets, or even large memory buffers is costly. A connection pool amortizes this cost by creating a fixed set of resources upfront and allowing multiple threads to borrow and return them as needed. Our goal is to design and implement the core logic for such a pool, ensuring it is robust and thread-safe.

The Core Idea: What is an Object Pool?

Before we talk specifically about connections, let's generalize. A connection pool is a specific type of object pool. The main purpose of an object pool is to reuse objects that are expensive to create. Instead of creating an object on demand and destroying it when done (like frequent malloc and free calls), we pre-allocate a "pool" of these objects. Threads can then "borrow" an object from the pool, use it, and "return" it, making it available for other threads.

The following images illustrate this concept in the context of database connections.

Database Connection Pooling Process
This diagram shows the basic flow: clients request connections from a pool, use them to access the data source, and return them to the pool for reuse, rather than creating new connections each time.
Connection Pool Management and Connection State
This visual breaks down the internal states. The pool manages a set of connections that can be idle, in use (open), or in other states. The client application interacts only with the pool, which handles the underlying resource management.

To get a more practical C-centric view of an object pool, and the motivation behind it, the following video is an excellent primer.

What is an object pool, and how to create one in C?

The video 'What is an object pool, and how to create one in C?' by Jacob Sorber provides a clear, practical introduction. It explains why object pools are used to avoid the non-determinism and fragmentation of dynamic allocation and walks through a simple C implementation.

Please watch the following segments (about 11 minutes total): Introduction (00:00 - 02:20): Explains what object pools are and the problems they solve. Initial Setup (06:01 - 08:24): Demonstrates the basic structure with a static array. Tracking Object State (08:24 - 11:15): Introduces a wrapper struct to track if an object is allocated. Borrowing Logic (11:15 - 12:50): Shows the implementation for borrowing an object. Returning Logic (13:23 - 15:26): Shows the implementation for returning an object. While this video doesn't cover thread safety, it perfectly establishes the single-threaded foundation we will build upon.


Designing a Production-Grade Connection Pool

The simple object pool from the video provides a great starting point, but a real-world connection pool must handle the chaos of dozens or hundreds of threads competing for resources. It's not just a data structure; it's a concurrency-aware resource scheduler.

To get a comprehensive overview of the components that make up a robust pool, we'll turn to an excellent article that lays out a blueprint for a production-grade system.

Building a Connection Pool from Scratch: Internals, Design...

The article 'Building a Connection Pool from Scratch' by Maneesh Chaturvedi provides a fantastic, in-depth look at the design of a modern connection pool. It covers the essential building blocks that we need to consider.

Please read the following sections to get a high-level design perspective: Introduction (from the start to "What Is a Connection Pool Really?"): This sets the stage and defines the core challenge. Section 1: Connection Factory: Understand the role of encapsulating resource creation. Section 2: Idle Queue and Borrower Queue: This is the heart of the pool. Pay attention to the discussion of data structures and coordination patterns. Section 7: Thread Safety and Concurrency Control: This section discusses the specific challenges of making the pool thread-safe, mentioning locks, condition variables, and lock-free approaches. We will be implementing a simplified version of this design, but understanding these components is crucial.


From Design to a C Implementation

Now, let's synthesize these ideas into a practical C implementation. Our connection pool needs to be thread-safe. When a thread tries to borrow a connection and all are in use, it must wait. When a thread returns a connection, it must signal one of the waiting threads that a resource is now free.

This is a classic producer-consumer problem, where "producers" are threads releasing connections and "consumers" are threads acquiring them. The ideal tools for this in pthreads are a mutex and a condition variable.

  • pthread_mutex_t: To protect the internal state of the pool (e.g., the list of connections, usage counters) from concurrent access.
  • pthread_cond_t: To allow threads to wait efficiently (without busy-looping) when no connections are available, and to be woken up when one is returned.

We'll use these to build our pool. For a quick refresher on how mutexes and condition variables work together, you can refer to the pthread Tutorial PDF (resource ID LINK), specifically sections 3.1 and 3.3.

1. Defining the Data Structures

First, we define the structures for a single connection and the pool itself. For this example, our "connection" will just be an ID and a state flag. The pool will contain an array of these connections, along with our synchronization primitives.

#include <pthread.h>
#include <stdbool.h>

// A dummy connection structure
typedef struct {
    int id;
} connection_t;

// The connection pool structure
typedef struct {
    connection_t* connections; // Array of connections
    int pool_size;
    int connections_in_use;

    bool* is_in_use; // Parallel array to track connection state

    pthread_mutex_t mutex;
    pthread_cond_t cond_var;
} connection_pool_t;

Note: Using a parallel is_in_use array is one way to track state. Another common approach is to wrap connection_t in another struct that includes the state, similar to the pool_object in the Jacob Sorber video.

2. Initialization and Destruction

We need functions to create and destroy the pool. pool_create will allocate memory and initialize the mutex and condition variable.

void pool_create(connection_pool_t* pool, int size) {
    pool->pool_size = size;
    pool->connections_in_use = 0;
    
    pool->connections = malloc(sizeof(connection_t) * size);
    pool->is_in_use = calloc(size, sizeof(bool)); // Initializes to false

    for (int i = 0; i < size; ++i) {
        pool->connections[i].id = i;
    }

    pthread_mutex_init(&pool->mutex, NULL);
    pthread_cond_init(&pool->cond_var, NULL);
}

void pool_destroy(connection_pool_t* pool) {
    free(pool->connections);
    free(pool->is_in_use);
    pthread_mutex_destroy(&pool->mutex);
    pthread_cond_destroy(&pool->cond_var);
}

3. Acquiring a Connection (pool_acquire)

This is the most critical function. A thread calls this to borrow a connection.

  1. Lock the mutex to protect the pool's state.
  2. Use a while loop to check if any connections are available. The loop is crucial to handle spurious wakeups from pthread_cond_wait.
  3. If all connections are in use, call pthread_cond_wait. This atomically unlocks the mutex and puts the thread to sleep. When woken up, it re-acquires the lock before continuing.
  4. Once a connection is available, find it, mark it as "in use", increment the usage counter, unlock the mutex, and return the connection.
connection_t* pool_acquire(connection_pool_t* pool) {
    pthread_mutex_lock(&pool->mutex);

    // Wait while the pool is full
    while (pool->connections_in_use == pool->pool_size) {
        pthread_cond_wait(&pool->cond_var, &pool->mutex);
    }

    // Find a free connection
    int conn_idx = -1;
    for (int i = 0; i < pool->pool_size; ++i) {
        if (!pool->is_in_use[i]) {
            conn_idx = i;
            break;
        }
    }

    // Mark as in use and update counter
    pool->is_in_use[conn_idx] = true;
    pool->connections_in_use++;

    pthread_mutex_unlock(&pool->mutex);

    return &pool->connections[conn_idx];
}
Test your understanding!

In pool_acquire, why is pthread_cond_wait called inside a while loop instead of a simple if statement?

Show answer

Using a while loop is essential for correctness due to spurious wakeups. A thread waiting on a condition variable can sometimes wake up even if it was not explicitly signaled. If we used an if statement, the thread would proceed assuming a connection is free, but it might not be. The while loop forces the thread to re-check the condition (pool->connections_in_use == pool->pool_size) after waking up, ensuring it only proceeds when a connection is genuinely available.

4. Releasing a Connection (pool_release)

This is the counterpart to acquire.

  1. Lock the mutex.
  2. Find the connection being returned in our array and mark it as "not in use".
  3. Decrement the usage counter.
  4. Call pthread_cond_signal to wake up one thread that might be waiting in pool_acquire.
  5. Unlock the mutex.
void pool_release(connection_pool_t* pool, connection_t* conn) {
    pthread_mutex_lock(&pool->mutex);

    // Find the connection's index to update its state
    int conn_idx = conn->id;

    // Mark as free and update counter
    pool->is_in_use[conn_idx] = false;
    pool->connections_in_use--;

    // Signal one waiting thread that a connection is now available
    pthread_cond_signal(&pool->cond_var);

    pthread_mutex_unlock(&pool->mutex);
}

Note: A more robust pool_release would validate that the pointer conn is actually a valid connection belonging to the pool. For simplicity, we use its ID to find its index.


An Alternative: Using a Semaphore

A counting semaphore is also an excellent tool for this problem. A semaphore initialized to the pool size naturally represents the number of available resources.

  • pool_acquire() would call sem_wait(). This atomically decrements the semaphore count, blocking if the count is zero.
  • pool_release() would call sem_post(). This atomically increments the semaphore count, unblocking a waiting thread if any exist.

You would still need a mutex to safely find a free slot in the connection array and update its state, but the semaphore would handle the waiting/signaling logic. This design separates the counting of resources (semaphore) from the management of the data structure (mutex).


Conclusion

In this lesson, we designed and outlined a complete implementation for a thread-safe connection pool in C. This is a powerful and widely-used pattern for managing scarce, expensive resources in a concurrent environment.

Key Takeaways:

  • A connection pool is a specific type of object pool designed to reuse expensive resources like database connections.
  • The core challenge is managing concurrent access from multiple threads safely and efficiently.
  • The combination of a mutex and a condition variable provides a classic, robust solution for protecting the pool's state and managing waiting threads.
  • The logic involves locking, checking a condition in a while loop, waiting with pthread_cond_wait, and signaling with pthread_cond_signal upon release.
  • A counting semaphore offers an elegant alternative for managing the count of available resources.

Preview of the Next Lesson:
We've now built two major concurrent components: a work-stealing queue for tasks and a connection pool for resources. In our next lesson, we will combine these ideas. We will implement a parallel map function using a thread pool to apply a function to each element of an array concurrently. This will directly leverage the kind of task-based parallelism we explored in the previous lesson.