线程在后台运行时在OS X上睡眠时间过长

时间:2018-04-05 16:16:28

标签: multithreading macos qt sleep

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

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

3 个答案:

答案 0 :(得分:3)

您所看到的是Apple省电App Nap功能的效果。

你可以通过运行Apple的Activity Manager程序并查看&#34; App Nap&#34;来验证它是App Nap是罪魁祸首。列(您可能需要右键单击流程表的标题栏以使该列首先可见)。如果你的程序被app-napped,你会看到&#34;是&#34;在该列中为您的程序在表中的行。

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

答案 1 :(得分:1)

这是由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://stackoverflow.com/a/49679984/1329652
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();
}

答案 2 :(得分:0)

在这种情况下工作的另一种解决方案是使用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);