Могу ли я улучшить разрешение Thread.Sleep?

Разрешение Thread.Sleep() варьируется от 1 до 15,6 мс

Учитывая это консольное приложение:

class Program
{
    static void Main()
    {
        int outer = 100;
        int inner = 100;
        Stopwatch sw = new Stopwatch();

        for (int j = 0; j < outer; j++)
        {
            int i;
            sw.Restart();
            for (i = 0; i < inner; i++)
                Thread.Sleep(1);
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
        }
    }
}

Я ожидал, что на выходе будет 100 чисел, близких к 100. Вместо этого я получаю что-то вроде этого:

99 99 99 100 99 99 99 106 106 99 99 99 100 100 99 99 99 99 101 99 99 99 99 99 101 99 99 99 99 101 99 99 99 100 99 99 99 99 99 103 99 99 99 99 100 99 99 99 99 813 1559 1559 1559 1559 1559 1559 1559 1559 1559 1559 1559 1559 1559 1559 1559 1559 1559 1559 1559 1559 1560 1559 1559 1559 1559 1558 1558 1558 1558 1558 1558 1558 1558 1558 1558 1558 1558 1558 1558 1558 1558 1558 1558 1558 1559 1558 1558 1558 1558 1558

Но иногда я не получаю точных результатов; это будет ~ 1559 каждый раз.

Почему это непоследовательно?

Некоторые гуглы научили меня, что 15,6 мс — это длина временного среза, так что это объясняет ~ 1559 результатов. Но почему иногда я получаю правильный результат, а иногда просто кратный 15,6? (например, Thread.Sleep(20) обычно дает ~31,2 мс)

Как на это влияет аппаратное или программное обеспечение?

Я спрашиваю это из-за того, что привело меня к его открытию:

Я разрабатывал свое приложение на 32-битной двухъядерной машине. Сегодня моя машина была обновлена ​​до 64-битного четырехъядерного процессора с полной переустановкой ОС. (Windows 7 и .NET 4 в обоих случаях, однако я не могу быть уверен, что на старой машине был W7 SP1, а на новой есть.)

После запуска моего приложения на новой машине я сразу же заметил, что мои формы исчезают дольше. У меня есть собственный метод для затухания моих форм, который использует Thread.Sleep() со значениями от 10 до 50. В старой системе это, казалось, работало каждый раз отлично. В новой системе исчезновение занимает гораздо больше времени, чем должно.

Почему это поведение изменилось между моей старой системой и моей новой системой? Это связано с железом или софтом?

Могу ли я сделать его неизменно точным? (разрешение ~1 мс)

Есть ли что-то, что я могу сделать в своей программе, чтобы сделать Thread.Sleep() надежно точным примерно до 1 мс? Или даже 10 мс?


person Igby Largeman    schedule 30.09.2011    source источник
comment
Настоящая программа все равно не должна спать(). Но стандартные Таймеры имеют такое же разрешение.   -  person Henk Holterman    schedule 30.09.2011
comment
Мне понравился вопрос, как я его написал. Пожалуйста, воздержитесь от внесения правок, которые явно не исправляют ошибку.   -  person Igby Largeman    schedule 30.09.2011
comment
Заставлять читателей нажимать ненужные страницы для получения совершенно бессмысленных данных — ошибка.   -  person Henk Holterman    schedule 30.09.2011
comment
@Henk: мой исходный пост короче, чем твое редактирование. Ваше редактирование сократило чье-то редактирование, что сделало его излишне длинным. И данные не лишены смысла.   -  person Igby Largeman    schedule 01.10.2011
comment
Просто из любопытства, соответствует ли уменьшение разрешения — момент, когда вы перестаете видеть, как он спит в течение 100 мс, и вместо этого начинаете видеть, что он приостанавливается на 1500 мс — какому-либо внешнему событию? Скажем, ваше приложение теряет фокус?   -  person Joe White    schedule 01.10.2011
comment
@Joe: нет, ничего, что я мог обнаружить. Помимо фоновых процессов, моя машина простаивала.   -  person Igby Largeman    schedule 01.10.2011
comment
Возможно, какой-то другой процесс или сама ОС часто выполняет timeBeginPeriod(1)/timeEndPeriod(1).   -  person Igby Largeman    schedule 01.10.2011


Ответы (5)


Ответ заключается в том, чтобы не использовать Thread.Sleep, а вместо этого использовать таймер с высоким разрешением. Вам нужно будет выполнить затухание в цикле занятости, но похоже, что это не проблема. Вы просто не можете ожидать высокого разрешения от Thread.Sleep, и он известен тем, что ведет себя по-разному на разном оборудовании.

Вы можете использовать класс Stopwatch в .net, который использует счетчики производительности с высоким разрешением, если они поддерживаются на оборудовании.

person David Heffernan    schedule 30.09.2011
comment
Я пошел с этим решением. Я получил миллисекундную точность, сидя в цикле, проверяя StopWatch.MillisecondsElapsed. Как вы сказали, в этом случае занятая петля в порядке. - person Igby Largeman; 01.10.2011
comment
Но я все еще хотел бы, чтобы кто-нибудь мог сказать мне, что происходит - почему он переключается между разрешением 1 мс и 15 мс, казалось бы, случайным образом. - person Igby Largeman; 01.10.2011
comment
Мое единственное предположение состоит в том, что какой-то другой процесс или сама ОС выполняет timeBeginPeriod(1)/timeEndPeriod(1). - person Igby Largeman; 01.10.2011

«Функция Sleep приостанавливает выполнение текущего потока на по крайней мере указанный интервал».

-> http://social.msdn.microsoft.com/Forums/en/clr/thread/facc2b57-9a27-4049-bb32-ef093fbf4c29

person MasterCassim    schedule 30.09.2011
comment
Хотя это связано, это не отвечает ни на один из моих вопросов. - person Igby Largeman; 30.09.2011
comment
Фраза по крайней мере отвечает на ваш прямой вопрос (с Нет). - person Henk Holterman; 30.09.2011

Я могу ответить на один из своих вопросов: Могу ли я сделать его неизменно точным? (разрешение ~1 мс)

Да, кажется, могу, используя timeBeginPeriod() и timeEndPeriod().

Я проверил это, и это работает.

Некоторые вещи, которые я читал, предполагают, что вызов timeBeginPeriod(1) для продолжительности приложения - плохая идея. Однако вызов его в начале короткого метода и последующая очистка с помощью timeEndPeriod() в конце метода должны быть приемлемыми.

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

person Igby Largeman    schedule 30.09.2011

На вашем компьютере запущена программа, которая вызывает timeBeginPeriod() и timeEndPeriod(). Обычно программа, связанная с мультимедиа, использует timeSetEvent() для установки таймера на одну миллисекунду. Это также влияет на разрешение Sleep().

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

Сон в течение 20 мс и фактически получение 2/64 секунды в противном случае логичны, процессор просто не проснется достаточно скоро, чтобы заметить, что прошло 20 мс. Вы получаете только кратные 1/64 секунды. Таким образом, разумный выбор для таймера, который реализует эффекты затухания, составляет 15 мс, что дает в худшем случае анимацию со скоростью 64 кадра в секунду. Предполагая, что ваш эффект прорисовывается достаточно быстро. Вы будете немного сбиты с толку, если будет вызван timeBeginPeriod, но ненамного. Вычисление стадии анимации по часам тоже работает, но в моей книге это немного излишне.

person Hans Passant    schedule 30.09.2011
comment
Это правда, 16 мс достаточно для этой конкретной ситуации. Значительное увеличение произошло потому, что метод работал, зациклив 100 раз, изменяя непрозрачность от 1 до 0 с шагом 1%. Итак, 100 * 3 = 0,3 секунды стало 100 * 15,6 = 1,6 секунды. Таким образом, я могу либо использовать timeBeginPeriod/timeEndPeriod, либо я могу переработать цикл так, чтобы он повторял только циклы (затухание миллисекунд/16) раз и изменял непрозрачность с шагом (1/количество циклов). Я пробовал оба метода, и они оба работают хорошо. (Или я могу использовать таймер с высоким разрешением) - person Igby Largeman; 01.10.2011
comment
Ну, как я уже говорил, не парься по мелочам. Но не используйте 16, это слишком долго и даст вам либо 2/64, либо 0,016 секунды. Используйте 15, чтобы получить либо 1/64, либо 0,015 секунды. - person Hans Passant; 01.10.2011
comment
Отметил благодарности. В конце концов я использовал StopWatch и просто зациклился, проверяя MillisecondsElapsed; этот метод точен до миллисекунды. В этом случае есть циклы нормально. - person Igby Largeman; 01.10.2011
comment
Это плохая идея. Ваша анимация будет работать намного плавнее, когда вы спите или используете таймер. Когда вы сжигаете циклы, как вы это делаете, планировщик потоков помещает вас в собачью конуру на некоторое время после того, как вы прожжете квант, чтобы дать возможность другим потокам в других процессах работать. - person Hans Passant; 23.01.2013
comment
Боже, Ганс, ты мог бы сказать мне раньше! Я больше не работаю в этой компании и даже не живу в этой стране! Хотя это действительно работало нормально. Я просто исчезал из формы примерно за полсекунды. - person Igby Largeman; 24.01.2013
comment
Ну, Боже, ты сказал, что тебе все равно, поэтому и мне было все равно. Другие пользователи голосуют за этот ответ, они должны знать, в чем проблема с вашим ярлыком. - person Hans Passant; 24.01.2013

Ваш подход неверен. Если быть точным, вы никогда не заснете, и чем больше будет загружена ваша машина, тем более неправильным станет ваш цикл сна.

Что вы должны делать, так это смотреть, сколько времени прошло, и соответственно адаптироваться. Хотя вращаться вокруг Sleep — плохая идея, «лучшие» способы сделать это будут значительно сложнее. Я буду предлагать простое решение.

  • Вызовите DateTime.Now.Ticks и сохраните его в переменной (startTick).
  • В своем цикле вызовите Sleep, как вы уже делаете.
  • Снова вызовите DateTime.Now.Ticks и вычтите из него startTick — это значение будет означать, сколько 100 наносекундных единиц времени прошло с тех пор, как вы начали затухание (timeSinceStart).
  • Используя timeSinceStart, вычислите, насколько вы должны исчезнуть за это время.
  • Повторяйте до тех пор, пока вы полностью не исчезнете.
person Doug65536    schedule 30.09.2011
comment
Если я вызову Sleep в цикле, как я это уже делаю, какая разница? - person Igby Largeman; 01.10.2011
comment
-1 DateTime Ticks имеет аналогичные проблемы, так как Now привязан к разрешению системных часов, например, 1\64 секунды. - person Tim Lloyd; 01.10.2011
comment
Вы используете DateTime.Now.Ticks, чтобы отслеживать, сколько времени Sleep ДЕЙСТВИТЕЛЬНО спал. Он что-то затухает, сверхвысокая точность тайминга ему не понадобится. Его проблема, по сути, в том, что сон может спать дольше, чем запрошено - мое решение состоит в том, чтобы рассчитать, как долго он фактически спал, чтобы он вел себя правильно, когда система находится под нагрузкой. Почему так важно, что он не может спать в течение очень точного периода времени - примите тот факт, что у вас может не быть 100% контроля над процессором, и сон не обязательно будет спать нужное количество времени. Мой алгоритм корректно исчезает в зависимости от прошедшего времени. - person Doug65536; 01.10.2011
comment
Та же схема применима ко всему, что связано со временем — подсчет того, сколько времени НА САМОМ ДЕЛЕ прошло. Скажем, вы собираетесь обновлять отображение КБ/сек каждую секунду, ожидаете ли вы, что ваш код обновления будет вызываться точно каждую секунду? Я гарантирую, что этого не будет. Вы пытаетесь обновлять его каждую секунду, и ваш алгоритм подсчитывает, сколько времени ДЕЙСТВИТЕЛЬНО прошло и сколько байтов было передано при расчете КБ/сек. - person Doug65536; 01.10.2011
comment
Вы утверждаете, что Ticks представляет, сколько 100-наносекундных интервалов прошло при запросе, это не так. - person Tim Lloyd; 01.10.2011
comment
И, кстати, вызывать timeBeginPeriod, чтобы попытаться взломать Sleep, чтобы быть точным, — очень и очень плохая идея. По сути, вы говорите, что вам следует застопорить всю операционную систему и быстрее сжечь батареи ноутбуков людей в тщетной попытке сделать Sleep Sleep точным количеством времени, когда специально задокументировано, что он часто спит дольше, чем требуется. Когда система находится под нагрузкой, Sleep гарантированно будет спать НАМНОГО дольше, чем запрошено, независимо от timeBeginPeriod или нет. - person Doug65536; 01.10.2011
comment
@chibacity Это так, если вы вычтете отметку времени начала затухания из значения, которое вы получили только что. Прямо из документов MSDN: один тик представляет собой сто наносекунд или одну десятимиллионную долю секунды. В миллисекунде 10 000 тиков. - person Doug65536; 01.10.2011
comment
@Doug Попробуйте написать цикл, который запрашивает Now.Ticks, вы увидите, что он обновляется только каждые 1\64 секунды. - person Tim Lloyd; 01.10.2011
comment
Прошедшее время определяется как количество времени, прошедшее с некоторой контрольной точки. Если вы сохраните количество тиков в начале операции, то вычитание начального количества тиков из текущего количества тиков даст количество тиков с этой точки. - person Doug65536; 01.10.2011
comment
Что вы спорите? Что ему нужно быть более точным, чем 1/64 секунды? Он что-то затухает. 1/64 более чем достаточно точно. Экран будет обновляться только с частотой около 60 Гц, вы говорите, что ему нужно обновлять видеопамять чаще, чем обновляется монитор? - person Doug65536; 01.10.2011
comment
Но это не будет с интервалом в 1\64 секунды, не так ли? Это будет повсюду. - person Tim Lloyd; 01.10.2011
comment
@chibacity Вы совершенно не понимаете сути. Меня не волнует, будут ли тики точны до 1/64 секунды или точны до 1 пикосекунды. Суть, которую я пытаюсь донести, заключается в том, что вы спите, чтобы не тратить ЦП впустую, и вы подсчитываете, сколько ФАКТИЧЕСКОГО времени прошло для каждого обновления. Вы можете использовать все, что вам нравится, чтобы получить прошедшее время. Я предпочитаю тики (единицы FILETIME), потому что это просто число, которое вы можете использовать для расчета, и пересечение полуночи или переход в другой день, месяц или год не имеет значения. - person Doug65536; 01.10.2011
comment
Я не упускаю ни одного момента, ОП ищет что-то для генерации регулярных событий, чтобы он мог выполнять плавную анимацию. Во всяком случае, используйте секундомер для тиков не DateTime.Now. - person Tim Lloyd; 01.10.2011
comment
Все борются за то, как сделать его СУПЕР точным, и никто не понимает, что это невозможно, и глупо пытаться. Это игровой движок, работающий со скоростью 60 кадров в секунду на графическом процессоре? Нет, это заболоченный код графического интерфейса Windows. Точность тайминга абсолютно не имеет значения, важно обеспечить правильное затухание, независимо от нагрузки на систему или скорости системы. Что, если бы это был код DirectX - тогда у вас все равно была бы та же проблема, что, если это графический процессор ноутбука и частота кадров ниже - вам нужно точно затухать с течением времени при 60 кадрах в секунду или 10 кадрах в секунду. - person Doug65536; 01.10.2011
comment
Секундомер — это ТОЧНО ЖЕ ВЕЩЬ, что и DateTime.Now. Какой член секундомера он собирается использовать, чтобы узнать прошедшее время? ElapsedTicks, если он умен - все остальные участники в любом случае рассчитываются на основе тиков, за исключением того, что они выполняют мусор с плавающей запятой, чтобы преобразовать его в эти единицы. - person Doug65536; 01.10.2011
comment
Вы очень ошибаетесь. Используйте секундомер. StopWatch — это не то же самое, что DateTime.Now. Секундомеры имеют высокое разрешение и представляют собой настоящие тики, а тики DateTime.Now имеют низкое разрешение и обновляются только каждые 1/64 секунды. Существует бесконечное количество информации об этом, если вы ее ищете. - person Tim Lloyd; 01.10.2011
comment
В конце концов я использовал это внутри своего цикла затухания: sw.Reset(); while (sw.ElapsedMilliseconds < ms) ;, где sw — секундомер, а ms — количество миллисекунд, которое я хочу ждать. Этот метод точен до миллисекунды. В этом случае не имеет значения, что я ем циклы. - person Igby Largeman; 01.10.2011
comment
@chibacity: мне не нужны события, достаточно простого ожидания. Мне не нужно уступать, потому что 1) это никогда не превышает полсекунды (всего) и 2) приложение не должно реагировать, когда это все равно происходит. - person Igby Largeman; 01.10.2011
comment
@Charles Конечно, я заключил события в кавычки, поскольку говорил образно, то есть концептуальные события, а не буквальные события. Секундомер — это то, что нужно использовать. Сжигание круга в тугой петле немного похоже на кувалду, но если это сработает для вас... - person Tim Lloyd; 01.10.2011