Thread-Safety Guarantees of mulle_allocator Functions: Default vs Custom Behavior

mulle_allocator functions inherit thread-safety from the underlying allocator implementation—the default allocator is thread-safe by delegating to libc, but custom allocators require external synchronization.

The mulle-c/mulle-allocator library provides a lightweight abstraction over memory allocation that stores function pointers in a struct mulle_allocator. Understanding the thread-safety guarantees of mulle_allocator functions is essential for concurrent applications, as the library implements no internal locking mechanisms itself.

Default Allocator Thread-Safety

The default allocator (mulle_allocator_default and mulle_allocator_stdlib) forwards all calls directly to the standard C library functions malloc, calloc, realloc, and free. According to the implementation in src/mulle-allocator.c (lines 34-52), these global instances are initialized with stdlib function pointers:

// From src/mulle-allocator.c#L34-L52
struct mulle_allocator   mulle_allocator_stdlib =
{
   .calloc   = calloc,
   .realloc  = realloc,
   .free     = free,
   .fail     = mulle_allocation_fail,
   .abafree  = NULL,
   .aba      = NULL
};

Modern C runtime libraries guarantee that malloc, free, and related functions are thread-safe (re-entrant and internally serialized). Consequently, calls to mulle_malloc() and mulle_free() are safe for concurrent use from multiple threads without additional locking.

Concurrent Usage Example

The following example demonstrates safe multi-threaded allocation using the default allocator:

#include <mulle-allocator/mulle-allocator.h>
#include <pthread.h>
#include <stdio.h>

void *thread_func(void *arg)
{
    /* Each thread allocates and frees its own block */
    void *p = mulle_malloc(1024);          // forwards to malloc()
    /* ... use the memory ... */
    mulle_free(p);                         // forwards to free()
    return NULL;
}

int main(void)
{
    const int N = 8;
    pthread_t th[N];

    for (int i = 0; i < N; ++i)
        pthread_create(&th[i], NULL, thread_func, NULL);

    for (int i = 0; i < N; ++i)
        pthread_join(th[i], NULL);

    printf("All threads completed without explicit locking.\n");
    return 0;
}

This works because mulle_malloc and mulle_free invoke the default allocator, which uses the thread-safe libc implementation.

Custom Allocator Thread-Safety Requirements

When providing a custom struct mulle_allocator, the library assumes the supplied callbacks are well-behaved. The public API functions (mulle_allocator_malloc, mulle_allocator_calloc, mulle_allocator_realloc, mulle_allocator_free) are thin inline wrappers that immediately dereference the function pointer and forward the call. Consequently, thread safety is the responsibility of the custom allocator—the wrapper provides no protection.

Implementing Thread-Safe Custom Allocators

To create a thread-safe custom allocator, you must implement your own synchronization around the allocation logic:

#include <mulle-allocator/mulle-allocator.h>
#include <pthread.h>
#include <stdlib.h>

static pthread_mutex_t my_lock = PTHREAD_MUTEX_INITIALIZER;

/* Thread-safe wrappers around the standard library */
static void *my_malloc(struct mulle_allocator *a, size_t size)
{
    (void)a;
    pthread_mutex_lock(&my_lock);
    void *p = malloc(size);
    pthread_mutex_unlock(&my_lock);
    return p;
}

static void my_free(struct mulle_allocator *a, void *ptr)
{
    (void)a;
    pthread_mutex_lock(&my_lock);
    free(ptr);
    pthread_mutex_unlock(&my_lock);
}

static void *my_calloc(struct mulle_allocator *a, size_t n, size_t size)
{
    (void)a;
    pthread_mutex_lock(&my_lock);
    void *p = calloc(n, size);
    pthread_mutex_unlock(&my_lock);
    return p;
}

static void *my_realloc(struct mulle_allocator *a, void *ptr, size_t size)
{
    (void)a;
    pthread_mutex_lock(&my_lock);
    void *p = realloc(ptr, size);
    pthread_mutex_unlock(&my_lock);
    return p;
}

/* Build the custom allocator */
static struct mulle_allocator my_threadsafe_alloc = {
    .calloc   = my_calloc,
    .realloc  = my_realloc,
    .free     = my_free,
    .fail     = mulle_allocation_fail,
    .abafree  = NULL,
    .aba      = NULL
};

void *worker(void *unused)
{
    void *p = mulle_allocator_malloc(&my_threadsafe_alloc, 256);
    /* ... */
    mulle_allocator_free(&my_threadsafe_alloc, p);
    return NULL;
}

In this implementation, the pthread_mutex explicitly serializes all allocation calls. The mulle_allocator_* API remains re-entrant because the underlying callbacks are now thread-safe.

Global Default Replacement Hazards

The global instance mulle_allocator_default can be swapped at runtime to redirect all default allocations to a custom implementation. However, this replacement operation is not thread-safe. The documentation in assets/dox/TOC.md (lines 59-62) explicitly states that replacement must be performed when no other thread is using the allocator, typically during program start-up.

#include <mulle-allocator/mulle-allocator.h>

/* Assume we have another allocator implementation `my_allocator` */
extern struct mulle_allocator my_allocator;

/* Replace the global default – do this before any other thread starts */
void install_my_allocator(void)
{
    /* Not thread-safe! Must be called when no other thread is using the allocator */
    mulle_allocator_default = my_allocator;
}

Attempting to swap the global allocator while other threads are actively allocating or freeing memory results in undefined behavior.

The abafree and aba function pointers in struct mulle_allocator provide optional hooks for ABA-problem mitigation systems (such as mulle-aba). The library makes no thread-safety claims regarding these hooks. Any safety guarantees must be provided by the external ABA system you plug in.

Summary

  • Default allocator: Thread-safe by delegation to libc malloc/free (defined in src/mulle-allocator.c, lines 34-52)
  • Custom allocators: Require explicit synchronization by the user; mulle_allocator wrappers provide no internal locking
  • Global replacement: Swapping mulle_allocator_default is not thread-safe and must occur during single-threaded initialization
  • ABA hooks: No thread-safety guarantees; dependent on external ABA implementation
  • Detection: Use mulle_allocator_is_stdlib_allocator (lines 68-73 in src/mulle-allocator.c) to verify if an allocator uses standard libc functions

Frequently Asked Questions

Is mulle_malloc thread-safe?

Yes. When using the default allocator, mulle_malloc forwards to the standard C library malloc, which is thread-safe on all modern platforms. The default allocator is defined in src/mulle-allocator.c and uses the stdlib function pointers directly.

Do I need to add locks when using a custom allocator with mulle_allocator?

Yes. If your custom allocator is not inherently thread-safe, you must provide your own synchronization (such as mutexes) around the malloc, free, calloc, and realloc callbacks. The mulle_allocator API functions are thin wrappers that do not perform any internal locking.

Can I replace the global allocator at runtime safely?

No. Replacing mulle_allocator_default at runtime is not thread-safe according to the documentation in assets/dox/TOC.md. You must perform the replacement during program initialization before spawning any threads that might use the allocator.

Are the ABA protection hooks in mulle_allocator thread-safe?

No. The abafree and aba function pointers are optional hooks for external ABA-management systems. The mulle-allocator library itself provides no thread-safety guarantees for these callbacks; safety must be implemented by the plugged-in ABA system (such as mulle-aba).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →