Skip to content

cPyMemTrace Thread Safety #8

Description

@paulross

cPyMemTrace Thread Safety

pymemtrace.cPyMemTrace up to and including version 0.6 is not thread safe.

The Problem

The CPython runtime can only have one Profile/Trace/Reference Trace callback function at any time. pymemtrace.cPyMemTrace gets over this limitation where multiple tracers are required by maintaining a linked list of callbacks. When a new one is created it is pushed onto the head of the list and takes precedence. When it goes out of scope it is popped from the list and the older one is re-registered.

For example:

from pymemtrace import cPyMemTrace

with cPyMemTrace.ReferenceTracing(filepath='A.log'):
    # Now writing to "A.log"
    with cPyMemTrace.ReferenceTracing(filepath='B.log'):
        # Writing to "A.log" is suspended.
        # Now writing to "B.log"
        pass
    # The log file "B.log" is closed.
    # Writing to "A.log" is resumed.
    pass
	# The log file "A.log" is closed.

This works fine for a single threaded process. However with multiple threads this fails.

Introducing Threading

Supposing that we have this code:

import threading

from pymemtrace import cPyMemTrace

def do_work(some_argument):
    with cPyMemTrace.ReferenceTracing() as profiler:
        profiler.write_message_to_log(f'Starting: {some_argument}')
        # Do some work...

threads = [
    threading.Thread(target=do_work, args=('Thread-A',),),
    threading.Thread(target=do_work, args=('Thread-B',),),
]
# Start each thread
for t in threads:
    t.start()
# Wait for all threads to finish
for t in threads:
    t.join()

Thread A will create a log file, say "A.log" (log file names are simplified here for clarity). The linked list of Reference Tracers will look like this:

Head Node
    |
File "A.log" ---> NULL

Then thread B creates a log file, say "B.log" The linked list of Reference Tracers now looks like this:

Head Node
    |
File "B.log" ---> File "A.log" ---> NULL

Now both threads are writing events to "B.log".

Suppose thread A completes first it will pop off the head node closing the file "B.log". Then the linked list will look like this:

Head Node
    |
File "A.log" ---> NULL

And, as thread B is still running, it is writing events to the file meant for thread A.

So both log files are corrupt:

  • "A.log" contains leading events from thread A and trailing events from thread B.
  • "B.log" has events from thread A and thread B.

Compelling a Common Log File

One solution might be that all treads write to a common, named, log file like this:

import threading

from pymemtrace import cPyMemTrace

def do_work(some_argument):
    with cPyMemTrace.ReferenceTracing(filepath='do_work.log') as profiler:
        profiler.write_message_to_log(f'Starting: {some_argument}')
        # Do some work...

threads = [
    threading.Thread(target=do_work, args=('Thread-A',),),
    threading.Thread(target=do_work, args=('Thread-B',),),
]
# Start each thread
for t in threads:
    t.start()
# Wait for all threads to finish
for t in threads:
    t.join()

Now both threads will have a separate FILE * to the same output stream but those FILE * handles can be in a different state. In particular the write pointer (found by tell()) can be different. Both FILE * handles will have a write pointer less than or equal to the file length so there is no chance of writing beyond that. However each thread can overwrite the other's output.

Solution

The complete solution to this is proposed elsewhere.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions