1

I want to run a code with process parallel to my main code but also want to access its parameters or start/stop the process via command prompt.

my machine is win7 64bit. Something in mind is:

from multiprocessing import Process

class dllapi():
    ...

def apiloop(params, args):
    apiclient = dllapi(**args)
    while True:
        apiclient.cycle()
        params = [....]

def mainloop(args):
    p = Process(target = apiloop, args=(params, args, ))
    while True:
        cmd = input()
        if cmd == 'kill':
            p.terminate()
        if cmd == 'stop':
            pass # no idea
        if cmd == 'resume':
            pass # no idea
        if cmd == 'report':
            print (params)

I wish to make it simple. I did tried to make apiloop as thread yet input() could freeze the program and stopped apiloop working until i pressed enter...

To share the parameters from apiloop process, i did try queue and pipe, but, seem to me, queue needs .join to wait until apiloop is done and pipe has buffer limit.

(actually i can make apiclient.cycle runs every 1s but i wish to keep apiclient alive)

I wish to know if it's worth to dig deeper with multiprocessing (e.g. will try manager as well...) or there are other approaches which is more suitable for my case. Thanks in advance...

* UPDATED: 201809170953*

Some progress with manager as below:

from multiprocessing import Process, Manager

class dllapi():
    ...
class webclientapi():
    ...

def apiloop(args, cmd, params):
    apiclient = dllapi(**args)
    status = True
    while True:
        # command from main
        if cmd == 'stop':
            status = False
        elif cmd == 'start':
            status = True
        cmd = None
        # stop or run
        if status == True:
            apiclient.cycle()
        # update parameters
        params['status'] = status

def uploadloop(cmds, params):
    uploadclient = webclientapi()
    status = True
    while True:
        # command from main
        if cmd == 'stop':
            status = False
        elif cmd == 'start':
            status = True
        cmd = None
        # stop or run
        if status == True:
            # upload 'status' from apiclient to somewhere
            uploadclient.cycle(params['status'])

def mainloop(args):

    manager = Manager()
    mpcmds = {}
    mpparams = {}
    mps = {}

    mpcmds   ['apiloop'] = manager.Value('u', 'start')
    mpparams ['apiloop'] = manager.dict()
    mps      ['apiloop'] = Process(target = apiloop, args=(args, mpcmds['apiloop'], mpparams['apiloop'])

    mpcmds   ['uploadloop'] = manager.Value('u', 'start')
    # mpparams ['uploadloop'] is directly from mpparams ['apiloop']
    mps      ['uploadloop'] = Process(target = uploadloop, args=(mpcmds['uploadloop'], mpparams['apiloop'])

    for key, mp in mps.items():
        mp.daemon = True
        mp.start()

    while True:
        cmd = input().split(' ')
        # kill daemon process with exit()
        if cmd[0] == 'bye':
            exit()
        # kill individual process
        if cmd[0] == 'kill':
            mps[cmd[1]].terminate()
        # stop individual process via command
        if cmd[0] == 'stop':
            mpcmds[cmd[1]] = 'stop'
        # stop individual process via command
        if cmd[0] == 'start':
            mpcmds[cmd[1]] = 'start'
        # report individual process info via command
        if cmd[0] == 'report':
            print (mpparams ['apiloop'])

Hope this'd help someone.

Darkonaut
  • 20,186
  • 7
  • 54
  • 65
  • So your main-thread in the parent process is not doing anything else than controlling apiloop and you are just spawning another process instead of a thread because you did not get it to work with a thread? Is that correct? – Darkonaut Sep 14 '18 at 20:56
  • correct, main-thread in parent process to control multiple processes via prompt. i wish it be able to supervise its child (e.g. stop, resume, read data), kill its child and leave the main-thread without zombie.. – Cherrimon Shop Sep 16 '18 at 01:07
  • But why would you need a child process? Your parent process is doing nothing but controlling. From your example it's not clear why you wouldn't just use threads, because that would be possible. Does your real app involve cpu-bound work in the parent process? – Darkonaut Sep 16 '18 at 03:07
  • I tried with thread before but input() blocked the thread operation for some reason in a long run...I did not investigate the issue...and suspect it was caused by serialization with single processor...thus I wish to try with multiprocessing instead. The window 64bit related dllapi consists of callback and ctypes functions, what's more info do u need to determine if thread would work I my case? – Cherrimon Shop Sep 16 '18 at 15:09

1 Answers1

2

I'm showing you how to solve the general problem with threads only, because that is what you tried first and your example doesn't bring up the need for a child-process.

In the example below your dllapi class is named Zoo and it's subclassing threading.Thread, adding some methods to allow execution control. It takes some data upon initialization and its cycle-method simply iterates repeatedly over this data and just counts how many times it has seen the specific item.

import time
import logging
from queue import Queue
from threading import Thread

from itertools import count, cycle


class Zoo(Thread):

    _ids = count(1)

    def __init__(self, cmd_queue, data, *args,
             log_level=logging.DEBUG, **kwargs):

        super().__init__()
        self.name = f'{self.__class__.__name__.lower()}-{next(self._ids)}'
        self.data = data
        self.log_level = log_level
        self.args = args
        self.kwargs = kwargs

        self.logger = self._init_logging()
        self.cmd_queue = cmd_queue

        self.data_size = len(data)
        self.actual_item = None
        self.iter_cnt = 0
        self.cnt = count(1)
        self.cyc = cycle(self.data)

    def cycle(self):
        item = next(self.cyc)
        if next(self.cnt) % self.data_size == 0:  # new iteration round
            self.iter_cnt += 1
        self.actual_item = f'{item}_{self.iter_cnt}'

    def run(self):
        """
        Run is the main-function in the new thread. Here we overwrite run
        inherited from threading.Thread.
        """
        while True:
            if self.cmd_queue.empty():
                self.cycle()
                time.sleep(1)  # optional heartbeat
            else:
                self._get_cmd()
                self.cmd_queue.task_done()  # unblocks prompter

    def stop(self):
        self.logger.info(f'stopping with actual item: {self.actual_item}')
        # do clean up
        raise SystemExit

    def pause(self):
        self.logger.info(f'pausing with actual item: {self.actual_item}')
        self.cmd_queue.task_done()  # unblocks producer joining the queue
        self._get_cmd()  # just wait blockingly until next command

    def resume(self):
        self.logger.info(f'resuming with actual item: {self.actual_item}')

    def report(self):
        self.logger.info(f'reporting with actual item: {self.actual_item}')
        print(f'completed {self.iter_cnt} iterations over data')

    def _init_logging(self):
        fmt = '[%(asctime)s %(levelname)-8s %(threadName)s' \
          ' %(funcName)s()] --- %(message)s'
        logging.basicConfig(format=fmt, level=self.log_level)
        return logging.getLogger()

    def _get_cmd(self):
        cmd = self.cmd_queue.get()
        try:
            self.__class__.__dict__[cmd](self)
        except KeyError:
            print(f'Command `{cmd}` is unknown.')

input is a blocking function. You need to outsource it in a separate thread so it doesn't block your main-thread. In the example below input is wrapped in Prompter, a class subclassing threading.Thread. Prompter passes inputs into a command-queue. This command-queue is read by Zoo.

class Prompter(Thread):
    """Prompt user for command input.
    Runs in a separate thread so the main-thread does not block.
    """
    def __init__(self, cmd_queue):
        super().__init__()
        self.cmd_queue = cmd_queue

    def run(self):
        while True:
            cmd = input('prompt> ')
            self.cmd_queue.put(cmd)
            self.cmd_queue.join()  # blocks until consumer calls task_done()


if __name__ == '__main__':

    data = ['ape', 'bear', 'cat', 'dog', 'elephant', 'frog']

    cmd_queue = Queue()
    prompter = Prompter(cmd_queue=cmd_queue)
    prompter.daemon = True

    zoo = Zoo(cmd_queue=cmd_queue, data=data)

    prompter.start()
    zoo.start()

Example session in terminal:

$python control_thread_over_prompt.py
prompt> report
[2018-09-16 17:59:16,856 INFO     zoo-1 report()] --- reporting with actual item: dog_0
completed 0 iterations over data
prompt> pause
[2018-09-16 17:59:26,864 INFO     zoo-1 pause()] --- pausing with actual item: bear_2
prompt> resume
[2018-09-16 17:59:33,291 INFO     zoo-1 resume()] --- resuming with actual item: bear_2
prompt> report
[2018-09-16 17:59:38,296 INFO     zoo-1 report()] --- reporting with actual item: ape_3
completed 3 iterations over data
prompt> stop
[2018-09-16 17:59:42,301 INFO     zoo-1 stop()] --- stopping with actual item: elephant_3
Darkonaut
  • 20,186
  • 7
  • 54
  • 65
  • a newbie question about when to use super()? i have updated my question. do you think that code doesn't need child-process.as well? when do we need child-process? is the number of CPU the only factor? i am going to try your solution on my project at the moment. – Cherrimon Shop Sep 17 '18 at 02:33
  • @Cherrimon Shop For an explanaiton on `super()` look [here](https://stackoverflow.com/questions/576169/understanding-python-super-with-init-methods). We are calling it because our goal is to extend the inherited `__ini__`. But when we define `__init__` again in the SubClass we are overwriting the inherited `__init__`, so we have to call it manually on the BaseClass and then we add what we want to add. – Darkonaut Sep 17 '18 at 02:59
  • @Cherrimon Shop The number of cores is the (reasonable) upper bound for the number of worker-processes when you have cpu-bound tasks which you would like to let run truely parallel. More on this I've written up [here](https://stackoverflow.com/a/52083532/9059420). But your example looks like mainly I/O, so initially you wouldn't need additional processes because your main-process is mostly just waiting for some I/O to complete so threads would be enough I think. – Darkonaut Sep 17 '18 at 03:03
  • thanks,input() with queue works as expected. I suppose threading is sufficient for this case. Yet it's not clear to me when i should use process instead. much appreciate if you can tell me when you have time. – Cherrimon Shop Sep 17 '18 at 14:23
  • @Cherrimon Shop When your task is computationally intensive and needs the GIL you would need additional processes for parallelism. For example, running a long for-loop and adding numbers would fully load the core your active thread of the process is getting executed on. Understanding what the [GIL](https://wiki.python.org/moin/GlobalInterpreterLock) is about, is key to understand when you need further processes. In case you have not read about it yet, do this first. – Darkonaut Sep 17 '18 at 14:59
  • @Cherrimon Shop You can use the [psutil](https://psutil.readthedocs.io/en/latest/) module to inspect cpu utilization of your process. Look for `cpu_percent` in the linked docs. If this gives you high percentages for your parent-process, it could be a good idea using additional processes. – Darkonaut Sep 17 '18 at 15:00
  • unfortunate that, the program again freezed after a day of running until i type something on the screen... – Cherrimon Shop Sep 19 '18 at 02:22
  • @Cherrimon Shop With your api-code or my unchanged answer? – Darkonaut Sep 19 '18 at 02:35
  • with your answer i suppose, only threading for classes. im not sure how to debug it....i start to think keep the prompt in one process and the rest to other process with threading.... – Cherrimon Shop Sep 19 '18 at 02:51
  • @Cherrimon Shop I doubt that would change anything. I remember one question where somebody also had a long-running job where it printed something to stdout repeatedly and it stopped after a while without explainable reason (not an answer from me on that). I would rather suspect the shell or the OS to interfer. You could also run exactly my code as long you did with your changes to exclude it's something with your api. – Darkonaut Sep 19 '18 at 03:02
  • hopefully this is last update on this question, the issue might be due to [QuickEdit mode enabled in my prompt](https://stackoverflow.com/questions/30418886/how-and-why-does-quickedit-mode-in-command-prompt-freeze-applications?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa). After I disable it, it looks fine after a day of running. – Cherrimon Shop Sep 20 '18 at 12:29