How to interrupt a rendering which is not displayed in the UI to interact with?
Minor code changes are required to make the code from the linked answer work for objects in selection. 

Following modal operator is just some kind of barebone code, a good starting point using the timer though. Edit the render path according to your Operating System, run the script, select the objects in 3d view, press F3 and type Render multi... 

import bpy

class Multi_Render(bpy.types.Operator):
    """Docstring"""
    bl_idname = "render.multi"
    bl_label = "Render multiple times"

    # Define some variables to register
    _timer = None
    shots = None
    stop = None
    rendering = None
    path = "/tmp/"       # Linux
    #path = "C:\\tmp\\"  # Windows

    # Define the handler functions. I use pre and
    # post to know if Blender "is rendering"
    def pre(self, dummy):
        self.rendering = True

    def post(self, dummy):
        self.shots.pop(0) # Remove current object from list
                          # Render next Object 
        self.rendering = False

    def cancelled(self, dummy):
        self.stop = True

    def execute(self, context):
        # Define the variables during execution. This allows
        # to define when called from a button
        self.stop = False
        self.rendering = False

        # One shot per selected object
        self.shots = [o.name for o in context.selected_objects]

        context.scene.render.filepath = self.path

        bpy.app.handlers.render_pre.append(self.pre)
        bpy.app.handlers.render_post.append(self.post)
        bpy.app.handlers.render_cancel.append(self.cancelled)

        # The timer gets created and the modal handler
        # is added to the window manager
        self._timer = context.window_manager.event_timer_add(0.5, window=context.window)
        context.window_manager.modal_handler_add(self)

        return {"RUNNING_MODAL"}

    def modal(self, context, event):
        if event.type == 'TIMER': # This event is signaled every half a second
                                  # and will start the render if available

            # If cancelled or no more shots to render, finish.
            if True in (not self.shots, self.stop is True): 

                # We remove the handlers and the modal timer to clean everything
                bpy.app.handlers.render_pre.remove(self.pre)
                bpy.app.handlers.render_post.remove(self.post)
                bpy.app.handlers.render_cancel.remove(self.cancelled)
                context.window_manager.event_timer_remove(self._timer)

                return {"FINISHED"} # I didn't separate the cancel and finish
                                    # events, because in my case I don't need to,
                                    # but you can create them as you need

            elif self.rendering is False: # Nothing is currently rendering.
                                          # Proceed to render.
                sc = context.scene

                # I'm using cameras named just as the output files,
                # but adapt to your needs
                #sc.camera = bpy.data.objects[self.shots[0]]

                sc.render.filepath = self.path + self.shots[0]
                bpy.ops.render.render("INVOKE_DEFAULT", write_still=True)

        return {"PASS_THROUGH"}
        # This is very important! If we used "RUNNING_MODAL", this new modal function
        # would prevent the use of the X button to cancel rendering, because this
        # button is managed by the modal function of the render operator,
        # not this new operator!


def register():
    bpy.utils.register_class(Multi_Render)

def unregister():
    bpy.utils.unregister_class(Multi_Render)

if __name__ == "__main__":
    register()

    #bpy.ops.render.multi('INVOKE_DEFAULT') # Test call


Note: You don't necessarily have to use a for-loop, the post method removes each item from the list in case the rendering is finished. Also I suggest split up the process into 2 operators, one for rendering only and another one to manage the whole process.



In case you don't want the render window to show up for whatever reason, you can either set the display_mode to 'NONE' temporary:

    elif self.rendering is False: # Nothing is currently rendering.
                                  # Proceed to render.
        sc = context.scene
        sc.render.display_mode = 'NONE'

        sc.render.filepath = self.path + self.shots[0]
        bpy.ops.render.render("INVOKE_DEFAULT", write_still=True)

        # Reset display mode
        sc.render.display_mode = 'WINDOW'


and add a check for the ESC event to remove the timer:

def modal(self, context, event):

    if event.type in {'ESC'}:
        context.window_manager.event_timer_remove(self._timer)
        bpy.app.handlers.render_pre.remove(self.pre)
        bpy.app.handlers.render_post.remove(self.post)
        bpy.app.handlers.render_cancel.remove(self.cancelled)

        self.report({'WARNING'}, 'User interuption')
        return {'FINISHED'}
    ...


Or you can call render.render() without INVOKE_DEFAULT to execute the operator directly (without the UI), e.g. based on a given variable (notice that the progress bar does not show up):

        if render_window:
            bpy.ops.render.render("INVOKE_DEFAULT", write_still=True)
        else:
            bpy.ops.render.render(write_still=True)




Also you can add an invoke() method along with a BoolProperty() to the operator so the user can decide whether or not displaying the render window:

render_window: bpy.props.BoolProperty(
    name="Show Render Window",
    description="A bool property",
    default = False
)

def invoke(self, context, event): # Used for user interaction
    wm = context.window_manager
    return wm.invoke_props_dialog(self)


and add a condition for that within the modal method (before rendering):

    elif self.rendering is False: # Nothing is currently rendering.
                                  # Proceed to render.
        sc = context.scene

        if self.render_window == False:
            sc.render.display_mode = 'NONE'

        sc.render.filepath = self.path + self.shots[0]
        bpy.ops.render.render("INVOKE_DEFAULT", write_still=True)

        # Reset display mode
        sc.render.display_mode = 'WINDOW'




However, it is indeed tricky to make it work properly...