web-dev-qa-db-fra.com

Comment créer un NSTimer sur un thread d'arrière-plan?

J'ai une tâche qui doit être effectuée toutes les 1 seconde. Actuellement, j'ai un NSTimer qui tire plusieurs fois toutes les 1 s. Comment puis-je déclencher le minuteur dans un thread d'arrière-plan (non-thread UI)?

Je pourrais avoir le feu NSTimer sur le thread principal puis utiliser NSBlockOperation pour distribuer un thread d'arrière-plan, mais je me demande s'il existe un moyen plus efficace de le faire.

64
David

Le minuteur devrait être installé dans une boucle d'exécution fonctionnant sur un thread d'arrière-plan déjà en cours d'exécution. Ce thread devrait continuer à exécuter la boucle d'exécution pour que le minuteur se déclenche réellement. Et pour que ce thread d'arrière-plan puisse continuer à déclencher d'autres événements de minuterie, il devrait de toute façon générer un nouveau thread pour gérer les événements (en supposant, bien sûr, que le traitement que vous effectuez prend beaucoup de temps).

Pour tout ce que ça vaut, je pense que la gestion des événements du minuteur en générant un nouveau thread en utilisant Grand Central Dispatch ou NSBlockOperation est une utilisation parfaitement raisonnable de votre thread principal.

19
Steven Fisher

Si vous en avez besoin pour que les minuteries fonctionnent toujours lorsque vous faites défiler vos vues (ou cartes), vous devez les planifier sur un mode de boucle d'exécution différent. Remplacez votre minuterie actuelle:

[NSTimer scheduledTimerWithTimeInterval:0.5
                                 target:self
                               selector:@selector(timerFired:)
                               userInfo:nil repeats:YES];

Avec celui-ci:

NSTimer *timer = [NSTimer timerWithTimeInterval:0.5
                                           target:self
                                         selector:@selector(timerFired:)
                                         userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

Pour plus de détails, consultez cet article de blog: Le suivi des événements arrête NSTimer

EDIT: deuxième bloc de code, le NSTimer s'exécute toujours sur le thread principal, toujours sur la même boucle d'exécution que les scrollviews. La différence est le mode de boucle d'exécution . Consultez le billet de blog pour une explication claire.

106
Marius

Si vous voulez passer au GCD pur et utiliser une source de répartition, Apple a un exemple de code pour cela dans leur Guide de programmation concurrentielle :

dispatch_source_t CreateDispatchTimer(uint64_t interval, uint64_t leeway, dispatch_queue_t queue, dispatch_block_t block)
{
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    if (timer)
    {
        dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
        dispatch_source_set_event_handler(timer, block);
        dispatch_resume(timer);
    }
    return timer;
}

Swift 3:

func createDispatchTimer(interval: DispatchTimeInterval,
                         leeway: DispatchTimeInterval,
                         queue: DispatchQueue,
                         block: @escaping ()->()) -> DispatchSourceTimer {
    let timer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags(rawValue: 0),
                                               queue: queue)
    timer.scheduleRepeating(deadline: DispatchTime.now(),
                            interval: interval,
                            leeway: leeway)

    // Use DispatchWorkItem for compatibility with iOS 9. Since iOS 10 you can use DispatchSourceHandler
    let workItem = DispatchWorkItem(block: block)
    timer.setEventHandler(handler: workItem)
    timer.resume()
    return timer
}

Vous pouvez ensuite configurer votre événement de minuterie d'une seconde en utilisant un code comme celui-ci:

dispatch_source_t newTimer = CreateDispatchTimer(1ull * NSEC_PER_SEC, (1ull * NSEC_PER_SEC) / 10, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // Repeating task
});

assurez-vous de stocker et de libérer votre minuterie une fois terminé, bien sûr. Ce qui précède vous donne une marge de manœuvre de 1/10 de seconde sur le déclenchement de ces événements, que vous pouvez resserrer si vous le souhaitez.

49
Brad Larson

Cela devrait fonctionner,

Il répète une méthode toutes les 1 seconde dans une file d'attente en arrière-plan sans utiliser NSTimers :)

- (void)methodToRepeatEveryOneSecond
{
    // Do your thing here

    // Call this method again using GCD 
    dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    double delayInSeconds = 1.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
    dispatch_after(popTime, q_background, ^(void){
        [self methodToRepeatEveryOneSecond];
    });
}

Si vous êtes dans la file d'attente principale et que vous souhaitez appeler la méthode ci-dessus, vous pouvez le faire pour qu'elle devienne une file d'attente en arrière-plan avant son exécution :)

dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_async(q_background, ^{
    [self methodToRepeatEveryOneSecond];
});

J'espère que ça aide

15
nacho4d

Pour Swift 3.0,

La réponse de Tikhonv n'explique pas trop. Ici ajoute une partie de ma compréhension.

Pour faire court, voici le code. Il est [~ # ~] différent [~ # ~] du code de Tikhonv à l'endroit où je crée la minuterie. Je crée le temporisateur en utilisant le constructeur et l'ajoute dans la boucle. Je pense que la fonction ScheduleTimer ajoutera la minuterie à RunLoop du thread principal. Il est donc préférable de créer une minuterie à l'aide du constructeur.

class RunTimer{
  let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent)
  let timer: Timer?

  private func startTimer() {
    // schedule timer on background
    queue.async { [unowned self] in
      if let _ = self.timer {
        self.timer?.invalidate()
        self.timer = nil
      }
      let currentRunLoop = RunLoop.current
      self.timer = Timer(timeInterval: self.updateInterval, target: self, selector: #selector(self.timerTriggered), userInfo: nil, repeats: true)
      currentRunLoop.add(self.timer!, forMode: .commonModes)
      currentRunLoop.run()
    }
  }

  func timerTriggered() {
    // it will run under queue by default
    debug()
  }

  func debug() {
     // print out the name of current queue
     let name = __dispatch_queue_get_label(nil)
     print(String(cString: name, encoding: .utf8))
  }

  func stopTimer() {
    queue.sync { [unowned self] in
      guard let _ = self.timer else {
        // error, timer already stopped
        return
      }
      self.timer?.invalidate()
      self.timer = nil
    }
  }
}

Créer une file d'attente

Tout d'abord, créez une file d'attente pour faire fonctionner le minuteur en arrière-plan et stockez cette file d'attente en tant que propriété de classe afin de la réutiliser pour arrêter le minuteur. Je ne sais pas si nous devons utiliser la même file d'attente pour démarrer et arrêter, la raison pour laquelle je l'ai fait est que j'ai vu un message d'avertissement ici .

La classe RunLoop n'est généralement pas considérée comme thread-safe et ses méthodes ne doivent être appelées que dans le contexte du thread actuel. Vous ne devez jamais essayer d'appeler les méthodes d'un objet RunLoop s'exécutant dans un thread différent, car cela pourrait entraîner des résultats inattendus.

J'ai donc décidé de stocker la file d'attente et d'utiliser la même file d'attente pour le minuteur pour éviter les problèmes de synchronisation.

Créez également un minuteur vide et stocké dans la variable de classe également. Rendez-le facultatif pour pouvoir arrêter la minuterie et la régler sur zéro.

class RunTimer{
  let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent)
  let timer: Timer?
}

Démarrer la minuterie

Pour démarrer la minuterie, appelez d'abord async à partir de DispatchQueue. Il est ensuite recommandé de vérifier d'abord si le chronomètre a déjà démarré. Si la variable de temporisation n'est pas nulle, invalidez-la et définissez-la sur zéro.

L'étape suivante consiste à obtenir le RunLoop actuel. Parce que nous l'avons fait dans le bloc de file d'attente que nous avons créé, il obtiendra le RunLoop pour la file d'attente d'arrière-plan que nous avons créée auparavant.

Créez la minuterie. Ici, au lieu d'utiliser ScheduleTimer, nous appelons simplement le constructeur de timer et passons la propriété que vous souhaitez pour le timer, comme timeInterval, target, selector, etc.

Ajoutez le minuteur créé au RunLoop. Exécuter.

Voici une question sur l'exécution de RunLoop. Selon la documentation ici, il indique qu'il commence effectivement une boucle infinie qui traite les données des sources d'entrée et des temporisateurs de la boucle d'exécution.

private func startTimer() {
  // schedule timer on background
  queue.async { [unowned self] in
    if let _ = self.timer {
      self.timer?.invalidate()
      self.timer = nil
    }

    let currentRunLoop = RunLoop.current
    self.timer = Timer(timeInterval: self.updateInterval, target: self, selector: #selector(self.timerTriggered), userInfo: nil, repeats: true)
    currentRunLoop.add(self.timer!, forMode: .commonModes)
    currentRunLoop.run()
  }
}

Minuterie de déclenchement

Implémentez la fonction comme d'habitude. Lorsque cette fonction est appelée, elle est appelée par défaut dans la file d'attente.

func timerTriggered() {
  // under queue by default
  debug()
}

func debug() {
  let name = __dispatch_queue_get_label(nil)
  print(String(cString: name, encoding: .utf8))
}

La fonction de débogage ci-dessus est utilisée pour imprimer le nom de la file d'attente. Si jamais vous vous inquiétez s'il a fonctionné dans la file d'attente, vous pouvez l'appeler pour vérifier.

Arrêter la minuterie

Stop timer est facile, appelez validate () et définissez la variable timer stockée dans la classe sur nil.

Ici, je l'exécute à nouveau dans la file d'attente. En raison de l'avertissement ici, j'ai décidé d'exécuter tout le code lié à la minuterie dans la file d'attente pour éviter les conflits.

func stopTimer() {
  queue.sync { [unowned self] in
    guard let _ = self.timer else {
      // error, timer already stopped
      return
    }
    self.timer?.invalidate()
    self.timer = nil
  }
}

Questions relatives à RunLoop

Je suis en quelque sorte un peu confus si nous devons arrêter manuellement le RunLoop ou non. Selon la documentation ici, il semble que quand aucun chronomètre ne lui est attaché, il se ferme immédiatement. Donc, quand nous arrêtons le chronomètre, il devrait exister lui-même. Cependant, à la fin de ce document, il disait également:

la suppression de toutes les sources d'entrée et temporisateurs connus de la boucle d'exécution n'est pas une garantie que la boucle d'exécution se terminera. macOS peut installer et supprimer des sources d'entrée supplémentaires selon les besoins pour traiter les demandes ciblées sur le thread du destinataire. Ces sources pourraient donc empêcher la boucle d'exécution de se terminer.

J'ai essayé la solution ci-dessous fournie dans la documentation pour une garantie de terminer la boucle. Cependant, la minuterie ne se déclenche pas après avoir modifié .run () par le code ci-dessous.

while (self.timer != nil && currentRunLoop.run(mode: .commonModes, before: Date.distantFuture)) {};

Ce que je pense, c'est qu'il pourrait être sûr d'utiliser simplement .run () sur iOS. Parce que la documentation indique que macOS est installé et supprime les sources d'entrée supplémentaires selon les besoins pour traiter les demandes ciblées sur le thread du récepteur. Donc, iOS pourrait être bien.

13
nuynait

Ma Swift 3.0 pour iOS 10+, timerMethod() sera appelée dans la file d'attente en arrière-plan.

class ViewController: UIViewController {

    var timer: Timer!
    let queue = DispatchQueue(label: "Timer DispatchQueue", qos: .background, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)

    override func viewDidLoad() {
        super.viewDidLoad()

        queue.async { [unowned self] in
            let currentRunLoop = RunLoop.current
            let timeInterval = 1.0
            self.timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(self.timerMethod), userInfo: nil, repeats: true)
            self.timer.tolerance = timeInterval * 0.1
            currentRunLoop.add(self.timer, forMode: .commonModes)
            currentRunLoop.run()
        }
    }

    func timerMethod() {
        print("code")
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        queue.sync {
            timer.invalidate()
        }
    }
}
2
Tikhonov Alexander

Swift uniquement (bien qu'il puisse probablement être modifié pour être utilisé avec Objective-C)

Consultez DispatchTimer de https://github.com/arkdan/ARKExtensions , qui "Exécute une fermeture sur la file d'attente de répartition spécifiée, avec des intervalles de temps spécifiés, pour un nombre de fois spécifié (facultativement ). "

let queue = DispatchQueue(label: "ArbitraryQueue")
let timer = DispatchTimer(timeInterval: 1, queue: queue) { timer in
    // body to execute until cancelled by timer.cancel()
}
1
user1244109

Aujourd'hui après 6 ans, j'essaye de faire la même chose, voici la solution alternative: GCD ou NSThread.

Les temporisateurs fonctionnent en conjonction avec les boucles d'exécution, la boucle d'exécution d'un thread ne peut être récupérée qu'à partir du thread, la clé est donc de planifier le temporisateur dans le thread.

Sauf runloop du thread principal, runloop doit démarrer manuellement; il devrait y avoir des événements à gérer lors de l'exécution de runloop, comme Timer, sinon runloop se fermera, et nous pouvons l'utiliser pour quitter un runloop si timer est la seule source d'événement: invalidez le timer.

Le code suivant est Swift 4:

Solution 0: GCD

weak var weakTimer: Timer?
@objc func timerMethod() {
    // vefiry whether timer is fired in background thread
    NSLog("It's called from main thread: \(Thread.isMainThread)")
}

func scheduleTimerInBackgroundThread(){
    DispatchQueue.global().async(execute: {
        //This method schedules timer to current runloop.
        self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
        //start runloop manually, otherwise timer won't fire
        //add timer before run, otherwise runloop find there's nothing to do and exit directly.
        RunLoop.current.run()
    })
}

Le minuteur a une référence forte à la cible, et runloop a une forte référence au minuteur, après invalidation du minuteur, il libère la cible, donc gardez une référence faible à lui dans la cible et invalidez-le en temps opportun pour quitter runloop (puis quitter le thread).

Remarque: en tant qu'optimisation, syncfonction de DispatchQueue appelle le bloc sur le thread en cours lorsque cela est possible. En fait, vous exécutez le code ci-dessus dans le thread principal, le minuteur est déclenché dans le thread principal, donc n'utilisez pas la fonction sync, sinon le minuteur n'est pas déclenché sur le thread souhaité.

Vous pouvez nommer le thread pour suivre son activité en interrompant l'exécution du programme dans Xcode. Dans GCD, utilisez:

Thread.current.name = "ThreadWithTimer"

Solution 1: Thread

Nous pourrions utiliser NSThread directement. N'ayez pas peur, le code est facile.

func configurateTimerInBackgroundThread(){
    // Don't worry, thread won't be recycled after this method return.
    // Of course, it must be started.
    let thread = Thread.init(target: self, selector: #selector(addTimer), object: nil)
    thread.start()
}

@objc func addTimer() {
    weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
    RunLoop.current.run()
}

Solution 2: fil de sous-classe

Si vous souhaitez utiliser la sous-classe Thread:

class TimerThread: Thread {
    var timer: Timer
    init(timer: Timer) {
        self.timer = timer
        super.init()
    }

    override func main() {
        RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
        RunLoop.current.run()
    }
}

Remarque: n'ajoutez pas de temporisateur dans init, sinon, timer est ajouté au runloop du thread de l'appelant init, pas au runloop de ce thread, par exemple, vous exécutez le code suivant dans le thread principal, si TimerThread ajoutez le timer dans la méthode init, timer sera programmé sur le runloop du thread principal, et non sur le runloop de timerThread. Vous pouvez le vérifier dans timerMethod() log.

let timer = Timer.init(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
weakTimer = timer
let timerThread = TimerThread.init(timer: timer)
timerThread.start()

PS À propos de Runloop.current.run(), son document suggère de ne pas appeler cette méthode si nous voulons que runloop se termine, utilisez run(mode: RunLoopMode, before limitDate: Date), en fait run() invoquez cette méthode à plusieurs reprises dans NSDefaultRunloopMode , quel est le mode? Plus de détails dans runloop et thread .

1
seedante
class BgLoop:Operation{
    func main(){
        while (!isCancelled) {
            sample();
            Thread.sleep(forTimeInterval: 1);
        }
    }
}
0
john07