Бесконечный цикл, удобный для процессора

Написать бесконечный цикл просто:

while(true){
    //add whatever break condition here
}

Но это снизит производительность процессора. Этот поток выполнения будет потреблять как можно больше ресурсов процессора.

Каков наилучший способ снизить нагрузку на процессор? Добавление некоторых Thread.Sleep(n) должно помочь, но установка большого значения времени ожидания для метода Sleep() может указывать на то, что приложение не отвечает операционной системе.

Допустим, мне нужно выполнять задачу каждую минуту или около того в консольном приложении. Мне нужно, чтобы Main() работал в «бесконечном цикле», пока таймер запускает событие, которое выполнит эту работу. Я хотел бы оставить Main() с наименьшим влиянием на ЦП.

Какие методы вы предлагаете. Sleep() может быть в порядке, но, как я уже упоминал, это может указывать на то, что операционная система не отвечает потоку.

ПОСЛЕДНЕЕ РЕДАКТИРОВАНИЕ:

Я хочу лучше объяснить, что я ищу:

  1. Мне нужно консольное приложение, а не служба Windows. Консольные приложения могут имитировать службы Windows в системах Windows Mobile 6.x с помощью Compact Framework.

  2. Мне нужен способ поддерживать работу приложения, пока работает устройство Windows Mobile.

  3. Мы все знаем, что консольное приложение работает до тех пор, пока выполняется его статическая функция Main(), поэтому мне нужен способ предотвратить выход из функции Main().

  4. В особых ситуациях (например, при обновлении приложения) мне нужно запросить остановку приложения, поэтому мне нужно бесконечно зацикливаться и проверять какое-то условие выхода. Например, вот почему Console.ReadLine() мне ни к чему. Нет проверки условий выхода.

  5. Что касается вышеизложенного, я все еще хочу, чтобы функция Main() была как можно более дружественной к ресурсам. Оставим в стороне отпечаток функции, которая проверяет условие выхода.


person Adi    schedule 13.09.2011    source источник
comment
Thread.Sleep(0) наверное хорошо   -  person leppie    schedule 13.09.2011
comment
Почему бы тебе не Timer?   -  person Neil Knight    schedule 13.09.2011
comment
@leppie Sleep(0) отказывается от остатка текущего отрезка времени, но это все равно приводит к 100% загрузке ЦП. Так что здесь это не работает.   -  person CodesInChaos    schedule 13.09.2011
comment
Можете ли вы уточнить, что может указывать на то, что приложение не отвечает на проблему операционной системы? Вы уверены, что это проблема консольного приложения?   -  person Heinzi    schedule 13.09.2011
comment
Также ОС не заботится о неотвечающих потоках. Он заботится о неотвечающих потоках, которым принадлежит окно. А тут, похоже, не так.   -  person CodesInChaos    schedule 13.09.2011
comment
@CodeInChaos - Sleep(0) приводит к тому, что «все % не используются где-либо еще», что иногда довольно круто, особенно при создании приложения, которое использует «оставшиеся ресурсы» для выполнения своей работы (например, SETI@Home или что-то еще). .   -  person Jonathan Dickinson    schedule 13.09.2011
comment
Для SETI и подобных приложений я бы просто уменьшил приоритет своего процесса/потока, чтобы он не получал процессорное время, когда это нужно кому-то другому.   -  person CodesInChaos    schedule 13.09.2011
comment
Что должен делать ваш метод Main в этом бесконечном цикле? Возможно, main может запустить таймер, а затем вызвать Console.ReadLine().   -  person Chris Dunaway    schedule 13.09.2011


Ответы (11)


Чтобы избежать бесконечного цикла, просто используйте WaitHandle. Чтобы позволить процессу выйти из внешнего мира, используйте EventWaitHandle с уникальной строкой. Ниже приведен пример.

Если вы запускаете его в первый раз, он просто распечатывает сообщение каждые 10 секунд. Если вы тем временем запустите второй экземпляр программы, он сообщит другому процессу о том, что нужно изящно выйти, и также немедленно завершит работу. Использование ЦП для этого подхода: 0%

private static void Main(string[] args)
{
    // Create a IPC wait handle with a unique identifier.
    bool createdNew;
    var waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, "CF2D4313-33DE-489D-9721-6AFF69841DEA", out createdNew);
    var signaled = false;

    // If the handle was already there, inform the other process to exit itself.
    // Afterwards we'll also die.
    if (!createdNew)
    {
        Log("Inform other process to stop.");
        waitHandle.Set();
        Log("Informer exited.");

        return;
    }

    // Start a another thread that does something every 10 seconds.
    var timer = new Timer(OnTimerElapsed, null, TimeSpan.Zero, TimeSpan.FromSeconds(10));

    // Wait if someone tells us to die or do every five seconds something else.
    do
    {
        signaled = waitHandle.WaitOne(TimeSpan.FromSeconds(5));
        // ToDo: Something else if desired.
    } while (!signaled);

    // The above loop with an interceptor could also be replaced by an endless waiter
    //waitHandle.WaitOne();

    Log("Got signal to kill myself.");
}

private static void Log(string message)
{
    Console.WriteLine(DateTime.Now + ": " + message);
}

private static void OnTimerElapsed(object state)
{
    Log("Timer elapsed.");
}
person Oliver    schedule 11.09.2012
comment
Это намного, намного лучший ответ, чем большинство в этой теме, я перешел от использования ЦП ~ 30% с циклом в стиле while(true) к среднему использованию ЦП ~ 0,04% с помощью WaitHandle. - person Dillanm; 27.09.2016
comment
как выйти из цикла по событию, а затем запустить новый? - person shmnff; 11.07.2018
comment
@AntoshaShmonoff: чтобы выйти из цикла, вам нужно вызвать waitHandle.Set() из другого потока или процесса, затем заново создать дескриптор с помощью new EventWaitHandle(...) и снова выполнить цикл while. - person Oliver; 11.07.2018
comment
я просто не понимаю, как использовать эту строку var timer = new Timer(OnTimerElapsed, null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); - person Kob_24; 11.01.2019
comment
Он создает System.Threading.Timer. Параметры — это вызываемый делегат, параметры метода, сколько времени ждать до первого вызова и сколько времени должно быть между всеми последующими вызовами. - person Oliver; 11.01.2019

Вы можете использовать класс System.Threading.Timer, который предоставляет возможность выполнять обратный вызов асинхронно в заданный период времени.

public Timer(
    TimerCallback callback,
    Object state,
    int dueTime,
    int period
)

В качестве альтернативы есть класс System.Timers.Timer, который предоставляет прошедшее событие, которое возникает, когда заданный период времени истекло.

person sll    schedule 13.09.2011
comment
Время имеет минимальное разрешение 30 мс, что может быть для него проблемой, а может и не быть; по крайней мере, укажите это в своем ответе. - person Jonathan Dickinson; 13.09.2011
comment
@Jonathan Dickinson: я считаю, что с точки зрения нагрузки на ЦП такого дискретного значения достаточно, в отличие от 100% загрузки ЦП, но в любом случае спасибо за эту деталь. - person sll; 13.09.2011
comment
@Jonathan Dickinson: кстати, где вы нашли это значение? - person sll; 13.09.2011
comment
это должно быть общеизвестно, если вы используете многопоточность (поскольку это влияет на все: Environment.TickCount, Thread.Sleep и т. д.) - и, к сожалению, это 15,6 мс: ​​stackoverflow.com/questions/3744032/ - person Jonathan Dickinson; 13.09.2011
comment
Да, но почему 30 мс, а не ~ 15 мс? Есть ссылки на MSDN? - person sll; 13.09.2011
comment
Я читал об этом много лет назад, и тогда частота BIOS могла быть 33 Гц. Итак, в основном: «это, вероятно, ~ 15 мс, но может быть и 30 мс». Вы не можете привязать его к точному значению, потому что оно зависит от аппаратного обеспечения/BIOS (вам нужно вызвать timeGetDevCaps, чтобы узнать частоту) — печать Environment.TickCount на моей машине имеет разрешение 20–30 мс. msdn.microsoft.com/en-us/library /ms686298(v=vs.85).aspx - person Jonathan Dickinson; 13.09.2011
comment
давайте продолжим это обсуждение в чате - person Jonathan Dickinson; 13.09.2011
comment
@All По крайней мере, в моем случае разрешение таймера не проблема. Я хочу, чтобы что-то было сделано один раз примерно в 1 минуту. Но примите во внимание 1 час или 1 день или 1 неделю. Итак, почему я должен держать основной поток занятым, чтобы проверить всегда истинное условие, такое как while(true), вместо того, чтобы переводить поток в спящий режим на необходимое количество времени? - person Adi; 14.09.2011
comment
@sll Это не проблема таймера, а скорее проблема того, как поддерживать работу функции Main (), не потребляя процессорное время и не ожидая срабатывания других других таймеров. Я также хотел бы выйти из приложения (то есть из функции Main()), если выполняется определенное условие (например: получена команда сокета и т. д.). - person Adi; 14.09.2011
comment
@Ai: вы можете использовать ManualResetEvent + WaitOne() для такого сценария - person sll; 15.09.2011

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

person anothershrubery    schedule 13.09.2011
comment
Я думаю, что накладные расходы на запуск нового экземпляра программы .net каждую минуту немного высоки. - person CodesInChaos; 13.09.2011
comment
Может быть немного больше накладных расходов, но я бы все равно избегал бесконечного цикла или службы. Вы можете получить лучшее из обоих миров и запускать задачу каждые 5 минут, но у задачи есть таймер, который запускает действие каждую минуту в течение 5 минут. Это означает меньшие накладные расходы из-за запуска программы .Net каждую минуту и ​​меньшую вероятность утечки памяти, которая может заблокировать ЦП и повлиять на другие процессы и/или пользователей. - person anothershrubery; 13.09.2011

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

Другой вариант — написать службу Windows, которая работает в фоновом режиме. Служба может использовать простой класс Alarm, подобный следующему в MSDN:

http://msdn.microsoft.com/en-us/library/wkzf914z%28v=VS.90%29.aspx#Y2400

Вы можете использовать его для периодического запуска вашего метода. Внутри этого класса Alarm используется таймер:

http://msdn.microsoft.com/en-us/library/system.timers.timer.aspx

Просто установите интервал таймера правильно (например, 60000 миллисекунд), и он будет периодически вызывать событие Elapsed. Прикрепите обработчик событий к событию Elapsed для выполнения вашей задачи. Нет необходимости реализовывать «бесконечный цикл» только для того, чтобы поддерживать работоспособность приложения. За вас этим занимается сервис.

person Christophe Geers    schedule 13.09.2011
comment
Судя по всему, планировщик заданий может запускаться как минимум каждые 5 минут. - person Markive; 23.04.2014
comment
В последнее время я больше люблю использовать Quartz.NET для планирования задач. Создание службы Windows с помощью Topshelf и планирование заданий с помощью Quartz.NET. - person Christophe Geers; 06.06.2014
comment
Хотел бы я увидеть Quartz.Net до того, как начал свой недавний проект.. На бумаге это выглядит потрясающе.. - person Markive; 10.06.2014

Я сделал это для приложения, которое должно было обрабатывать файлы по мере того, как они помещались в папку. Лучше всего использовать таймер (как было предложено) с Console.ReadLine() в конце «основного» без включения цикла.

Теперь ваше беспокойство по поводу остановки приложения:

Я также сделал это с помощью какого-то элементарного «файлового» монитора. Простое создание файла «quit.txt» в корневой папке приложения (либо моей программой, либо другим приложением, которое может запросить его остановку) заставит приложение закрыться. Полукод:

<do your timer thing here>
watcher = new FileSystemWatcher();
watcher.Path = <path of your application or other known accessible path>;
watcher.Changed += new FileSystemEventHandler(OnNewFile);
Console.ReadLine();

OnNewFile может быть примерно таким:

private static void OnNewFile(object source, FileSystemEventArgs e)
{
    if(System.IO.Path.GetFileName(e.FullPath)).ToLower()=="quit.txt")
        ... remove current quit.txt
        Environment.Exit(1);
}

Теперь вы упомянули, что это (или может быть) для мобильного приложения? Возможно, у вас нет наблюдателя за файловой системой. В этом случае, возможно, вам просто нужно «убить» процесс (вы сказали: «В особых ситуациях (например, при обновлении приложения) мне нужно запросить остановку приложения». просто убить процесс)

person Nelson Rodriguez    schedule 18.05.2012

Мне кажется, вы хотите, чтобы Main() входил в прерываемый цикл. Чтобы это произошло, где-то должно быть задействовано несколько потоков (или ваш цикл должен периодически опрашиваться; хотя я не обсуждаю это решение здесь). Либо другой поток в том же приложении, либо поток в другом процессе должен иметь возможность сигнализировать вашему циклу Main() о том, что он должен завершиться.

Если это так, то я думаю, вы хотите использовать ManualResetEvent или EventWaitHandle . Вы можете дождаться этого события, пока оно не будет передано (и сигнализация должна быть выполнена другим потоком).

Например:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            startThreadThatSignalsTerminatorAfterSomeTime();
            Console.WriteLine("Waiting for terminator to be signalled.");
            waitForTerminatorToBeSignalled();
            Console.WriteLine("Finished waiting.");
            Console.ReadLine();
        }

        private static void waitForTerminatorToBeSignalled()
        {
            _terminator.WaitOne(); // Waits forever, but you can specify a timeout if needed.
        }

        private static void startThreadThatSignalsTerminatorAfterSomeTime()
        {
            // Instead of this thread signalling the event, a thread in a completely
            // different process could do so.

            Task.Factory.StartNew(() =>
            {
                Thread.Sleep(5000);
                _terminator.Set();
            });
        }

        // I'm using an EventWaitHandle rather than a ManualResetEvent because that can be named and therefore
        // used by threads in a different process. For intra-process use you can use a ManualResetEvent, which 
        // uses slightly fewer resources and so may be a better choice.

        static readonly EventWaitHandle _terminator = new EventWaitHandle(false, EventResetMode.ManualReset, "MyEventName");
    }
}
person Matthew Watson    schedule 11.09.2012

Вы можете использовать Begin-/End-Invoke для перехода к другим потокам. Например.

public static void ExecuteAsyncLoop(Func<bool> loopBody)
{
    loopBody.BeginInvoke(ExecuteAsyncLoop, loopBody);
}

private static void ExecuteAsyncLoop(IAsyncResult result)
{
    var func = ((Func<bool>)result.AsyncState);
    try
    {
        if (!func.EndInvoke(result))
            return;
    }
    catch
    {
        // Do something with exception.
        return;
    }

    func.BeginInvoke(ExecuteAsyncLoop, func);
}

Вы бы использовали его как таковой:

ExecuteAsyncLoop(() =>
    {
        // Do something.
        return true; // Loop indefinitely.
    });

Это использовало 60% одного ядра на моей машине (полностью пустой цикл). Кроме того, вы можете использовать этот код (Source) в теле вашего петля:

private static readonly bool IsSingleCpuMachine = (Environment.ProcessorCount == 1);
[DllImport("kernel32", ExactSpelling = true)]
private static extern void SwitchToThread();

private static void StallThread()
{
    // On a single-CPU system, spinning does no good
    if (IsSingleCpuMachine) SwitchToThread();
    // Multi-CPU system might be hyper-threaded, let other thread run
    else Thread.SpinWait(1);
}

while (true)
{
    // Do something.
    StallThread();
}

Это использовало 20% одного ядра на моей машине.

person Jonathan Dickinson    schedule 13.09.2011
comment
Я считаю, что вам не нужно проверять, является ли текущая машина одноядерной или многоядерной в каждом цикле цикла, проверяйте ее один раз при запуске приложения. - person sll; 15.09.2011
comment
@sll static readonly с инициализатором - значит проверяется один раз. - person Jonathan Dickinson; 15.09.2011

Чтобы пояснить комментарий, сделанный CodeInChaos:

Вы можете установить заданный приоритет потока. Потоки планируются для выполнения на основе их приоритета. Алгоритм планирования, используемый для определения порядка выполнения потоков, зависит от операционной системы. Все потоки по умолчанию имеют «нормальный» приоритет, но если вы установите низкий уровень цикла; он не должен красть время у потоков, установленных в нормальном режиме.

person Tremmors    schedule 13.09.2011

Подход Timer, вероятно, ваш лучший выбор, но поскольку вы упомянули Thread.Sleep, есть интересный Thread.SpinWait или SpinWait struct альтернатива для аналогичных проблем, которая иногда может быть лучше, чем короткие вызовы Thread.Sleep.

Также см. этот вопрос: Какова цель метода Thread.SpinWait?

person Zaid Masud    schedule 11.09.2012

Здесь много «продвинутых» ответов, но для большинства должно быть достаточно простого использования Thread.Sleep(lowvalue).

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

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

Итак, что-то вроде этого — простое и легкое решение с 0% ЦП для приложения без пользовательского интерфейса.

static void Main(string[] args)
{
    bool wait = true;
    int sleepLen = 1 * 60 * 1000; // 1 minute
    while (wait)
    {

        //... your code

        var sleepCount = sleepLen / 100;
        for (int i = 0; i < sleepCount; i++)
        {
            Thread.Sleep(100);
        }
    }
}

Относительно того, как ОС определяет, что приложение не отвечает. Я не знаю никаких других тестов, кроме UI-приложений, где есть методы проверки того, обрабатывает ли UI-поток UI-код. Спящий поток в пользовательском интерфейсе будет легко обнаружен. Windows «Приложение не отвечает» использует простой собственный метод «SendMessageTimeout», чтобы определить, есть ли у приложения неотвечающий пользовательский интерфейс.

Любой бесконечный цикл в приложении пользовательского интерфейса всегда должен выполняться в отдельном потоке.

person Wolf5    schedule 20.05.2016

Чтобы консольные приложения продолжали работать, просто добавьте Console.ReadLine() в конец кода в Main().

Если пользователь не должен иметь возможность завершить приложение, вы можете сделать это с помощью цикла, подобного следующему:

while (true){
   Console.ReadLine();
}
person Peter    schedule 13.09.2011
comment
Похоже, его приложению лучше не иметь пользовательского интерфейса. Кстати, как только Console.ReadLine() будет достигнута в первый раз... приложение будет просто ждать нажатия клавиши, пока не начнется следующая итерация. Кто-то должен был бы нажимать клавишу каждую минуту.... похоже на серию "Остаться в живых". - person Christophe Geers; 13.09.2011
comment
@Peter, я все еще хочу иметь некоторый контроль над моментом, когда приложение будет изящно закрыто. Таким образом, цикл кажется хорошим кандидатом - person Adi; 14.09.2011
comment
Как узнать, когда выйти из приложения? Возможно, вы можете выполнить эту проверку в обратном вызове таймера. Затем вы можете закрыть приложение, вызвав Environment.Exit(0). - person Peter; 19.09.2011