Проблемы с 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 встроенного сервера — ха.
Да он сам по себе валится если его загружать/выгружать.
Почему он это делает — пока не знаю. И, честно говоря, в данный конкретный момент даже не тянет узнавать — зачем портить себе выходные? 🙂
—
Кстати, вчера исполнился ровно один год с момента релиза третьей версии провайдера.