LINUX.ORG.RU

Как освобождать память завершённых нитей?

 ,


0

2

Вопросы аллокаторов памяти в последнее время стали трендом, а у меня опять нарисовалась старая проблема с псевдо-утечками.

Вот кусок кода:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

void *thread_func(void *p) {
    void *ptr;
    printf("thread %p started\n", p);
    ptr = malloc(355);
    sleep(1);
    free(ptr);
    printf("thread %p terminated\n", p);
    return NULL;
}

int main(void) {
  pthread_t t[1000];
  int k;

  for (k = 0; k < 1000; k ++)
     pthread_create(&t[k], NULL, thread_func, (void *)(size_t)k);

  sleep(1000);

  for (k = 0; k < 1000; k ++)
    pthread_join(t[k], NULL);

  return 0;
}

который создаёт много нитей. У меня на 64-битной коробке эта программа выделяет 11g виртуальной памяти, которая не освобождается, когда нити завершаются.

Раньше я решал эту проблему, просто ограничивая число создаваемых нитей, но это не всегда удобно делать. Особенно в случае, если число создаваемых нитей не известно заранее. Может есть какой способ вернуть системе память, выделенную под арену для завершённой нити? Она ведь нити больше не понадобится.

Решение. Выяснилось, что pthread_detach вполне достаточно, если не создавать много нитей одновременно.

★★★★★

Последнее исправление: i-rinat (всего исправлений: 1)

Погоди, ты хочешь сказать, что память течёт после выполнения pthread_join? Или если до, то, может, тебе нужен pthread_detach?

post-factum ★★★★★
()
Последнее исправление: post-factum (всего исправлений: 1)
Ответ на: комментарий от post-factum

Или если до, то, может, тебе нужен pthread_detach?

А джоин реализовать через какой-нибудь семафор вручную.

crowbar
()

Может есть какой способ вернуть системе память, выделенную под арену для завершённой нити?

У аллокаторов в общем случае нет такого деления, все арены общие. Нужно искать/писать специальный аллокатор который будет держать для потока отдельную арену, мб что-то подобное есть в tсmalloc, но вроде он всё равно проблему не решает.

Она ведь нити больше не понадобится.

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

mashina ★★★★★
()
Ответ на: комментарий от post-factum

Погоди, ты хочешь сказать, что память течёт после выполнения pthread_join?

После pthread_join занятая виртуальная память (vsz которая) уменьшается где-то до 4g, но это всё равно как-то много. После pthread_detach ситуация такая же, как и после pthread_join.

i-rinat ★★★★★
() автор топика
Ответ на: комментарий от post-factum

pthread_detach

О, а с pthread_detach занятая память растёт значительно медленнее, с этим уже можно жить. Как выяснилось, у меня не вызывается pthread_join. Видимо, пора почитать man'ы.

i-rinat ★★★★★
() автор топика

for (k = 0; k < 1000; k ++)

Ну и вообще если захотелось делать столько много потоков, то вероятно что-то криво задизайнено в приложении.

mashina ★★★★★
()
Ответ на: комментарий от mashina

Не факт, что криво, но тогда уже лучше на GPU считать.

peregrine ★★★★★
()
Ответ на: комментарий от mashina

В коде примера всё сделано для примера, так много нитей на деле не надо. Нити нужны были для проигрывания звука через ALSA. Звуков может быть много, данные для них нужно получать, вызывая callback из сторонней библиотеки, так что проще поназапускать нитей по количеству аудиоплееров.

i-rinat ★★★★★
() автор топика
Ответ на: комментарий от post-factum

«Бац-бац и в продакшн» — наше всё.

Ситуация осложняется тем, что у меня с одной стороны браузер, а с другой — PepperFlash. Первый просто убивает процесс plugin-container, из-за чего valgrind не успевает подсчитать утечки. Если слать plugin-container'у SIGHUP, он вроде что-то находит, но там тоже всё непросто, ибо PepperFlash явно сам допускает утечки — одну переменную он точно оставляет не-unref-нутой.

i-rinat ★★★★★
() автор топика

По поводу pthread_detach: удваиваю

int k;
pthread_create(&t[k], NULL, thread_func, (void *)(size_t)k);
// -> p
printf("thread %p started\n", p);

Пальцем в небо: а попробуй вот так:

int k;
pthread_create(&t[k], NULL, thread_func, &k);
// -> p
printf("thread %d started\n", *(int *)p);

KennyMinigun ★★★★★
()
Последнее исправление: KennyMinigun (всего исправлений: 1)

Кстати, выписка из мана:

On Linux/x86-32, the default stack size for a new thread is 2 megabytes.  Under the NPTL threading implementation, if  the  RLIMIT_STACK soft  resource  limit at the time the program started has any value other than «unlimited», then it determines the default stack size of new threads.  Using pthread_attr_setstacksize(3), the stack size attribute can be explicitly set in the attr argument used to  create  a thread, in order to obtain a stack size other than the default.

KennyMinigun ★★★★★
()
Последнее исправление: KennyMinigun (всего исправлений: 1)
Ответ на: комментарий от carb_blog9

malloc_trim() - 2сек смотрения мана

Видимо, двух секунд недостаточно. sbrk() не влияет на mmap-нутые области.

PTHREAD_CREATE_DETACHED

Знал бы, где падать, соломки бы подстелил. А любая решенная задача кажется очевидной.

i-rinat ★★★★★
() автор топика
Ответ на: комментарий от KennyMinigun

Пальцем в небо: а попробуй вот так:

Что-то я не понимаю, в чём смысл. Если передавать ссылку на счётчик цикла, печататься будет непонятно что.

В любом случае, проблема решилась отцеплением нитей. Я думал, что у меня в коде нити подбираются join'ами, а оказалось, что нет.

i-rinat ★★★★★
() автор топика
Ответ на: комментарий от KennyMinigun
int k;
pthread_create(&t[k], NULL, thread_func, &k);
// -> p
printf("thread %d started\n", *(int *)p);

Ой-ой, я осознал. Прошу прощения. Ногами только не бейте

KennyMinigun ★★★★★
()
Ответ на: комментарий от i-rinat

Я думал, что у меня в коде нити подбираются join'ами, а оказалось, что нет.

Ну вообще да, только по-очереди (т.е. идёт накопление). А отсоединённые по завершению. Только вопрос: как ты будешь дожидаться завершения отсоединённых? Семафоры?

KennyMinigun ★★★★★
()
Ответ на: комментарий от kvap

join не вызывается, потому что sleep(1000) ждёт 1000 секунд.

Если сделать sleep поменьше, но таким, чтобы он дождался, пока все нити завершатся, и вставить ещё один после join'ов, выделение снизится с 11g до 4g, что тоже довольно много. Если не создавать много нитей одновременно, адресное пространство используется заново, и до 4g не доходит.

i-rinat ★★★★★
() автор топика
Ответ на: комментарий от i-rinat

Видимо, двух секунд недостаточно. sbrk() не влияет на mmap-нутые области.

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

carb_blog9
()
Ответ на: комментарий от i-rinat

Можно вообще без sleep, потому что join дожидается, пока завершится нить. Если интересует, почему во время долгого sleep *до* join памяти так много занято — так это, наверное, та тысяча стеков. После завершения нитей в памяти ядра всё равно остаются дескрипторы (и похоже, что со стеком), и join/detach нужен для их уничтожения. Если стек остаётся, то 4G как раз оправданы. Нужно попробовать создать 1000 нитей, которые ничего не делают, и посмотреть, сколько они занимают до join/detach.

kvap
()
Ответ на: комментарий от KennyMinigun

Ну вообще да, только по-очереди (т.е. идёт накопление).

Я имел в виду другой код, вот этот. В коде для примера я join'ы вставил на автомате, и был уверен, что в основном коде они тоже есть. Ошибся.

как ты будешь дожидаться завершения отсоединённых? Семафоры?

У меня оказалось три вида нитей. Первый всегда в одном экземпляре и никогда не завершается. Второй не нужно контролировать, завершился и завершился. А на завершение третьего типа я тупо сделал цикл ожидания — мне надоело отлавливать livelock'и.

i-rinat ★★★★★
() автор топика
Ответ на: комментарий от kvap

Можно вообще без sleep

Тогда я не успеваю переключиться в терминал с top'ом, чтобы посмотреть, сколько оно сожрало памяти.

i-rinat ★★★★★
() автор топика
Ответ на: комментарий от i-rinat

Чего-то я вообще не нашёл реализации malloc_trim() в jemalloc.

Ну какбэ я говорю про гнутый маллок, а не про jemalloc. Ты разве говорил про jemalloc?

Про jemalloc я ничего не знаю - я не вижу смысла в этой поделки, кроме как заткнуть дыру, когда дефолтный захлебывается и то его ценность крайне сомнительна.

А зачем тебе вообще нужен именно маллок? Тыж там говорил про алсу, зачем там маллок?

carb_blog9
()
$./pthreads 
==========================================
before pthread_create
ReadOnly portion of Libraries: Total=71.2M resident=66.0M(93%) swapped_out_or_unallocated=5268K(7%)
Writable regions: Total=17.4M written=76K(0%) resident=124K(1%) swapped_out=0K(0%) unallocated=17.3M(99%)
TOTAL                             145.0M
==========================================
==========================================
started
ReadOnly portion of Libraries: Total=71.2M resident=66.0M(93%) swapped_out_or_unallocated=5268K(7%)
Writable regions: Total=526.2M written=8068K(1%) resident=8136K(2%) swapped_out=0K(0%) unallocated=518.3M(98%)
TOTAL                             657.7M
==========================================
==========================================
join
ReadOnly portion of Libraries: Total=71.2M resident=66.0M(93%) swapped_out_or_unallocated=5268K(7%)
Writable regions: Total=18.4M written=64K(0%) resident=136K(1%) swapped_out=0K(0%) unallocated=18.3M(99%)
TOTAL                             146.0M
==========================================
$ ./pthreads 
==========================================
before pthread_create
 total             6332K
==========================================
==========================================
started
 total         10234344K
==========================================
==========================================
join
 total          2071128K
==========================================

хе-хе, хреноват glibcовский аллокатор по сравнению с os x овым, в каком-то смысле

anonymous
()
Ответ на: комментарий от i-rinat

malloc_trim()

не работает, я потрудился проверить

anonymous
()
Ответ на: комментарий от anonymous
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#ifdef __APPLE__
const char *pmap_cmd = "vmmap";
#else
const char *pmap_cmd = "pmap";
#endif

void pmap(const char *msg)
{
	char buf[128];
	printf("==========================================\n");
	printf("%s\n", msg);
	snprintf(buf, sizeof(buf), "%s %d | grep -i total", pmap_cmd, getpid());
	system(buf);
	printf("==========================================\n");
}

void *thread_func(void *p) {
	void *ptr;
	ptr = malloc(355);
	sleep(1);
	free(ptr);
	return NULL;
}

int main(void) {
	pthread_t t[1000];
	int k;

	pmap("before pthread_create");

	for (k = 0; k < 1000; k ++)
		pthread_create(&t[k], NULL, thread_func, (void *)(size_t)k);
	
	pmap("started");

	for (k = 0; k < 1000; k ++)
		pthread_join(t[k], NULL);

	pmap("join");

	return 0;
}
anonymous
()
Ответ на: комментарий от i-rinat

Попробовал программу из коммента. Хоть pmap и думает, что выделено много, на самом деле это всё виртуально, пока не заполнишь нулями. free показывает, что ничего не занято.

Переделал, добавив заполнение выделенной памяти нулями. Теперь память реально занимается (free показывает, что занято много гигабайтов). Но после завершения нитей, до join, память реально освобождается, pmap опять показывает виртуально выделенную память.

Таким образом, проблемы никакой нету.

kvap
()
Ответ на: комментарий от carb_blog9

Ну какбэ я говорю про гнутый маллок, а не про jemalloc. Ты разве говорил про jemalloc?

я реализации не выбираю, нужно работать с тем, что есть. Вот в Firefox используется jemalloc, значит плагин будет работать с jemalloc.

i-rinat ★★★★★
() автор топика
Ответ на: комментарий от post-factum

У меня нет возможности выбирать — что дадут, то и использую. Разве что GSlice из GLib из самодеятельности, чтобы основной аллокатор по мелочам не отвлекать.

i-rinat ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.