Проблемы с ICU. Подход #2
Салют всем.
Хочется двигаться вперед — к программированию по-настоящему интересных вещей, однако грехи старые проблемы тяготят сознание. В последнем выпуске провайдера (v3.9) многие из них были закрыты. Даже хочется думать, что практически все. Кроме утечек ресурсов, связанных с ICU. Я о них тут уже писал. Полгода назад. А сейчас вот предпринял повторную попытку разобраться в причинах. Надеясь, что 5-ти недельный отпуск достаточно растормозил мозги для решения этой задачи. Итак.
Описание проблемы.
Утечка дескрипторов в тестовом процессе (в котором работает провайдер).
Когда происходит
1. Многопоточная работа.
2. Есть выгрузка ICU из тестового процесса.
Кто течет?
Судя по всему — не освобождаются критические секции, которые ICU использует для собственной защиты в многопоточной среде.
Чуть меньше двух лет назад, наступив на первые грабли с ICU (icuuc30.dll), я модифицировал эту DLL — добавил DllMain с инициализацией и деинициализацией:
BOOL WINAPI DllMain(HINSTANCE const /*hInstance*/, DWORD const dwReason, LPVOID const /*lpReserved*/) { BOOL bResult=TRUE; try { switch(dwReason) { case DLL_PROCESS_ATTACH: { umtx_init(NULL); break; }//DLL_PROCESS_ATTACH case DLL_PROCESS_DETACH: { //u_cleanup(); umtx_cleanup(); break; }//DLL_PROCESS_DETACH }//switch } catch(...) { assert(false && "ICU DllMain"); bResult=FALSE; } return bResult; }//DllMain
Изначально здесь главным был вызов «umtx_init(NULL)», который монопольно инициализировал объекты синхронизации.
Вызов «umtx_cleanup()» (согласно документации для ICU) должен освободить все критические секции. И тем самым освободить дескрипторы, которые к ним привязаны. Однако дескрипторы текли и долгое время была мысль — «где-то в недрах icuuc30.dll есть еще объекты, которые жрут дескрипторы».
На днях эта мысль вместе с этой утечкой меня (в очередной раз) задрали утомили и я прошелся отладчиком по всему коду ICU, который вызывается из провайдера. Обнаружил, что в глобальных переменных сохраняются указатели на динамически выделяемую память. Ага. Значит все таки надо вызывать «u_cleanup()» — в ней есть и вызов «umtx_cleanup()» и память освобождается. Не помогло — дескрипторы все еще текут.
Вот ёлкин дрын. Полез в реализацию umtx_cleanup(). Вот её оригинальный код:
U_CFUNC UBool umtx_cleanup(void) { umtx_destroy(NULL); pMutexInitFn = NULL; pMutexDestroyFn = NULL; pMutexLockFn = NULL; pMutexUnlockFn = NULL; gMutexContext = NULL; gGlobalMutex = NULL; pIncFn = NULL; pDecFn = NULL; gIncDecMutex = NULL; #if (ICU_USE_THREADS == 1) if (gMutexPoolInitialized) { int i; for (i=0; i<MAX_MUTEXES; i++) { if (gMutexesInUse[i]) { #if defined (WIN32) DeleteCriticalSection(&gMutexes[i]); #elif defined (POSIX) && ! defined (SOLARIS_MT) pthread_mutex_destroy(&gMutexes[i]); #elif defined (SOLARIS_MT) mutex_destroy(&gMutexes[i]); #endif gMutexesInUse[i] = 0; } } } gMutexPoolInitialized = FALSE; #endif return TRUE; }
Тупо на него посмотрел.
Потом посмотрел на initGlobalMutex (в этом же C-файле), в котором gMutexes инициализируются.
Потом — на функции umtx_init/umtx_destroy, в которых идет модификация gMutexesInUse.
И понял.
gMutexes — это пул готовых критических секций.
gMutexesInUse[i] — это флаги занятости элементов из gMutexes. Когда элемент освобождается, сюда пишется 0, а сама критическая секция не освобождается.
То есть вот это вот «if (gMutexesInUse[i])» в umtx_cleanup — вообще не в тему. И разрушать критическую секцию надо по-любому. А если критическая секция «InUse», то тот кто не освободил мьютекс — ССЗБ.
Вообщем, сделал вот так:
U_CFUNC UBool umtx_cleanup(void) { umtx_destroy(NULL); pMutexInitFn = NULL; pMutexDestroyFn = NULL; pMutexLockFn = NULL; pMutexUnlockFn = NULL; gMutexContext = NULL; gGlobalMutex = NULL; pIncFn = NULL; pDecFn = NULL; gIncDecMutex = NULL; #if (ICU_USE_THREADS == 1) if (gMutexPoolInitialized) { int i; for (i=0; i<MAX_MUTEXES; i++) { U_ASSERT(gMutexesInUse[i] == 0); /*if (gMutexesInUse[i])*/ { #if defined (WIN32) DeleteCriticalSection(&gMutexes[i]); #elif defined (POSIX) && ! defined (SOLARIS_MT) pthread_mutex_destroy(&gMutexes[i]); #elif defined (SOLARIS_MT) mutex_destroy(&gMutexes[i]); #endif gMutexesInUse[i] = 0; } } } gMutexPoolInitialized = FALSE; #endif return TRUE; }
DllMain выглядит вот так
BOOL WINAPI DllMain(HINSTANCE const /*hInstance*/, DWORD const dwReason, LPVOID const /*lpReserved*/) { BOOL bResult=TRUE; try { switch(dwReason) { case DLL_PROCESS_ATTACH: { umtx_init(NULL); break; }//DLL_PROCESS_ATTACH case DLL_PROCESS_DETACH: { u_cleanup(); break; }//DLL_PROCESS_DETACH }//switch } catch(...) { assert(false && "ICU DllMain"); bResult=FALSE; } return bResult; }//DllMain
И, кажись — помогло!
Многочасовой многопоточный прогон тестов для ICU-шных кодовых страниц GB18030 и CP943C на финише выдал такие состояния тестового и серверного процесса (FB SuperClassic x64):
Сведения о серверном процессе.
Ресурсы серверного процесса.
Сведения о тестовом процессе.
Ресурсы тестового процесса.
Как видно из последней картинки — после завершения всех тестов и выгрузки всех модулей (провайдер, fbclient.dll, icuuc30.dll) и деинициализации COM у тестового процесса осталось 95 дескрипторов. Это типа реально круто. Потому что раньше было — больше тысячи.
По памяти (Private Bytes) все еще остаются вопросы. Хотя у сервера и тестового процесса — эти значения соизмеримы. Но это я еще покопаюсь в ICU.
Главное — перестали течь дескрипторы.
А теперь надо
1. взять оригинальный встроенный сервер FB c его оригинальным ICU
2. провайдер+модифицированное ICU
И прогнать эти же тесты в многопоточном режиме с периодической выгрузкой встроенного сервера из памяти процесса. Сервер — будет работать со своим ICU, провайдер — со своим.
Хотя оно и так понятно, что будут течь и дескрипторы и память. Но надо убедиться 🙂
Kovalenko on 27 августа, 2011
Протестировать ICU встроенного сервера — ха.
Да он сам по себе валится если его загружать/выгружать.
Почему он это делает — пока не знаю. И, честно говоря, в данный конкретный момент даже не тянет узнавать — зачем портить себе выходные? 🙂
—
Кстати, вчера исполнился ровно один год с момента релиза третьей версии провайдера.