线程在后台时在 OS X 上休眠时间过长

Thread Sleeps Too Long on OS X When In Background

我在 OS X 上的 Qt 应用程序中有一个后台线程用于收集数据。线程应该在每次迭代之间休眠 100 毫秒,但它并不总是正常工作。当应用程序是最顶层的 OS X 应用程序时,睡眠正常。但如果不是,则在运行大约一分钟后,睡眠会持续任意时间,最多约 10 秒。

这是一个演示问题的简单 Cocoa 应用程序(请注意 .mm for 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 时的行为是相同的(在代码中注释掉)。

您看到的是 Apple 的 power-saving App Nap 功能的效果。

您可以通过 运行 Apple 的 Activity 管理程序并查看 "App Nap" 列(您可能需要 right-click 在进程 table 的 header-bar 中使该列首先可见)。如果您的程序是 app-napped,您将在 table.

中程序行的该列中看到 "Yes"

如果您想以编程方式为您的程序禁用 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"];
   }
}

这是 App Nap 造成的,我可以在 macOS 10.13.4 上重现该问题。当 reproduce 设置为 true 时,下面的示例重现了它。当设置为 false 时,LatencyCriticalLock 负责确保 App Nap 处于非活动状态。

另请注意,睡眠并不能确保您的操作 运行 符合规定的时间 - 如果操作需要任何时间,即使由于系统负载和延迟,该时间也会比预期的要长。大多数平台上的系统计时器确保平均周期是正确的。 sleep-based 节奏总是 运行 比预期的时间长。

// https://github.com/KubaO/Whosebugn/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 
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();
}

在这种情况下可行的替代解决方案是使用 c 函数增加线程优先级 pthread_setschedparam,如果出于某种原因您想要一个应用程序,其 Naps 具有不具有后台线程的应用程序。

  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);