Pull to refresh

Comments 113

Именно такая картинка в ум и приходит… :)
Идеальных программ нет. В том числе и ос с их api
«Самая лучшая ОС — та, которой нет»…
…Лучше сказать: «Лучшая ОС — та, которая не написана».
>>Об этом же писалось и в комментарии к страничке помощи с темой "_beginthread vs CreateThread
Об этом пишется где-только можно — не использовать CreateThread если пользуетесь CRT.
Странные у вас разработчики =)
Поддерживаю, у нас в универе на втором курсе по рукам били за CreateThread. С этой функцией в коде лабораторки даже допуск к экзамену не давали. А у вас люди, которые этого не знают пишут «сложные проекты».
Да нет. Просто framework писался очень давно. Тесты проходились успешно.
Странно, что везде об этом пишется: когда я пытался найти в гугле причину, по которой может неправильно отработать CreateThread(), о _beginthreadex() я не нашел упоминания.
>Но как же, однако, трудно решить проблему в работе программы, когда глючит сама ОС! Ну хорошо, не ОС, а библиотеки и драйверы для нее.

Ничего там не глючит. Вы просто не разобрались. У функций CreateThread и _beginthread() есть понятные причины работать так, как они работают. Читайте Рихтера на счёт деталей. Что касается удаления файлов — покажите, где и кто вам гарантировал, что файл удалиться мгновенно? Файловые операции всегда были длительными. С удалением файлов вообще чудеса бывают. Например, попробуйте удалить видео-файл, который в данный момент воспроизводится в плеере. Он удалиться. Вроде бы. На самом деле он пропадёт из этой папки и переместиться куда-то в %Temp%. При этом плеер, который его играет, по-прежнему будет «видеть» файл, сможет читать из него и т.д. Другие программы файл по этому пути уже не видят. А после закрытия плеера — удаляется и файл из %Temp%.

В общем, не нужно нормальное поведение ОС комментировать последней картинкой. Нужно читать доки и разбираться.
> покажите, где и кто вам гарантировал, что файл удалиться мгновенно?
Вообще, я на это рассчитывал, вызывая синхронную функцию _wremove() и проверяя результат опять же синхронной функцией _waccess().

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

The DeleteFile function marks a file for deletion on close. Therefore, the file deletion does not occur until the last handle to the file is closed. Subsequent calls to CreateFile to open the file fail with ERROR_ACCESS_DENIED.

C чего вы взяли что другой процесс (например индексатор) не открыл ваш файл который вы пытаетесь удалить?
Так статья именно об этом — что функции с очевидными названиями реализуют весьма неочевидное поведение. В идеале таких мест в API быть не должно.
При чем тут название, если оно адекватно документировано?
Адекватно документировано неадекватное поведение. Ещё раз: проблема в поведении, а не в документации.

Неужели не понятно? Синхронная функция DeleteFile должна удалить файл или вернуть ошибку. А если она делает что-то другое, она и называться должна соответствующе — MarkForDeletion, например.
Да как же она должна, если в документации есть информация о том, что нет.
«Задокументированный баг — не баг, а фича».
Я понимаю о чем вы, но так хочется побыть занудой.
Или например unlink() Поведение, судя по найденным на скорую руку манам в инете, идентично.
Система ж многозадачная, не дос старый добрый. Доступ к разделяемым ресурсам не гарантирует 100% успеха. Маркать все методы как Try-чего-то-там тоже, как мне кажется, не вариант.
Нет, поведение у unlink — такое, какое и должно быть у этой функции. Программы, которые открыли файл продолжают с ним работать, но в директории его уже не видно. Можно, к примеру, взять и создать другой файл с таким же именем.
Так же обратите внимание, что она называется именно unlink, а не delete или remove. Не пытается выдавать одно действие за другое.
Почитайте статью внимательней. На функцию удаления я не жаловался. Мне не понятна была ситуация, когда нельзя создать файл с тем же именем после того, как предыдущий был удален. При этом переименовать файл в это имя можно.
Чтобы не жаловаться на выдуманные глюки ОС / судьбу нужно всего лишь читать документацию. Разве это так сложно?
> Почему вы считаете нормальным поведением невозможность создать файл с тем же именем, после того, как он только что был удален?
Думаю, в большинство случаев, лучше записывать новое содержимое в файл, вместо того чтобы удалить файл и сразу-же что-то в нём записать.
Кстати тоже интересно, первая же мысль была — а зачем его вообще было удалять? Открыть на перезапись сразу.

ЗЫ не совсем понимаю логику, запускаем стороннюю прогу, у нее все получается, у нашей — фейл, в результате виновата ось…
Удалить и переименовать файлы — быстрее, чем перезаписывать их содержимое. Хотя бы потому, что скорость такого алгоритма не зависит от размера самих файлов.

> ЗЫ не совсем понимаю логику, запускаем стороннюю прогу, у нее все получается, у нашей — фейл, в результате виновата ось…
Читайте внимательней. В первой ситуации я и написал, что косяк у нас. А на ось я грешил во второй ситуации.
Ну так переименование же работает. Не работает создание нового, пустого файла, разве нет? Создание нового пустого файла можно делать поверх старого, без удаления. Должно работать.
Вспомнилось вот этот кусок кода из исходников Google Chromium:
if (!file_util::Delete(db_name, false) && !file_util::Delete(db_name, false)) {
  // Try to delete twice. If we can't, fail.
  LOG(ERROR) << "unable to delete old TopSites file";
  return false;
}


Комментарий к вышеуказанному коду
Этот код может показаться многим программистам странным. Какой смысл два раза пробовать удалить файл? А смысл есть. Тот, кто его писал, достиг просветления и суть бытия программ. Файл однозначно удаляется или не удаляется только в учебниках и абстрактном мире. В реальной системе бывает так, что файл только что нельзя было удалить и мгновением позже — можно. Причиной тому могут быть антивирусы, вирусы, системы контроля версий, и бог весть что ещё. Программисты часто не задумываются о подобных ситуациях. Они мыслят так, раз не удалость удалить файл, то значит и не получится. Но, если хочется сделать хорошо и не мусорить в каталогах, нужно учитывать эти посторонние воздействия. Я сталкивался с ровно такой же ситуацией, когда файл не удаляется один раз на 1000 запусков. И решение было ровно таким же. Ну, разве что я ещё на всякий случай Sleep(0) вставил посередине.

Источник:
PVS-Studio vs Chromium

Ага, а потом из-за таких приколов система «удаляет» файл пятью попытками с перерывами в секунду — на всякий случай, «вдруг разблокируется». В результате, чтобы узнать о заблокированности файла, приходится ждать 5 секунд. Это долбаное поведение железно прошито в оболочке форточек — настройки нет, хака в реестре нет, потому что это магическая константа в коде.
Думаю автор не хотел сказать, что файлы должны удалится мгновенно.

Ситуация в которой функция возвращает положительный ответ на запрос удаления до фактического удаления файла может быть нормальной.
Но только в том случае, если для всех последующих вызовов в рамках использованного API система будет вести себя так, как будто файл удален.
В противном случае это явный косяк системы.

Это не трудно реализовать: можно например при создании файла на месте недоудаленного заставить поток ожидать окончания его удаления или еще как нибудь.
С какого перепугу если я удалил файл и мне сообщили что функция удаления выполнена успешно, на поверку оказывается что это сущая ложь?
И вокруг этой лжи приходится накручивать воркэраунды.
Можно Вас попросить линк на описание проблемы с CreateThread?
У меня ни разу проблем с этой функцией небыло, может быть проблема проявляется только при использовании некоторых функций CRT?

Да и судя из описания на MSDN, то симптомы должны быть другие: «the CRT may terminate the process in low-memory conditions». То есть программа должна была падать, а не фейлится вызов CreateThread.
Тут больше похоже на утечку хендлов или что-то в этом роде…

И, кстати, вопрос автору — тестировалась ли программа с использованием Application Verifier?
Да, когда ошибка трудно воспроизводится, приходится очень вдумчиво смотреть в код и думать, что же там могло сломаться.
К сожалению, визуальным анализом кода в большом проекте найти ошибку практически невозможно.

В вашем случае с CreateThread, истинную проблему ошибки вы так и не узнали, и возможно ошибка к вам вернется через какое-то время.

Попробуйте ответить на следующие вопросы:

* Вы уверенны в количестве одновременно существующих в программе потоков?
Вы дожидаетесь полного завершения старых потоков с использованием WaitFor* ф-ций перед созданием новых потоков? Возможно при нагрузке количество активных потоков в вашей программе растет?

* Возможно вы увеличивали размер стека для потоков?

* Стоит подумать о переработке архитектуры и уменьшить количество используемых потоков.
К примеру если множество потоков используется для ожидания асинхронных событий, то можно уменьшить количество потоков в 64 раза с использованием одного потока и ф-ции WaitForMultipleObjects.

* Иногда может происходить фрагментация памяти, попробуйте проанализировать память процесса после сбоя ф-ции CreateThread с использованием утилиты VMMap

* Также стоит обратить внимание на количество одновременно используемых хендлов.
Запустите программу под Application Verifier.

А насчет CreateThread vs _beginthreadex, то судя из статьи на support.microsoft.com, проблема может проявится только при использовании следующих функций: malloc(), fopen(), _open(), strtok(), ctime(), or localtime().

Да и то, там говорится о утечках порядка 70-80 байт при остановке потока.
Чтобы программа перестала работать из-за таких утечек, нужно чтобы программа тем и занималась что создавала/удаляла потоки.

Лично мне такую утечку воспроизвести не удалось, возможно в последних версиях CRT была добавлена какая-то заплатка.

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

Убрав лишь симптом, вы не вылечите болезнь.
1) Да, уверен. Нет, новые потоки создаются, пока старые еще работают. Отсюда и рост количества потоков. Да, растет при нагрузке, но растет весьма ограниченно: при стабильной работе максимум количества потоков около 130. Потом убывает.

2) Нет, размер стека стоит по-умолчанию.

3) К сожалению, потоки независимы. И все ждут разных событий. Да, хотелось бы переработать архитектуру, но руки до этого дойдут еще не скоро.

4) Как я уже писал, ноутбук не рабочий, если туда поставить студию, то эффект может пропасть. А когда проблема перестала проявляться, то ноутбук уже точно не отдадут на тестирование.

5) Пока все эксперименты с Application Verifier заканчиваются крахом программы без сообщения об ошибке, еще до отрисовки главного окна.

Спасибо за советы. Возьму на карандаш. )
1) Возможно в этом ошибка. Попробуйте добавить ожидание незавершенных потоков перед созданием новых, если количество потоков превышает определенное значение.

А вообще в Windows есть встроенный Thread Pool

3) Посмотрите Thread Pool API, возможно там есть подходящие ф-ции.
Например RegisterWaitForSingleObject

5) Креш программы при Application Verifier — это совершенно ожидаемое явление.
Таким способом Verifier уведомляет о неисправностях в программе.

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

Когда Ваша программа сможет работать стабильно с включенным App Verifier,
это будет означать, что вы правильно взаимодействуете с операционной системой.
(Не обращаетесь к удаленным данным, правильно закрываете хендлы и т.п.)

Желаю успехов!
Если не подбор под «чтоб заработало», то подбор «как сделать быстрее» наверняка всем приходилось применять.
Бывало, что теоретически кажется должно быть быстрее, даже математически количество операций вроде бы меньше, а на практике работает медленнее, из-за того, что кеш процессора сбрасывается или чтение памяти перестает быть линейным, и упирается в тайминги не давая раскрыться ее частоте. Dual pivot quicksort этим характерен. В теории быстрее, на практике оказался медленнее обычного quicksort.
PS по соседству тема с формальной верификацией, интересно, могла бы она задетектить проблемы с CreateThread?
Думаю, на данной стадии формальная верификация подходит только для «Hello, world». Потому что для больших проектов написание правил верификации — работа такая же, если не больше, по трудоемкости, как и написание самого проекта.
Под «программированием методом подбора» я имел в виду антипаттерн.

В книге программист-прагматик также упоминается как
«Программирование на стечение обстоятельств»

Вот несколько советов из книги:
* Всегда отдавайте себе отчет в том, что вы делаете.

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

* Тестируйте не только вашу программу, но и ваши предположения. Не гадайте, попробуйте осуществить это на деле.

>>Подбор «как сделать быстрее»

Это должен быть не слепой подбор, а проверка теоретических предположений на практике.
Здесь главное понять почему так получилось быстрее, а не полагаться на обстоятельства.
Ведь набор тестовых данных может получится удачным для алгоритма, и на реальных данных все будет намного медленней…

>>Интересно, могла бы она задетектить проблемы с CreateThread

Да здесь еще нужно разобраться до конца в чем собственно была проблема.
Лично я в описанное в статье решение не верю.
Простой заменой CreateThread на _beginthreadex описанная проблема не могла быть устранена.

Возможно _beginthreadex чуточку замедлил процесс создания потоков и количество потоков на единицу времени уменьшилось.

А возможно изменилось окружение на тестовой машине, к примеру установилось обновление или запустился дефгарментатор, и проблема перестала повторятся сама собой.
Мы ведь не знаем деталей тестирования, проверялась ли старая версия еще раз или нет.

Также, разработчики могли случайно внести еще какое-то изменение кроме замены на _beginthreadex, которое каким-то образом повлияло на процесс создания потоков.
К примеру добавили вывод в лог-файл.
К сожалению, или к счастью, ваши предположения не верны: изменили только вызов CreateThread на _beginthreadex.

Да, старая версия тестировалась, проблема была. Нет, тестовое окружение не менялось. И остальной код тоже не менялся. Только один вызов заменили.

Количество одновременно работающих потоков не изменилось. В работающей версии их больше, чем в неработающей.

И еще могу добавить, что эта правка исправила другие подземные стуки (непонятно почему возникающие ошибки) в другой функциональности продукта, которые проявлялись уже у тестировщика. И эти ошибки имели ту же самую природу: невозможность в нужный момент создать поток.
> Можно Вас попросить линк на описание проблемы с CreateThread?
Описание нашей проблемы приведено в тексте: CreateThread() завершается с ошибкой «Failed to create thread. Not enough storage is available to process this command.» Или вам нужна какая-то другая информация?

> То есть программа должна была падать, а не фейлится вызов CreateThread.
Возможно, имелось в виду, что будет завершен процесс создания потока, а не пользовательский процесс.

> Тут больше похоже на утечку хендлов или что-то в этом роде…
Утечек не было, проверяли.

> тестировалась ли программа с использованием Application Verifier
Нет, но я возьму на заметку. Самому интересно, справится ли эта программа с нашим проектом. :)
>>Или вам нужна какая-то другая информация?
Этот вопрос был адресован пользователю tangro.
Я просил его привести ссылку на детали указанной им информации:
«У функций CreateThread и _beginthread() есть понятные причины работать так, как они работают. Читайте Рихтера на счёт деталей.»

>>Возможно, имелось в виду, что будет завершен процесс создания потока, а не пользовательский процесс.
Нет, там четко написано: «terminate the process».

>>Самому интересно, справится ли эта программа с нашим проектом. :)
Ха, тут скорее наоборот :)
А, так это мне был вопрос? Да пожалуйста: «Рихтер Дж. — Windows. Создание эффективных Win32-приложений с учётом специфики 64-разрядной версии Windows — 2008». Вся шестая глава русскоязычного издания. И нет, одного раздела «Ой, вместо _beginthreadex я по ошибке вызвал CreateThread» не достаточно.
Я почитал главу книги и провел некоторые тесты.

Рихтер утверждает, что блок данных tiddata не будет освобожден в случае использования статической версии CRT.
(Про освобождение этой структуры в случае динамической CRT он упоминал)

Но судя с моих тестов это не так, по крайне мере для VS 2005 и VS 2010.
Этот блок данных освобождается при помощи колбека _freefls, который вызывается при уничтожении потока.
А в случае динамической CRT, как и писал Рихтер, эта структура освобождается в ф-ии DllMain, когда CRT получает уведомление DLL_THREAD_DETACH.

В общем я сделал следующие выводы:

* Ф-я CreateThread при использовании CRT безусловно deprecated, но по факту это не приведет к каким-либо видимым глюкам, если пользоваться свежими версиями CRT от Microsoft.
(Конечно, если при этом не пользоваться ф-ей signal)

* При этом я не вижу смысла не использовать _beginthreadex вместо CreateThread для нового кода.

* Но так-же и не вижу особого смысла бросаться менять CreateThread на _beginthreadex в существующем коде.

* Замена CreateThread на _beginthreadex не могла решить описанную в статье проблему.
> Замена CreateThread на _beginthreadex не могла решить описанную в статье проблему.
Очень смелое утверждение, учитывая, что у меня на руках история изменения кода, а у вас только предположения. Вы, конечно, можете больше доверять собственному опыту, чем мне, но мне врать тоже нет резона.
Вероятно, имелось ввиду, что проблема была замаскирована этой заменой, а не решена.
Я верю в то, что Вы пишете то, во что сами верите, но это не означает то, что Вы не ошибаетесь.

Без приведения доказательств в виде небольшого примера или толкового обоснования этот спор не имеет смысла.
Небольшого примера я привести не могу. Единственным примером мог бы быть исходный код нашего проекта. Но к нему доступ закрыт по понятным причинам. Я постараюсь написать небольшой тест и попробовать воспроизвести ошибку на нем.

Вы правы, что ошибка вызвана совокупностью всех выполняющихся задач в программе. Но искать причину, почему наш фикс сработал — это как искать иголку в стоге сена. Хотя мне самому интересно.

Единственным указанием на то, что проблема действительно была решена при помощи _beginthreadex, это то, что на целевом ноутбуке были запущены тяжеловесные программы, типа Photoshop'а. Следовательно, в какой-то момент могла просто не успеть выделиться память под tiddata или выделение памяти закончилось ошибкой.
Еще одной уликой в пользу этой теории — под другим пользователем на этой же машине (никаких тяжеловесных программ не запущено) проблема не воспроизводится.
Боюсь, что Ваши догадки о том, что сбой CreateThread мог произойти из-за сторонних программ неверны.

Ф-ця CreateThread скорее всего возвращала ошибку не из-за нехватки памяти, а из-за фрагментации адресного пространства. Ведь каждому потоку необходимо зарезервировать 1 МБ (по умолчанию) непрерывного адресного пространства.

При помощи диспетчера задач невозможно увидеть фрагментацию адресного пространства. Для этого необходимо запускать утилиту VMMap.

Вам необходимо изучить Архитектуру памяти Windows.
2Гб это много.
Куча внутри фрагментирована однако снаружи это цельный блок.
Навряд ли 90 метров не нашлось.
Ну куча из-за сильной фрагментации могла сама вырасти.

У вас есть другие предположения почему ф-ця CreateThread могла завершится с ошибкой, вернув при этом ERROR_NOT_ENOUGH_MEMORY?

В любом случае без детального анализа все предположения бессмысленны.
Хорошо, допустим вы правы. Как вы могли бы тогда объяснить тот факт, что замена CreateThread() позволила если не исправить, то хотя бы замаскировать ошибку? _beginthreadex() не требуется 1 МБ непрерывной памяти?
Возможно ваша версия CRT действительно не освобождает tiddata для каждого освобожденного потока…
Можете это проверить?

И, кстати какой компилятор/CRT/студию вы используете?
Сборка происходит при помощи Visual Studio 2010.
И укажите, пожалуйста версию Windows на тестовом ноутбуке…
Windows 7 Ultimate x64 SP1.

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

>>что в нужный момент TLS не удалось инициализировать
Вы не путаете CRT tiddata и WinAPI Thread Local Storage?

Кстати, очистка памяти tiddata реализована через TLS Callback.
Может быть этот колбек по какой-то причине у вас не вызывается?
Вроде не путаю. Это же разные вещи? :)
Просто вспомнил особенность про использование TLS.
Чтобы что-то объяснить необходимо провести детальный анализ.
У меня нет возможности это сделать, да и желания тоже…
Я извиняюсь, а зачем вообще не-серверной программе создавать 90+ потоков? Если они все работают одновременно, то ничего хорошего на 2-4 ядрах из этого не выйдет. Если они постоянно спят, то это просто ugly design. Если же потоки нужны для асинхронных вызовов (а-ля .Net I/O Thread Pool, где разрешено 1000 потоков), то возникает вопрос, почему так много вызовов не успело завершиться и вернуть поток в пул. К примеру, в .Net 1.1 было такое при зависании сокетов.
> а зачем вообще не-серверной программе создавать 90+ потоков?
By design. Так достигается максимальная производительность в нашей программной архитектуре.
Какая, (тут был мат), «максимальная производительность»? При превышении количеством активных потоков количеств ядер возрастают только накладные расходы на их переключение и синхронизацию.
Нет, не все потоки активны. Количество активных равно по порядку количеству ядер. Просто в начале работы, когда нужно разогнать движок, происходит такой скачок потоков.
Это плохой дизайн. tangro прав, постарайтесь принять критику адекватно, а не писать оправданий.
Полностью с вами согласен. И я не оправдывался, сам знаю, что костыль на костыле в программе. :)

Другое дело, что когда рефакторинг стоит на последнем месте в приоритете разработки проекта (а на первом — новые фичи), то вот такие ошибки отнимают очень много времени на их исправление.
90 еще вполне терпимо и по силам системе.
Такое и правда может быть оправдано. Приведу пример из нашей такой программы — в ней разные потоки могут выполняться разное кол-во времени, т.к. внутри каждого потока есть обращение по http к какому нибудь внешнему сервису (иногда это и другие протоколы), и вот разные сервисы могут отвечать совершенно по разному (какой нибудь может вообще временно упасть и там будут сплошные таймауты, как правило это секунд 20, или что нибудь еще). Ну и еще один немаловажный нюанс — по сути потоки независимы друг от друга (нет точки где ожидается завершение абсолютно всех потоков, так что нет смысла их ограничивать). Именно в таких случаях многопоточность и нужна.
А вообще, откровенно говоря, количество ядер здесь не играет вообще никакой роли, если вдуматься — у вас уже работает ооочень много потоков одновременно (не только из вашей программе), при этом если в каком то из них затык — все остальное как бы продолжает работать. Многопоточность и многоядерность (обычно паралелльное программирование выступает вторым в сравнении) — совершенно разные вещи ;)
Для такой задачи обычно реализовываются асинхронные event'ы с небольшим пулом потоков.
Event'ы на ожидание http, когда событие произошло отдаём задачу потоку из пула на обработку.
Наверное плохо рассказал. Основная задача потока не выполнить http запрос, просто выполнением http запроса я хотел показать пример того кусочка, зависимый от внешних факторов, и который может выполняться долго и это не должно тормозить все остальное.
Основная мысль — если ваша программа выполняет много почти независимых задач одновременно — каждую такую задачу можно запускать в отдельном потоке и кол-во ядер тут ни при чем. Вот что я хотел сказать.
Например, обрабатывать поступающие запросы (не http запросы ;) — запросы, как правило, независимы же.
И да, наверное я еще не прав по одной важной причине — я рассказываю про потоки, которые запускаются через ThreadPool в .NET — не ручками (про что я не сказал), что тоже не мало важно, конечно…
Ну и я про то же, если есть много длительных задач, то лучше всё завести на асинхронную обработку, а не играть в гонки с потоками.
Если это чисто вычислительные задачи, не зависящие от внешних факторов (от сети, фс, внешней электронки), то тогда всё равно лучше организовать простенькую очередь задач, а не плодить 100500 потоков.
Это как бы best practise проектирования многопоточных приложений.
Ну я наверное просто знаю, что ThreadPool не даст мне запустить 100500 потоков одновременно. При этом, перед тем, как запустить задачу на выполнение в ThreadPool, тоже есть маленькая очередь, но она уже больше описывает ограничения по бизнес логике (не про сами потоки, поэтому не говорил про нее)
Просто мне казалось, что 90 потоков это еще нормальное кол-во
>>Например, обрабатывать поступающие запросы (не http запросы ;) — запросы, как правило, независимы же.
А почему не делать как все, очередью? Асинхронные ждущие задачи понятное дело в пул, но прием сообщений и переправку их в очередь как нибудь один поток справится. А на выполнение уже потоки по количеству ядер. Сотни активных потоков, соглашусь с предыдущими комментаторами — плохой дизайн.
Ну прием сообщений естественно идет в одном потоке, из которого, если лимиты позволяют, они сразу отдаются на исполнение в отдельном потоке, если превышен некий лимит (не по потокам, тут уже специфичное в программе), то выполнение откладывается. Выполняющих же потоков и может быть много — именно в этом я не вижу ничего особо такого плохого. Не 100500, конечно же.

В общем, меня наверное смутило то, что 90+ потоков в серверном ПО кого то испугало. И что, в контексте этого, заговорили еще про количество ядер процессора.

Естественно что запуск всего и сразу в отдельный поток неправильно (сам видел пример приложения на .NET где прямо руками создавали Thread и делали ему Start, что валило приложение при нагрузке). У меня есть некая очередь, логическая в рамках выполняющихся задач, ThreadPool для самих потоков (очередь для потоков, которая в моем случае предоставляется платформой), да и сами сами запросы по http в моем случае «проходят» внутри через настройки ServicePoint (тоже по сути дается платформой и является неким пулом уже для подключений). Просто то, что перед 90+ потоков кажется для меня слишком очевидным, поэтому я не стал заострять на этом внимание…
Во-первых, речь идет о не серверном п.о., судя по тестированию на клиентском ноутбуке Я специально это подчеркнул в своем посте. Для серверов еще как-то можно оправдать большим количеством задач/запросов.

Во-вторых, одновременное выполнение потоков, количество которых больше вычислительных ядер, требует остановок и Context Switching. Потому априори, на 2х процессорах с 2 ядрами будут быстрее работать 4 потока с очередью задач, чем 160 и переключением. Это основа серверного программирования — не выделять никогда каждому подключенному клиенту по одному потоку, не выделять никогда одной мелкой задаче по одному потоку.

Не зря в .Net 2.0 в Thread Pool был жесткий лимит на 25 потоков на ядро. В 4.0 уже пошла какая-то гибкая формула, что поощряет разработчиков не думая об архитектуре, а кидать сразу свои задачи в ThreadPool, а затем в реальных условиях начинаются тормоза, но код уже написан и оплачен, потому стараются масштабировать уже железо, а заодно ругать фреймворки, админов, инопланетян с Марса и пр.
Во-первых, речь идет о не серверном п.о., судя по тестированию на клиентском ноутбуке

Ну почему сразу не о серверном? Если у разработчика есть большой набор унаследованной инфраструктуры, то ситуация когда ноутбук мощнее сервера вполне себе реальна. А если дело в ОС, то WinServer 2003/2008 вполне нормально живут на ноутбуках. Я некоторое время пользовался ноутами под управлением данных операционных систем.

90+ потоков меня несколько смущают, но все-же это вполне реальный сценарий. Например файловый I/O может быть синхронным, тогда для уменьшения негативных эффектов из-за временно заблокированных на дисковых операциях процессов/потоков имеет смысл держать большой пул.
Например файловый I/O может быть синхронным, тогда для уменьшения негативных эффектов из-за временно заблокированных на дисковых операциях процессов/потоков имеет смысл держать большой пул.

Пример не ахти, производительность его будет не то, чтобы «не на высоте», а просто «ниже плинтуса». Зато, все I/O операции рано или поздно будут выполнены, хотя скорее «поздно». Асинхронное I/O тут гораздо уместнее.

Чуть более удачный пример это вызов тысяч БД запросов в куче потоков — при большом количестве insert/update/delete блокировки заставят большую часть потоков ждать и разблокироваться при необходимости. Правда, практика показывает, что все-равно это довольно медленно и если отсортировать команды на 3-4 потока, то можно получить гораздо лучшую производительность.

Ну а тестирование серверного софта на ноутбуке вообще вызывает сомнения. Судя по тексту статьи, автор как раз писал о юзерском ноутбуке.
Я, возможно, ввел вас в заблуждение. Серверное ПО в нашем случае — это не совсем серверное ПО в общем понимании.
Архитектура ПО подразумевает, что внутри самой программы есть серверная часть, которая обрабатывает и подготавливает данные для различного железа, с которым наше ПО взаимодействует. Так вот когда появляется новое железо в ходе работы, то к серверной части идет запрос на выдачу большого количества данных. К сожалению, код написан так, что в первое время этот запрос можно удовлетворить только большим количеством потоков.
Многопоточность по хорошему нужна для оптимальной нагрузки ядер/процессоров. Если создавать на каждую задачу (вызов удаленного сервиса) по потоку, то результат будет немного иной (вплоть до того деградации производительности).
Если стоит задача эффективной обработки HTTP запросов, то нужно использовать порт завершения. Количество потоков при этом принято брать в два раза больше количества процессоров.
Второй вопрос — это обычное кэширование.
Мне кажется, это проблема даже не столько в некомпетентности в программировании под виндоуз, сколько банально в низкой дисциплине написания говнокода. Смешивать разные API и фреймворки — почти всегда плохая идея, как стилистически, так и прагматически. Использовать и голый Win API, и std/boost, и CRT, и Qt — неизбежно навлечь на себя подобные проблемы.
Как вы незаметно назвали всех моих сотрудников и меня в том числе некомпетентными говнокодерами под Windows!
Вы сами себя назвали. В статье.
Мы стараемся это исправить. Вот, один конфликт API исправили. :)
Софтверная разработка в рамках использованных Вами технологий требует большого количество специальных знаний по использованию фреймворков и высокого уровня организации разработки в архитектурном плане.

Все знают что нативные api для windows восновном не являются интуитивно понятными и там нет никакой защиты от «дурака». Вообще там нельзя полагаться на здравый смысл, только на документацию и стиль уже написанного, работающего кода.

Наймите себе хорошего архитектора, который уже все Ваши проблемы проходил.
И пусть все пишут код в рамках того стиля, который он создаст.
А как, не смешивая разные API и фреймворки, написать программу, работающую с сетью и имеющую GUI, к примеру? Если не использовать сторонние библиотеки, то такую программу можно писать ооочень долго.
Qt? Содержит как классы работы с сетью (модуль QtNetwork), так и весьма продвинутое GUI (QtGui+QML). Причем разработка на Qt весьма быстра.
Думаю для второй проблемы нужно использовать ассемблерные вставки, либо взаимодействовать с ядром ОС, если конечно такая возможность есть.
По поводу №2:
Разбирался когда-то с одним багом, всё выглядело так, как будто если удалить файл и сразу же снова его создать, то дата создания не меняется. Глубже не копал, правда, и так и не узнал, из-за чего именно такие чудеса происходят.
По поводу №2 автор пишет совсем какую-то фантастику. На самом деле _wremove либо удаляет файл, либо нет — нужно проверять код возврата. В зависимости от флагов с какими файл был открыт, удаление может и не сработать, особенно хитро всё работает, если режим включал в себя бит FILE_SHARE_DELETE. Так же надо учитывать, что программа работает не одна в системе, есть всякие хуки, например, антивирусы, которые тоже могут обломить удаление файла. И вообще, FindFirstFile не слишком удачный способ проверки наличия файла, есть более быстрый и точный: ( GetFileAttributes(… ) != INVALID_FILE_ATTRIBUTES ).

P.S. Я вообще не понимаю цели данной статьи… Мазохизм?
> На самом деле _wremove либо удаляет файл, либо нет — нужно проверять код возврата.
Не поверите, проверяется. В посте приведен упрощенный код.

> В зависимости от флагов с какими файл был открыт, удаление может и не сработать.
Файл со случайно сгенерированным именем создавался с правами на чтение и запись, а затем удалялся. Читайте код.

> Я вообще не понимаю цели данной статьи… Мазохизм?
Цель — возможно, помочь тем, кто когда-либо сталкивался с такой ситуацией.
Помочь как?! Описывать «странную» ситуацию? А я в неё не верю. Описывать её с упрощениями? Хотя известно, дьявол прячется в деталях. Отсюда вывод: то, что вы описали, должно работать 100%. А вот то, что НЕ описали как раз и содержит неочевидную ошибку, а судя по №1, виновата скорее всего невнимательность к документации. Отсюда цель статьи — показать как полезно читать документацию что ли?!
Вы не верите в ситуацию, описанную во втором случае? Соберите тестовую программу и запустите ее. Код теста я привел. Он тоже будет глючить. Единственное, что я не написал — это вывод номера итерации, на которой произошла ошибка.
Я запускал.
100000 итераций — ошибки нет.
Правда еще более упрощенно:
CreateFile()/DeleteFile()/CreateFile(..., CREATE_NEW, ...).
Последнее создание файла всегда было успешным.
Я в конце статьи выложил свой полный тест. У меня ошибка воспроизводится стабильно в пределах 100000 итераций.
Я использовал CRT, а не WinAPI.
Подождал с вашим примером до миллиона итераций — ничего.
Как правильно кто-то написал, возможно виноват антивирус — открывал файл, и поэтому система вынуждена была ждать когда все хендлы закроются, перед непосредственным удалением файла.
Да, так и есть. Обновил пост.
Однако вы прекрасно понимаете, что просить пользователя отключить антивирус не есть хорошо. )
В блоге blogs.msdn.com/b/oldnewthing автор частенько рассказывает про то, почему что-то у кого-то не работает. И довольно часто упор делается на то, что люди сначала используют недокументированные возможности, а потом удивляются, что это сломалось на новой версии Windows или в других ситуациях. Может кому будет интересно.
Постоянно этот блог читаю, и другим советую. Да и автор мог бы добавить вниз статьи, как ссылку на авторитетный и, что самое главное, полезный ресурс.
Скажите, когда Вы когда проводили тестирование — антивирус у Вас стоял?
Они вносят дополнительные сложности, весьма специфические.
Но это всегда нужно учитывать, потому что у конечных пользователей антивирусы используются.
Да, антивирус стоял. Посоветовать пользователям отключить антивирус мы не можем. :)
Попробуйте отключить и повторить тесты, возможно, это проблема не ОС, а антивируса.

У меня такое было, если интересно, попробую в понедельник найти код, демонстрирующий ошибку при удалении файла в присутствии антивиря. Причем файл был абсолютно «чистый», без вирусных паттернов.
Да, вы были правы. Без антивируса проблема не повторяется. Однако разграничение доступа к файлам — задача ОС, а не приложений.
У меня был примерно такой же код, который конфликтовал с антивирусом.

Решение проблемы в том, что вместо
f = fopen(fileName, mode);

сделали
for (int i=0; i<10; i++)
{
    f = fopen(fileName, mode);
    if (f) break;
    Sleep(100);
}

И проблема исчезла.

Для удаления файла — аналогично.
UFO just landed and posted this here
UFO just landed and posted this here
> Решение: а его мы так и не нашли. Т.к. в реальном проекте не было аналогичной ситуации с созданием/удалением одного и того же фала, то на ошибку решили забить.

Правильное решение: читать msdn, там написано как и почему удаляются файлы под NTFS. Если коротко, то в этом случае ОС действительно сохраняет некоторое время информацию об уже удаленном файле.
Как уже правильно подметили мешать Win API и CRT в одной программе — bad practice. Выберете что-то одно.
Лично я считаю текущую реализацию CRT в Visual Studio (которая кстати к собственно Windows имеет весьма дальнее отношение) достаточно спорной и не рекоммендовал бы его (CRT) применение в более-менее серьезных проектах. Его лучше вообще отключать при линковке для верности — чтобы не закрался в код неявный вызов CRT.
Спасибо автору за статью, нашел в ней для себя важную новую информацию. Про _beginthreadex не знал!
Почитать Рихтера (я уже не говорю про Руссиновича) перед кодингом? Не, не слышали! Я может кого-нибудь обижу, но это все детские ошибки.
Спасибо за наводку на эти книги! Действительно, первой ситуации можно было бы избежать, прочитав Рихтера.
По поводу #2 это явная недоработка архитектуры ОС.
В *nix если несколько процессов открыли один файл, то его можно запросто удалить при помощи функции unlink(). При этом ссылка на файл исчезает, и если это была последняя ссылка на файл, то открыть его будет невозможно, но процессы которые его уже открыли и имеют дескриптор могут продолжать работать с этим файлом. Файл фактически будет удалён после закрытия последнего дескриптора.
Но при этом можно создать новый файл с таким же именем, Это будет абсолютно другой файл.
Автор явно решил постебаться над разработчиками win api. Но вообще-то есть более смешные функции. Например DrawText в целом выводит текст, но если в аргумент передать uFormat значение константы DT_CALCRECT, то функция неожиданно ничего не рисует, а определяет высоту прямоугольника в который может быть вписан переданный текст. Ржу не могу… «In either case, DrawText returns the height of the formatted text but does not draw the text.» Возникает чувство, что разработчики microsoft экономили на кол-ве функций.

Именно поэтому, тот кто пишет под win должен быть всегда начеку. MSDN и Рихтер для них родные доки/книги, которые нужно изучать под микроскопом. Читать и перечитывать. Ну и не бухтеть :)
Вот почему полезен статический анализ кода. Он может указать на те места, которые программист по незнанию считает совершенно правильными. Жаль только многие проходят мимо подобных предупреждений…
Sign up to leave a comment.

Articles