Подтвердить что ты не робот

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

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

Однако в python (2.6 и 2.7) на win32 и на Ubuntu мы видим утечки памяти. Кажется, что локальное хранилище потоков не освобождается должным образом, когда поток python завершается. Или, по крайней мере, так или иначе процесс python не "осознает", что память освобождается. Та же проблема также встречается в С# -программе на 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: %d\n",tid);
    return;}

Скомпилируйте wit MINGW в windows/gcc на linux как:

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

В окнах мы могли бы скомпилировать с помощью 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() при запуске каждого нового потока не помогает.

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

Надеюсь, что любой может помочь!

Cheers, Саймон Кокендорф, Национальный опрос и кадастр Дании.

4b9b3361

Ответ 1

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

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

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

Ответ 2

Я предполагаю, что проблема не в соединении с потоками. На странице man для pthread_join:

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

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