Странности с циклами: в debug работает, в release нет

    День добрый, хабраюзеры.

    Давно я ничего не писал, но здесь просто вопиющий случай. Я с недавних пор пишу на Objective C, сталкивался со многими и многими непонятными ситуациями, из которых мог искать выход день и два. В итоге все решалось и оказывалось банальным и простым. К тому же «все это уже было в Симпсонах» — после локализации проблемы ее решение находилось на первой странице гугла.

    Но здесь! Отладил приложение, собрал в архив для AppStore, решил перепроверить на устройстве — полезли баги.

    Что бывает и как с этим бороться — предлагаю обсудить ниже


    Итак, программа моя выполняла следующее:
    В главном потоке (буду для простоты здесь и далее называть Threads потоками) отображается глобальный UIScrollView, в котором рендерятся изображения, текст, аудио и видео. Для текущей открытой страницы рендеринг осуществляется в этом же главном потоке. Для плавности перехода на соседние страницы, в отдельно созданных потоках выполняется предварительный рендеринг (фото низкого разрешения растягивается на экран), и полный рендеринг. Назовем поток с предварительным рендерингом потоком №1, с полным — потоком №2.

    Для чего нужен предварительный рендеринг? Чтобы пользователь мог быстро-быстро перелистывать страницы и видеть низкокачественную страницу, а не ждать каждый раз загрузки полной версии.
    Полный рендеринг — тут понятно. Человек читает одну страницу, переходит на соседнюю — а там уже всё подгружено, и запускается на очередь «новая» соседняя.

    К слову — нельзя же рендерить одну страницу одновременно из двух потоков, поэтому и поток №1, и поток №2, дожидались окончания рендеринга текущей страницы кем бы то ни было, и потом уже проверяли — есть ли предварительный/полный рендеринг или нет, и в зависимости от этого осуществляли задуманное, либо выходили.
    Для всего этого использовался флаг BOOL isRendering. И в методах -(void) preRenderPage и -(void) fullRenderPage находилась вот такая конструкция:

    while (!isRendering) {
    }
    isRendering = YES;

    … //непосредственно рендеринг

    isRendering = NO;
    return;

    Все замечательно работало при запуске из xcode, но переставало работать при запуске Archive/release build.
    На локализацию проблемы у меня ушел весь рабочий день, а вот после — я не нашел ни одного упоминания о ней. Итак, внимание!
    Если цикл ожидания изменить вот так:
    while(!isRendering) {
    [NSThread sleepForTimeInterval:0.01];
    }
    то все снова работает, как и в девелоперском билде.

    У меня есть только версия, что цикл while может запускаться лимитированное количество раз. При запуске из xcode после каждой инструкции программа общается со средой разработки, потому и работает медленнее. При запуске релизного варианта такой задержки нет, цикл while выполняется слишком много раз, и он убивается. Либо подвешивается в suspend и больше не выполняется.

    Вот такие дела.

    Быть может, я в принципе криво реализовал ожидание разрешающего флага, и стоило это сделать по-другому. Раскройте мне глаза :) Может есть что-то банальнее и проще.

    PS вариант с NSRunLoop и [NSDate distantFuture] не рассматривал, т.к. он при отсутствии команд выполняется только один раз. А делать [NSDate dateWithTimeIntervalSinceNow:0.01] — по моему мнению никак не отличается от моего решения, да и более громоздкое.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 31

      +13
      так пустой же цикл, вот оптимизатор его и выпилил в релизе.
        +3
        Он его не просто выпилил. NSLog до этого цикла выполняется, а после него — никогда. Т.е. он в нем виснет.
          +2
          Оптимизатор перенес переменную из памяти в регистры. Как результат — изменение значения в памяти другим потоком ничего не дает. Почитайте про специальный модификатор volatile — поймете суть проблемы.
            +1
            Хорошо. Volatile, так volatile. Спасибо за него, кстати.

            Но кто-нибудь может объяснить, почему этого в debug build не происходит, а только в release? Перепроверил все флаги и build settings — все одинаково для обеих конфигураций
              0
              Еслибы у debug build были бы теже флаги что и у release build — то он бы был release build ;)

              Если дебаг инфо генерируется то часть оптимизаций не будет выполняться. Не говоря о препроцессоре с его #ifdef DEBUG… #ifdef NDEBUG…
          0
          Это баг в компиляторе же, ты не понял
          +3
          Объявить isRendering как volatile?
            0
            100 пудово выоптимизировалло нафиг — на stackoverflow есть тоже про это
              0
              ответ по выпиливанию — выше
            +5
            Цикл while выполняется пока isRendering == NO

            Если isRendering изменяется в другом потоке без volatile — то не детерминированное количество раз.

            Никакого ограничения на количество итераций while нет и быть не может.

            А описание проблемы настолько путанное, что локализовать ее тяжело. Возможно, вы изобрели велосипед вместо очереди сообщений.
              +4
              Да, никогда не делайте синхронизацию (а это именно она) на bool. Используйте NSLock.
                +2
                Перечитал еще раз про NSLock. Посыпаю голову пеплом, это даже проще, чем мои циклы
              +14
              А разве за
              while (!isRendering) {
              }
              ещё не обрывают руки? Есть же мониторы, семафоры, мьютексы…
                +2
                Здесь идет работа в рамках одного потока. Но, насколько я понимаю, поставщиком задач является другой поток. В целом, все сводится к задаче «производители/потребители», которая на iOS решается с помощью GCD и sequential очереди задач. Ну, или NSOperationQueue для слабых духом.
                  0
                  NSOperationQueue начиная с iOS 4 основана на GCD как раз
                    0
                    Возможно, я не слишком копался в ее кишках. GCD показался мне в разы удобнее и я сразу забыл про эту обертку.
                      0
                      не возможно, а вполне явно написано в документации :)
                        +1
                        Я бы сказал «определенно», но не заглядывал в документацию, а привычки верить людям на слово не имею :)
                          0
                          За подсказку с NSLock спасибо, в авральном режиме работаем, вылетела из головы эта фича.
                          По поводу GCD: изначально все писалось на нем, были красивые конструкции вроде dispatch_group_wait и так далее. Но у GCD проблема с рендерингом вьюшек. Поэтому был создан свой класс, унаследованный от NSThread и там была реализована человеческая очередь на выполнение.

                          Да, вы меня сейчас забросаете помидорами за использование UIKit вне главного потока, но все же это не критично. Ибо главное не обращаться к одной и той же вьюшке из конкурирующих потоков.
                            +2
                            У GCD проблемы нет, это у вас проблемы с рендерингом.

                            Правильный рендеринг на GCD выглядит так:
                            1. Создаем ContextRef через CreateBitmapContext
                            2. Рисуем в него как в любой нормальный контекст
                            3. Создаем из контекста ImageRef
                            4. Передаем его в основной поток и
                            4.1. Ставим его в view.layer.contents
                            4.2. Преобразуем в UIImage и используем в слое-подложке на базе ImageView

                            А то, что вы делаете — это непонятно что. И оно может отвалиться в любой момент, причем совершенно недетерминированно.
                              0
                              Вообще предпочитаю не обращаться к главному потоку, чтобы не блокировать UI

                              Да, у GCD во время создания контекста (рендерил пдф) я тоже ловил невнятные и непонятные ошибки, вроде context is null, и cannot restore null context.
                                0
                                Нельзя работать с объектами, живущими в STA потоках вне их потока. Это же основы, что на винде, что на маке.

                                То, что у вас при этом что-то работает — чудо, а не заслуга.
                    +1
                    Вот именно, что два потока. И этот поток тупо жрёт ресурсы.
                    Любые инструменты, хоть для слабых, хоть для сильных, но не пустые циклы.
                    +1
                    и GCD, чтоб вручную треды не городить
                      0
                      Тоже вариант, главное не занимать процессор пустым циклом.
                    +1
                    По симптомам — скорее всего компилятор действительно выбросил этот цикл при оптимизации. Тогда вам совершенно необязательно вставлять в цикл sleep, достаточно будет любого выполняемого кода, чтобы компилятор не счел, что цикл ничего не делает. Это если вам именно припекает поскорей залить продукт в App Store.

                    А вообще в свободное время нужно бы переделать это добро на GCD или NSOperation. Чтобы быстро въехать, что к чему, можно посмотреть эпловое видео, если английский позволяет (https://developer.apple.com/videos/wwdc/2012/, например см. сессию 712)
                      0
                      Читайте, чуть выше объяснил, почему не использовался GCD или NSOperation.
                      И цикл не выпиливался, NSLog до него выполняется, а после него — нет.
                      +3
                      По заголовку уже догадывался, что компилятор выбросил цикл.
                      Зашел почитать комментарии — так и вышло.
                        0
                        Правило что-ли в PVS-Studio сделать на такие циклы… Уже сколько раз видел аналогичные вопросы.
                          +2
                          А вы dead code разве не репортите? А вообще, ИМХО, замечательный пример необходимости образования — не верю, что про такие вещи в ВУЗ'ах не рассказывают совсем.
                            0
                            Dead code — понятие растяжимое. Если имеется в виду бессмысленный код, то что-то ловим, что-то нет. Многий бессмысленный код это продукт использования разнообразных макросов. Таких, например, как UNREFERENCED_PARAMETER. Если речь идёт о недостижимом коде, то с этой задачей хорошо справляются компиляторы. Пока не видно смысла дублировать эту функциональность.

                        Only users with full accounts can post comments. Log in, please.