For a really good and in-depth article on this subject, just read Fix Your Timestep. There's also a ton of existing questions about this subject, although addressing different angles.
But in really simple terms, this is how you get a fixed physics (or anything, really) frame rate despite the rendering frame rate being unpredictable:
check how long it's been since the last frame
while we are overdue for a physics frame
do one frame of physics
ideal physics frames: p1 p2 p3 p4
render (actual) frames: r1 r2 r3 r4 r5
what's actually run: r1 p1 p2 r4 r5
At r2, we run physics for one frame (p1), at r3 we run two (p2, p3), at r4 and r5 we don't run any physics.
One key insight is that we don't have to run physics at an exact point in time. We just need to make sure the physics is up to date before we render everything. Some people get trapped into thinking they'll need some super-complex time-synchronised multi-threaded monstrosity to handle this, because of this misconception.
Usually this is implemented by taking the difference between the actual time and the expected time for the next physics update, accumulating that difference, and running physics frames as long as that difference is larger than the ideal physics frame time. So for example if ideally physics runs every 50ms, and it's been 120ms since the last frame, you run 2 physics frames (50 goes into 120 twice), and keep around the remainder 20ms. I think it's clearer if I just reproduce the code sample from the linked article:
double t = 0.0;
const double dt = 0.01;
double currentTime = hires_time_in_seconds();
double accumulator = 0.0;
double newTime = hires_time_in_seconds();
double frameTime = newTime - currentTime;
currentTime = newTime;
accumulator += frameTime;
while (accumulator >= dt)
integrate(state, t, dt);
accumulator -= dt;
t += dt;