Search code examples
pythonpython-multithreadingschedule

Double triggering of 2 threads running parallel


my code. it starts 2 threads with inner schedulers that print number every second

import threading
import time
from datetime import datetime

import schedule

_lock = threading.Lock()


def p(number):
    _lock.acquire()
    print(number, datetime.now())
    _lock.release()


def f(number):
    schedule.every(5).seconds.do(p, number)
    while True:
        schedule.run_pending()
        time.sleep(1)


thread = threading.Thread(target=f, args=(1,))
thread2 = threading.Thread(target=f, args=(2,))
thread.start()
thread2.start()

expected output

1 2020-03-25 22:07:17.817528
2 2020-03-25 22:07:17.817528
1 2020-03-25 22:07:22.821887
2 2020-03-25 22:07:22.821887
1 2020-03-25 22:07:27.826093
2 2020-03-25 22:07:27.826093

actual output (see 4 instead of 2 prints at 17' and 3 instead of 2 prints at 27')

1 2020-03-25 22:07:17.817528
2 2020-03-25 22:07:17.817528
1 2020-03-25 22:07:17.817528
2 2020-03-25 22:07:17.817528
1 2020-03-25 22:07:22.821887
2 2020-03-25 22:07:22.821887
1 2020-03-25 22:07:27.826093
2 2020-03-25 22:07:27.826093
2 2020-03-25 22:07:27.826093

I dont actually know why sometimes thread triggers function more than just once. Any idea what i do wrong ?


Solution

  • Both threads add a task to schedule and both threads execute run_pending(). What might be happening here is that one thread executes run_pending() causing both of the scheduled tasks to be run, but before run_pending() finishes executing in the first thread (and marking the pending tasks as completed), the second thread steps in and executes run_pending() as well, causing any pending tasks to be executed twice.

    By moving the lock from the function p and putting it around run_pending() instead I was unable to replicate the behavior of tasks being fired twice.

    def f(number):
        schedule.every(5).seconds.do(p, number)
        while True:
            _lock.acquire()
            schedule.run_pending()
            _lock.release()
            time.sleep(1)
    

    Note that you could rewrite the program as well such that only one thread calls the run_pending(), something like this:

    import threading
    import time
    from datetime import datetime
    
    import schedule
    
    def p(number):
        print(number, datetime.now())
    
    schedule.every(5).seconds.do(p, 1)
    schedule.every(5).seconds.do(p, 2)
    
    def task_runner():
        while True:
            schedule.run_pending()
            time.sleep(1)
    
    thread = threading.Thread(target=task_runner)
    thread.start()
    

    This is assuming you want to use the main thread for something other than running the scheduled tasks. Or you could just put a call to run_pending() in your application's event loop without having it in a separate thread.