Утечка памяти при использовании общей библиотеки с локальным хранилищем потоков через ctypes в программе python

#python #memory-leaks #ctypes #thread-local-storage

#python #утечки памяти #ctypes #поток-локальное хранилище

Вопрос:

Я использую ctypes модуль в Python для загрузки общей c-библиотеки, которая содержит локальное хранилище потоков. Это довольно большая c-библиотека с долгой историей, которую мы пытаемся сделать потокобезопасной. Библиотека содержит множество глобальных переменных и статики, поэтому нашей первоначальной стратегией обеспечения потокобезопасности было использование локального хранилища потоков. Мы хотим, чтобы наша библиотека не зависела от платформы, и компилировали и тестировали потокобезопасность как на win32, win64, так и на 64-разрядной Ubuntu. Из чистого c-процесса, похоже, не возникает никаких проблем.

Однако в python (2.6 и 2.7) в win32 и Ubuntu мы наблюдаем утечки памяти.Похоже, что локальное хранилище потоков не освобождается должным образом, когда поток python завершается. Или, по крайней мере, то, что процесс python каким-то образом не «осведомлен» о том, что память освобождена. На самом деле, та же проблема также наблюдается в c #-программе на win32, но ее нет на нашем тестовом компьютере сервера win64 (также работающем на python 2.7).

Проблема может быть воспроизведена с помощью простого игрушечного примера, подобного этому:

Создайте c-файл, содержащий (при linux/unix удалении __declspec(dllexport) ):

 #include <stdio.h>
#include <stdlib.h>
void __declspec(dllexport) Leaker(int tid){
    static __thread double leaky[1024];
    static __thread int init=0;
    if (!init){
          printf("Thread %d initializing.", tid);
          int i;
          for (i=0;i<1024;i  ) leaky[i]=i;
          init=1;}
    else
        printf("This is thread: %dn",tid);
    return;}
  

Скомпилируйте wit MINGW в Windows / gcc в Linux, как:

gcc -o leaky.dll (или leaky.so ) -shared the_file.c

В Windows мы могли бы скомпилировать с помощью Visual Studio, заменив __thread на __declspec(thread) . Однако в win32 (я полагаю, вплоть до WinXP) это не работает, если библиотека должна загружаться во время выполнения с помощью LoadLibrary .

Теперь создайте программу на Python, подобную:

 import threading, ctypes, sys, time
NRUNS=1000
KEEP_ALIVE=5
REPEAT=2
lib=ctypes.cdll.LoadLibrary("leaky.dll")
lib.Leaker.argtypes=[ctypes.c_int]
lib.Leaker.restype=None
def UseLibrary(tid,repetitions):
    for i in range(repetitions):
        lib.Leaker(tid)
        time.sleep(0.5)
def main():
    finished_threads=0
    while finished_threads<NRUNS:
        if threading.activeCount()<KEEP_ALIVE:
            finished_threads =1
            thread=threading.Thread(target=UseLibrary,args=(finished_threads,REPEAT))
            thread.start()
    while threading.activeCount()>1:
        print("Active threads: %i" %threading.activeCount())
        time.sleep(2)
    return
if __name__=="__main__":
    sys.exit(main())
  

Этого достаточно, чтобы воспроизвести ошибку. Явно импортировать сборщик мусора, выполнение collect gc.collect() при запуске каждого нового потока не помогает.

Некоторое время я думал, что проблема связана с несовместимыми средами выполнения (python, скомпилированный с Visual Studio, моя библиотека с MINGW ). Но проблема также в Ubuntu, но не на сервере win64, даже если библиотека скомпилирована с помощью MINGW .

Надеюсь, что кто-нибудь может помочь!

Приветствую, Саймон Коккендорф, Национальная служба обследования и кадастра Дании.

Комментарии:

1.пожалуйста, просмотрите известные ошибки python bugs.python.org/issue6627 bugs.python.org/issue3757

2. Не могли бы вы освободить свои переменные с утечкой при закрытии потока в C?

3. чтобы исправить это, попробуйте использовать malloc и free для инициализации и удаления массива

Ответ №1:

Похоже, что это вообще не ошибка ctypes или Python. Я могу воспроизвести ту же утечку с той же скоростью, написав только C-код.

Как ни странно, по крайней мере, в Ubuntu Linux 64, утечка происходит, если функция Leaker() с переменными __thread скомпилирована как .so и вызывается из программы с dlopen(). Этого не происходит при запуске точно такого же кода, но с обеими частями, скомпилированными вместе как обычная программа на C.

Я подозреваю, что ошибка заключается в некотором взаимодействии между динамически связанными библиотеками и локальным хранилищем потоков. Тем не менее, это выглядит как довольно серьезная ошибка (она действительно недокументирована?).

Комментарии:

1. Похоже, что этот поток подтверждает вашу теорию: sourceware.org/ml/libc-help/2011-04/msg00000.html

2. Да — и, похоже, это зависит от операционной системы. Я получаю поведение в Win XP (перенос библиотеки с помощью python или C #, и я предполагаю, что вы заметили из C) (32-разрядная версия), в Ubuntu (как 32-разрядная, так и 64-разрядная, я полагаю) — однако на сервере Windows (64-разрядная версия) я этого не вижу.

Ответ №2:

Я предполагаю, что проблема заключается в том, что не соединение с потоками является проблемой. Со страницы руководства для pthread_join:

Сбой при соединении с потоком, который можно объединить (то есть с тем, который не отсоединен), создает «поток зомби». Избегайте этого, поскольку каждый зомби-поток потребляет некоторые системные ресурсы, и когда накопится достаточное количество зомби-потоков, больше не будет возможности создавать новые потоки (или процессы).

Если вы измените свой цикл, чтобы собирать объекты потока и использовать.IsAlive() и .join () для них в этом последнем цикле while, я думаю, это должно позаботиться о вашей утечке памяти.

Комментарии:

1. Спасибо за ваш ответ. Однако, мне не кажется, что проблема не в присоединении к потокам. Даже если я присоединюсь к каждому потоку сразу после создания, так что будет только один поток, работающий отдельно от основного потока, проблема останется. Я также могу обнаружить в своей dll с помощью функции main (под Windows), что потоки отсоединяются от dll.

2. в качестве альтернативы вы можете установить setDaemon (True) в потоке перед запуском