Трудности программирования под Windows

в 16:25, , рубрики: windows, системное программирование, метки:

Когда участвуешь в разработке достаточно сложных проектов, то написать неправильно работающий код проще простого. Вся загвоздка заключается в том, что ошибку начинаешь искать в самых темных закоулках проекта, в самых сложных его частях. При этом в голову даже и мысли не приходит, что не работать может самый простой код, основа всего проекта: framework.
В данном посте я опишу две проблемы, с которыми я столкнулся на практике: невозможность создать еще один поток и переименовать файл. Используемый язык программирования: C/C++.

Вейся, ниточка, ничего не бойся

Итак, ситуация первая.

Дано: на одном из ноутбуков в офисе (не разработчиков) во время работы программы в один прекрасный момент функция CreateThread() перестает работать с кодом возврата «Failed to create thread. Not enough storage is available to process this command.»

Стоит отметить, что когда возникла эта ошибка, на целевом ноутбуке не работала существенная часть функционала. Пока разгребались все ошибки и удаленно проверялся результат при помощи логов (установить IDE на ноутбук и отладить программу возможности не было), прошло довольно значительное время. И вот, осталась только проблема создания потока.

Как только стало ясно, что программе не хватает памяти для этой задачи (судя по коду возврата), началась охота на ведьм: подозрение начало падать на все сложные библиотеки, в коде которых сам черт ногу сломит. Этому способствовал тот факт, что когда программа использовала GPU, проблема проявлялась. Если же использовался только CPU, проблемы не было.
Естественно, мы начали тщательно смотреть на библиотеку, обеспечивающую работу с GPU. Попутно было найдено и исправлено несколько важных ошибок, приводящих к утечке памяти. Однако исходную проблему это не решило. Потоки все равно в один прекрасный момент переставали создаваться.

Еще одной загвоздкой было то, что память-то не кончалась! Диспетчер задач показывал, что программа в критический момент использовала порядка 220 МБ, тогда как ноутбук был оснащен 8 ГБ оперативной памяти.

Нагрузочное тестирование

Выяснилось, что проблема проявлялась, когда программа создавала более 90 потоков. Начали грешить на пул потоков: думали, что он начинает неограниченно создавать потоки. Было принято решение снять ограничение на количество потоков, создаваемых программой в один момент времени, и выделить, скажем, 1000 потоков. На отладочной машине вся 1000 потоков создалась на ура, а вот на целевом ноутбуке проблем стало еще больше: разваливаться начал уже GUI.

Но все надежды на то, что источник проблемы был найден, были разбиты программой TestLimit от Sysinternals: на целевом ноутбуке данная тестовая программа без проблем создавала 1000 потоков и более. Даже надежда на то, что у нас в программе каждому потоку выделяется больше памяти, чем по умолчанию, расстаяла, как легкая дымка. Ошибка все же была в нашей программе.

RTFM

Мы уже начали думать, что данная проблема связана исключительно с целевым ноутбуком и проявляется только на специфичном железе. Однако и тут осечка: если создать нового пользователя, то под ним наша программа работает без сбоев.

Когда руки уже начали опускаться, один из сотрудников еще раз (вот уже в который раз) прочитал справку по CreateThread() в MSDN. А ларчик-то просто открывался:
«If a thread created using CreateThread calls the CRT, the CRT may terminate the process in low-memory conditions (proof)

Решение проблемы: использовать _beginthreadex() для создания потоков, вместо CreateThread(). Об этом же писалось и в комментарии к страничке помощи с темой "_beginthread vs CreateThread: which should you use?"

Тут же вспомнилось, что на целевом ноутбуке действительно крутились тяжеловестные приложения, что соответствует low-memory условию. Пул потоков был переписан на использование _beginthreadex() и в итоге… проблема осталась. Но уже проявлялась не так часто. А все потому, что в некоторых частях программы потоки создавались вручную, а не брались из пула.
Когда все вызовы CreateThread() были заменены на вызовы _beginthreadex() (в том числе и в сторонних библиотеках), проблема наконец-то была решена.

Основным затруднением в решении этой задачи было то, что проблема воспроизводилась только на специфичной машине при определенном использовании. При этом все наши тесты кода оказались бесполезны, так как в данном случае требовалась еще эмуляция нагрузки на ОС.

Вы точно уверены, что хотите переименовать файл?

Ситуация вторая.

Дано: в неопределенный момент работы программы, уже после того, как файл <filename> был удален, функция проверки существования файла <filename> возвращает true.

Первым делом смотрим на функцию проверки существования файла:

bool exist( const wchar_t* filename )
{
    bool result = !_waccess( filename, 0 );
    if( result )
    {
        return true;
    }

    WIN32_FIND_DATAW attrs;
    HANDLE handle = FindFirstFileW( filename, &attrs );
    if( handle == INVALID_HANDLE_VALUE )
    {
        return false;
    }

    FindClose( handle );

    return !!attrs.dwFileAttributes;
}

Помня о проблеме с CRT, возникшей в предыдущем примере, начинаю грешить на функцию _waccess(). Однако после написания небольшого теста для создания/удаления файла становится понятно, что она здесь ни при чем.

Оказалось, что после того, как файл был удален при помощи функции _wremove(), функция FindFirstFileW() все равно может найти аттрибуты для этого файла! Получается, что для удаления аттрибутов файла драйверу файловой системы NTFS требуется какое-то ненулевое время. Следовательно, полагаться на существование аттрибутов файла в функции проверки на существование никак нельзя.
Хорошо, нет проблем. Просто удаляем эту проверку наличия аттрибутов.

Теперь функция exist() работает корректно. Проблема решена? Не тут-то было!

Не могу создать файл, насяльника!

Изначально проблема была в следующем алгоритме:

  • удалить файл <dest_file>
  • переименовать файл <source_file> в <dest_file>

а именно в функции переименования: в ней стояла описанная выше функция exist() перед началом переименовывания. После исправления алгоритм начал работать корректно. Тест для этого участка кода выглядел так:

void test()
{
    const wchar_t firstName[]  = "alice.txt";
    const wchar_t secondName[] = "bob.txt";

    int mode = _S_IREAD | _S_IWRITE;
    
    // создаем первый файл
    int handle = _wcreat( firstName, mode );
    ::close( handle );
    
    // создаем второй файл
    handle = _wcreat( secondName, mode );
    ::close( handle );
    
    while( true )
    {
        // удаляем второй файл
        _wremove( secondName );

        // проверяем, что файл удален
        assert( !exist( secondName ) );

        // переименовываем первый файл во второй
        _wrename( firstName, secondName );
        
        // переименовываем его обратно
        _wrename( secondName, firstName );

        // восстанавливаем второй файл
        handle = _wcreat( secondName, mode );
        ::close( handle );
    }
}

Однако изначальный тест выглядел так:

void test()
{
    const wchar_t fileName[]  = "test.txt";
    int mode = _S_IREAD | _S_IWRITE;
    
    while( true )
    {
        // создаем файл
        int handle = _wcreat( fileName, mode );
        ::close( handle );
        
        // проверяем, что файл создан
        assert( exist( fileName ) );

        // удаляем файл
        _wremove( fileName );
        
        // проверяем, что файл удален
        assert( !exist( fileName ));
    }
}

Итак, вначале некорректно работала функция exist(): со временем проверка на отсутствие файла выдавала некорректный результат. Мы это исправили, убрав проверку наличия аттрибутов запрашиваемого файла. И тут начала глючить функция _wcreat()! Примерно после 800-й итерации цикла (или даже раньше) она завершалась с ошибкой…

Известно, что _wcreat() является оберткой над CreateFile(). Так вот в момент ошибки последняя функция завершалась с кодом возврата «ACCESS_DENIED»! То есть, несмотря на то, что мы сказали системе удалить файл и даже проверили, что файла больше нет, на самом деле драйвер файловой системы все еще удалял файл. Отсюда и отказ в доступе.

Решение: а его мы так и не нашли. Т.к. в реальном проекте не было аналогичной ситуации с созданием/удалением одного и того же фала, то на ошибку решили забить. Примечательно, что в такой ситуации MoveFile() работает корректно, т.е. может переименовать сторонний файл в удаленный!

Эпилог

Мне нравится программировать и решать различные алгоритмические задачи. Но как же, однако, трудно решить проблему в работе программы, когда глючит сама ОС! Ну хорошо, не ОС, а библиотеки и драйверы для нее. И вот в такие моменты, когда натыкаешься на такие ошибки, на ум приходит только одна картинка:
Трудности программирования под Windows

Автор: BrainHacker

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js