I'm trying to simulate different tasks running in parallel in Python, and each parallel process is run at different frequencies (e.g. 200 Hz, 100 Hz and 50 Hz). I used code from this question to make a Timer class to run these processes in "Real-Time", but the processes de-synchronize over time (e.i., three 200 Hz tasks can sometimes run in between two 100 Hz tasks).
To synchronize my processes I make tick counters in their shared memory. Every iteration of the 200 Hz process increments a counter, then waits for it to be reset to 0 when the counter reaches 2, while every iteration of 100 Hz process waits for that counter to reach 2 before resetting it. Same thing for the 50 Hz process, but with another counter. I use a while/pass method for the waiting.
Here is the code :
from multiprocessing import Process, Event, Value
import time
# Add Timer class for multiprocessing
class Timer(Process):
    def __init__(self, interval, iteration, function, args=[], kwargs={}):
        super(Timer, self).__init__()
        self.interval = interval
        self.iteration = iteration
        self.iterationLeft = self.iteration
        self.function = function
        self.args = args
        self.kwargs = kwargs
        self.finished = Event()
    def cancel(self):
        """Stop the timer if it hasn't finished yet"""
        self.finished.set()
    def run(self):
        startTimeProcess = time.perf_counter()
        while self.iterationLeft > 0:
            startTimePeriod = time.perf_counter()
            self.function(*self.args, **self.kwargs)
            # print(self.interval-(time.clock() - startTimePeriod))
            self.finished.wait(self.interval-(time.perf_counter() - startTimePeriod))
            self.iterationLeft -= 1
        print(f'Process finished in {round(time.perf_counter()-startTimeProcess, 5)} seconds')
def func0(id, freq, tick_p1):
    # Wait for 2 runs of Process 1 (tick_p1)
    while tick_p1.value < 2:
        pass
    tick_p1.value = 0   # Reset tick_p1
    # Add fake computational time depending on the frequency of the process
    print(f'id: {id} at {freq} Hz') 
    if freq == 400:
        time.sleep(0.002)
    elif freq == 200:
        time.sleep(0.003)
    elif freq == 100:
        time.sleep(0.007)
    elif freq == 50:
        time.sleep(0.015)
def func1(id, freq, tick_p1, tick_p2):
    # Wait for tick_p1 to have been reset by Process0
    while tick_p1.value >= 2:
        pass
    # Wait for 2 runs of Process 2 (tick_p2)
    while tick_p2.value < 2:
        pass
    tick_p2.value = 0   # Reset tick_p2
    # Add fake computational time depending on the frequency of the process
    print(f'id: {id} at {freq} Hz') 
    if freq == 400:
        time.sleep(0.002)
    elif freq == 200:
        time.sleep(0.003)
    elif freq == 100:
        time.sleep(0.007)
    elif freq == 50:
        time.sleep(0.015)
    # Increment tick_p1
    tick_p1.value += 1
def func2(id, freq, tick_p2):
    # Wait for tick_p2 to have been reset by Process1
    while tick_p2.value >= 2:
        pass
    # Add fake computational time depending on the frequency of the process
    print(f'id: {id} at {freq} Hz') 
    if freq == 400:
        time.sleep(0.002)
    elif freq == 200:
        time.sleep(0.003)
    elif freq == 100:
        time.sleep(0.007)
    elif freq == 50:
        time.sleep(0.015)
    # Increment tick_p2
    tick_p2.value += 1
if __name__ == '__main__':
    freqs = [50,100,200]
    # freqs = [0.25,0.5,1]
    Tf = 10
    tick_p1 = Value('i', 1)
    tick_p2 = Value('i', 1)  
    processes = []
    p0 = Timer(interval=1/freqs[0], iteration=round(Tf*freqs[0]), function = func0, args=(0,freqs[0], tick_p1))
    p1 = Timer(interval=1/freqs[1], iteration=round(Tf*freqs[1]), function = func1, args=(1,freqs[1], tick_p1, tick_p2))
    p2 = Timer(interval=1/freqs[2], iteration=round(Tf*freqs[2]), function = func2, args=(2,freqs[2], tick_p2))
    processes.append(p0)
    processes.append(p1)
    processes.append(p2)
    start = time.perf_counter()
    for process in processes:
        process.start()   
    for process in processes:
        process.join()
    finish = time.perf_counter()
    print(f'Finished in {round(finish-start, 5)} seconds')
As you can see, I've added sleep time within the processes, to simulate computational time. When I remove the print commands in the processes, the script requires 10.2 seconds of runtime to simulate 10 seconds of "real-time" calculations (2% increase, which is acceptable).
My question is, is this the best way to achieve what I'm trying to do? Is there a better/faster way?
Thanks!