当在后台时,OS X中的线程休眠时间过长

3
我在一款Qt App中有一个后台线程用于收集数据。该线程应该在每次迭代之间睡眠100毫秒,但它并不总是正常工作。当该App为OS X最高级别的应用程序时,睡眠正常工作。但当它不是最高级别的应用程序时,睡眠的持续时间不确定,可能长达约10秒,在大约一分钟的操作后。
下面是一个简单的Cocoa App,演示了这个问题(注意.mm表示objc++)
AppDelegate.mm:
#import "AppDelegate.h"
#include <iostream>
#include <thread>
#include <libgen.h>
using namespace std::chrono;

#define DEFAULT_COLLECTOR_SAMPLING_FREQUENCY 10

namespace Helpers {
  uint64_t time_ms() {
    return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
  }
}

std::thread _collectorThread;
bool _running;

@interface AppDelegate ()

@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
  _running = true;
  uint64_t start = Helpers::time_ms();
  _collectorThread =
  std::thread (
               [&]{
                 while(_running) {
                   uint64_t t1, t2;
                   t1 = Helpers::time_ms();
                   std::this_thread::sleep_for((std::chrono::duration<int, std::milli>)(1000 / DEFAULT_COLLECTOR_SAMPLING_FREQUENCY));
                   t2 = Helpers::time_ms();
                   std::cout << (int)((t1 - start)/1000) << " TestSleep: sleep lasted " << t2-t1 << " ms" << std::endl;
                 }
               });    
}


- (void)applicationWillTerminate:(NSNotification *)aNotification {
  _running = false;
  _collectorThread.join();
}


@end

标准输出:

0 TestSleep: sleep lasted 102 ms.  // Window is in background
0 TestSleep: sleep lasted 101 ms.  // behind Xcode window
0 TestSleep: sleep lasted 104 ms
0 TestSleep: sleep lasted 104 ms
0 TestSleep: sleep lasted 105 ms
0 TestSleep: sleep lasted 105 ms
0 TestSleep: sleep lasted 105 ms
0 TestSleep: sleep lasted 104 ms
0 TestSleep: sleep lasted 102 ms
0 TestSleep: sleep lasted 102 ms
1 TestSleep: sleep lasted 105 ms
1 TestSleep: sleep lasted 105 ms
1 TestSleep: sleep lasted 104 ms
1 TestSleep: sleep lasted 101 ms
1 TestSleep: sleep lasted 101 ms
1 TestSleep: sleep lasted 100 ms
...
...
52 TestSleep: sleep lasted 102 ms
52 TestSleep: sleep lasted 101 ms
52 TestSleep: sleep lasted 104 ms
52 TestSleep: sleep lasted 105 ms
52 TestSleep: sleep lasted 104 ms
52 TestSleep: sleep lasted 100 ms
52 TestSleep: sleep lasted 322 ms. // after ~1 minute,
53 TestSleep: sleep lasted 100 ms. // sleep gets way off
53 TestSleep: sleep lasted 499 ms
53 TestSleep: sleep lasted 1093 ms
54 TestSleep: sleep lasted 1086 ms
56 TestSleep: sleep lasted 1061 ms
57 TestSleep: sleep lasted 1090 ms
58 TestSleep: sleep lasted 1100 ms
59 TestSleep: sleep lasted 1099 ms
60 TestSleep: sleep lasted 1096 ms
61 TestSleep: sleep lasted 390 ms
61 TestSleep: sleep lasted 100 ms
61 TestSleep: sleep lasted 102 ms   // click on app window
62 TestSleep: sleep lasted 102 ms  // to bring it to foreground
62 TestSleep: sleep lasted 105 ms

另一方面,以下完整的程序不会减缓速度:

#include <iostream>
#include <thread>
#include <libgen.h>
using namespace std::chrono;

#define DEFAULT_COLLECTOR_SAMPLING_FREQUENCY 10

namespace Helpers {
    uint64_t time_ms() {
        return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
    }
}

int main(int argc, char *argv[])
{
    bool _running = true;
    uint64_t start = Helpers::time_ms();
    std::thread collectorThread = std::thread (
                [&]{
        while(_running) {
            uint64_t t1, t2;
            t1 = Helpers::time_ms();
            std::this_thread::sleep_for((std::chrono::duration<int, std::milli>)(1000 / DEFAULT_COLLECTOR_SAMPLING_FREQUENCY));
            t2 = Helpers::time_ms();
            std::cout << (int)((t1 - start)/1000) << " TestSleep: sleep lasted " << t2-t1 << " ms" << std::endl;
        }
    });
    collectorThread.join();
    return 0;
}

// clang++ -std=c++14 -o testc++ main.cpp 

标准输出:

0 TestSleep: sleep lasted 100 ms
0 TestSleep: sleep lasted 101 ms
0 TestSleep: sleep lasted 105 ms
0 TestSleep: sleep lasted 105 ms
0 TestSleep: sleep lasted 100 ms
0 TestSleep: sleep lasted 100 ms
0 TestSleep: sleep lasted 101 ms
0 TestSleep: sleep lasted 104 ms
0 TestSleep: sleep lasted 101 ms
0 TestSleep: sleep lasted 104 ms
1 TestSleep: sleep lasted 102 ms
1 TestSleep: sleep lasted 101 ms
1 TestSleep: sleep lasted 101 ms
1 TestSleep: sleep lasted 101 ms
1 TestSleep: sleep lasted 100 ms
...
...
99 TestSleep: sleep lasted 101 ms
99 TestSleep: sleep lasted 105 ms
99 TestSleep: sleep lasted 104 ms
100 TestSleep: sleep lasted 104 ms
100 TestSleep: sleep lasted 101 ms
100 TestSleep: sleep lasted 104 ms

我的原始应用是QML,也显示出相同的减速行为。

TestSleep.pro:

QT += quick
CONFIG += c++11
SOURCES += \
        main.cpp
RESOURCES += qml.qrc

main.qml:

import QtQuick 2.9
import QtQuick.Controls 2.2

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: qsTr("Scroll")

    ScrollView {
        anchors.fill: parent

        ListView {
            width: parent.width
            model: 20
            delegate: ItemDelegate {
                text: "Item " + (index + 1)
                width: parent.width
            }
        }
    }
}

main.cpp:

#define DEFAULT_COLLECTOR_SAMPLING_FREQUENCY 10

namespace Helpers {
    uint64_t time_ms() {
        return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
    }
}

int main(int argc, char *argv[])
{
    bool _running = true;
    QThread *collectorThread = QThread::create(
//    std::thread collectorThread = std::thread (
                [&]{
        while(_running) {
            uint64_t t1;
            t1 = Helpers::time_ms();
            QThread::msleep(1000 / DEFAULT_COLLECTOR_SAMPLING_FREQUENCY);
//            std::this_thread::sleep_for((std::chrono::duration<int, std::milli>)(1000 / DEFAULT_COLLECTOR_SAMPLING_FREQUENCY));
            t1 = Helpers::time_ms() - t1;
            std::cout << "TestUSleep: sleep lasted " << t1 << " ms" << std::endl;
        }
    });
    collectorThread->start();
    collectorThread->setPriority(QThread::TimeCriticalPriority);

    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    if (engine.rootObjects().isEmpty())
        return -1;

    int returnValue = app.exec();
//    collectorThread.join();
    collectorThread->quit();
    collectorThread->wait();
    collectorThread->deleteLater();
    return returnValue;
}

标准输出:

0 TestSleep: sleep lasted 101 ms
0 TestSleep: sleep lasted 100 ms
0 TestSleep: sleep lasted 101 ms
0 TestSleep: sleep lasted 100 ms
0 TestSleep: sleep lasted 102 ms
0 TestSleep: sleep lasted 100 ms
0 TestSleep: sleep lasted 102 ms
0 TestSleep: sleep lasted 101 ms
0 TestSleep: sleep lasted 101 ms
0 TestSleep: sleep lasted 101 ms
1 TestSleep: sleep lasted 100 ms
1 TestSleep: sleep lasted 101 ms
1 TestSleep: sleep lasted 101 ms
1 TestSleep: sleep lasted 101 ms
...
...
63 TestSleep: sleep lasted 100 ms
63 TestSleep: sleep lasted 101 ms
63 TestSleep: sleep lasted 102 ms
63 TestSleep: sleep lasted 101 ms
63 TestSleep: sleep lasted 101 ms
63 TestSleep: sleep lasted 7069 ms  # slows down
70 TestSleep: sleep lasted 235 ms
70 TestSleep: sleep lasted 10100 ms
80 TestSleep: sleep lasted 7350 ms
88 TestSleep: sleep lasted 10100 ms
98 TestSleep: sleep lasted 3566 ms
101 TestSleep: sleep lasted 100 ms
102 TestSleep: sleep lasted 3242 ms
105 TestSleep: sleep lasted 2373 ms
107 TestSleep: sleep lasted 100 ms  # click on main window
107 TestSleep: sleep lasted 101 ms  # to put app on top
107 TestSleep: sleep lasted 101 ms  # and back to normal
107 TestSleep: sleep lasted 101 ms  # behavior
108 TestSleep: sleep lasted 101 ms
108 TestSleep: sleep lasted 102 ms
...

使用 std::thread 而非 QThread 时,行为相同(在代码中已注释掉)。

2
尝试增加线程的优先级。 - zneak
请查看 main.cpp 文件。它已经设置为最高优先级。 - Colin
Linux没有每个线程的优先级。如果您的线程没有运行,setPriority不起作用。 - zneak
让我重新表述一下:Linux 上不可能存在问题,因为它没有每个线程的优先级,另外,你的 setPriority 调用是无效的,因为在调用 start(它还接受一个可选的优先级参数)之前,它是一个空操作。 - zneak
@zneak 更新:虽然使用QThread::setPriority设置线程优先级没有效果,但是通过pthread_setschedparam设置优先级有效。感谢! - Colin
显示剩余11条评论
3个回答

8
你所看到的是苹果公司节能 App Nap 功能的影响。
您可以通过运行苹果的活动监视器程序,并查看“应用程序节能”列(您可能需要右键单击进程表的标题栏才能使该列可见)来验证 App Nap 是罪魁祸首。如果您的程序正在被 app-napped,您将在表格中看到您的程序行中的“是”。
如果您想为程序编程地禁用 app-nap,则可以将此 Objective-C++ 文件放入程序中,并在 main() 顶部调用 disable_app_nap() 函数。
#import <Foundation/Foundation.h>
#import <Foundation/NSProcessInfo.h>

void disable_app_nap(void)
{
   if ([[NSProcessInfo processInfo] respondsToSelector:@selector(beginActivityWithOptions:reason:)])
   {
      [[NSProcessInfo processInfo] beginActivityWithOptions:0x00FFFFFF reason:@"Not sleepy and don't want to nap"];
   }
}

谢谢,好好知道。听起来这对任何时间关键的OS X应用程序都是一个好的实践,所以我会尝试这个方法,看看问题是否得到解决。 - Colin

2
这是由于App Nap引起的问题,我可以在macOS 10.13.4上重现此问题。下面的示例在将reproduce设置为true时会复制它。当设置为false时,LatencyCriticalLock会确保App Nap未激活。

另外,请注意,睡眠并不确保您的操作以指定的时间间隔运行 - 如果操作需要任何时间,甚至由于系统负载和延迟,该时间间隔将比预期更长。大多数平台上的系统计时器确保平均周期正确。基于睡眠的节奏始终以比所需时间间隔更长的周期运行。
// https://github.com/KubaO/stackoverflown/tree/master/questions/appnap-49677034
#if !defined(__APPLE__)
#error This example is for macOS
#endif
#include <QtWidgets>
#include <mutex>
#include <objc/runtime.h>
#include <objc/message.h>

// see https://dev59.com/VYPba4cB1Zd3GeqPrFoD#49679984
namespace detail { struct LatencyCriticalLock {
   int count = {};
   id activity = {};
   id processInfo = {};
   id reason = {};
   std::unique_lock<std::mutex> mutex_lock() {
      init();
      return std::unique_lock<std::mutex>(mutex);
   }
private:
   std::mutex mutex;
   template <typename T> static T check(T i) {
      return (i != nil) ? i : throw std::runtime_error("LatencyCrticalLock init() failed");
   }
   void init() {
      if (processInfo != nil) return;
      auto const NSProcessInfo = check(objc_getClass("NSProcessInfo"));
      processInfo = check(objc_msgSend((id)NSProcessInfo, sel_getUid("processInfo")));
      reason = check(objc_msgSend((id)objc_getClass("NSString"), sel_getUid("alloc")));
      reason = check(objc_msgSend(reason, sel_getUid("initWithUTF8String:"), "LatencyCriticalLock"));
   }
}; }

class LatencyCriticalLock {
   static detail::LatencyCriticalLock d;
   bool locked = {};
public:
   struct NoLock {};
   LatencyCriticalLock &operator=(const LatencyCriticalLock &) = delete;
   LatencyCriticalLock(const LatencyCriticalLock &) = delete;
   LatencyCriticalLock() { lock(); }
   explicit LatencyCriticalLock(NoLock) {}
   ~LatencyCriticalLock() { unlock(); }
   void lock() {
      if (locked) return;
      auto l = d.mutex_lock();
      assert(d.count >= 0);
      if (!d.count) {
         assert(d.activity == nil);
         /* Start activity that tells App Nap to mind its own business: */
         /* NSActivityUserInitiatedAllowingIdleSystemSleep */
         /* | NSActivityLatencyCritical */
         d.activity = objc_msgSend(d.processInfo, sel_getUid("beginActivityWithOptions:reason:"),
                                   0x00FFFFFFULL | 0xFF00000000ULL, d.reason);
         assert(d.activity != nil);
      }
      d.count ++;
      locked = true;
      assert(d.count > 0 && locked);
   }
   void unlock() {
      if (!locked) return;
      auto l = d.mutex_lock();
      assert(d.count > 0);
      if (d.count == 1) {
         assert(d.activity != nil);
         objc_msgSend(d.processInfo, sel_getUid("endActivity:"), d.activity);
         d.activity = nil;
         locked = false;
      }
      d.count--;
      assert(d.count > 0 || d.count == 0 && !locked);
   }
   bool isLocked() const { return locked; }
};

detail::LatencyCriticalLock LatencyCriticalLock::d;

int main(int argc, char *argv[]) {
   struct Thread : QThread {
      bool reproduce = {};
      void run() override {
         LatencyCriticalLock lock{LatencyCriticalLock::NoLock()};
         if (!reproduce)
            lock.lock();
         const int period = 100;
         QElapsedTimer el;
         el.start();
         QTimer timer;
         timer.setTimerType(Qt::PreciseTimer);
         timer.start(period);
         connect(&timer, &QTimer::timeout, [&el]{
            auto const duration = el.restart();
            if (duration >= 1.1*period) qWarning() << duration << " ms";
         });
         QEventLoop().exec();
      }
      ~Thread() {
         quit();
         wait();
      }
   } thread;

   QApplication app{argc, argv};
   thread.reproduce = false;
   thread.start();

   QPushButton msg;
   msg.setText("Click to close");
   msg.showMinimized();
   msg.connect(&msg, &QPushButton::clicked, &msg, &QWidget::close);

   return app.exec();
}

感谢提供 QThread 的示例。是的,我知道 sleep 不会给我精确的时间。我正在定期处理一些数据,由于数据已缓冲,刷新速率不需要精确。 - Colin

0
一个在这种情况下起作用的替代方案是使用C函数pthread_setschedparam来增加线程优先级,如果您想要一个不休眠带有后台线程的应用程序。
  int priority_max = sched_get_priority_max(SCHED_RR);
  struct sched_param sp;
  sp.sched_priority = priority_max;
  pthread_setschedparam(_collectorThread.native_handle(), SCHED_RR, &sp);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接