-
-
Save stuaxo/889db016e51264581b50 to your computer and use it in GitHub Desktop.
| # modified from https://gist.github.com/schlamar/2311116#file-processify-py-L17 | |
| # also see http://stackoverflow.com/questions/2046603/is-it-possible-to-run-function-in-a-subprocess-without-threading-or-writing-a-se | |
| import inspect | |
| import os | |
| import sys | |
| import traceback | |
| from functools import wraps | |
| from multiprocessing import Process, Queue | |
| class Sentinel: | |
| pass | |
| def processify(func): | |
| '''Decorator to run a function as a process. | |
| Be sure that every argument and the return value | |
| is *pickable*. | |
| The created process is joined, so the code does not | |
| run in parallel. | |
| ''' | |
| def process_generator_func(q, *args, **kwargs): | |
| result = None | |
| error = None | |
| it = iter(func()) | |
| while error is None and result != Sentinel: | |
| try: | |
| result = next(it) | |
| error = None | |
| except StopIteration: | |
| result = Sentinel | |
| error = None | |
| except Exception: | |
| ex_type, ex_value, tb = sys.exc_info() | |
| error = ex_type, ex_value, ''.join(traceback.format_tb(tb)) | |
| result = None | |
| q.put((result, error)) | |
| def process_func(q, *args, **kwargs): | |
| try: | |
| result = func(*args, **kwargs) | |
| except Exception: | |
| ex_type, ex_value, tb = sys.exc_info() | |
| error = ex_type, ex_value, ''.join(traceback.format_tb(tb)) | |
| result = None | |
| else: | |
| error = None | |
| q.put((result, error)) | |
| def wrap_func(*args, **kwargs): | |
| # register original function with different name | |
| # in sys.modules so it is pickable | |
| process_func.__name__ = func.__name__ + 'processify_func' | |
| setattr(sys.modules[__name__], process_func.__name__, process_func) | |
| q = Queue() | |
| p = Process(target=process_func, args=[q] + list(args), kwargs=kwargs) | |
| p.start() | |
| result, error = q.get() | |
| p.join() | |
| if error: | |
| ex_type, ex_value, tb_str = error | |
| message = '%s (in subprocess)\n%s' % (str(ex_value), tb_str) | |
| raise ex_type(message) | |
| return result | |
| def wrap_generator_func(*args, **kwargs): | |
| # register original function with different name | |
| # in sys.modules so it is pickable | |
| process_generator_func.__name__ = func.__name__ + 'processify_generator_func' | |
| setattr(sys.modules[__name__], process_generator_func.__name__, process_generator_func) | |
| q = Queue() | |
| p = Process(target=process_generator_func, args=[q] + list(args), kwargs=kwargs) | |
| p.start() | |
| result = None | |
| error = None | |
| while error is None: | |
| result, error = q.get() | |
| if result == Sentinel: | |
| break | |
| yield result | |
| p.join() | |
| if error: | |
| ex_type, ex_value, tb_str = error | |
| message = '%s (in subprocess)\n%s' % (str(ex_value), tb_str) | |
| raise ex_type(message) | |
| @wraps(func) | |
| def wrapper(*args, **kwargs): | |
| if inspect.isgeneratorfunction(func): | |
| return wrap_generator_func(*args, **kwargs) | |
| else: | |
| return wrap_func(*args, **kwargs) | |
| return wrapper | |
| @processify | |
| def test_function(): | |
| return os.getpid() | |
| @processify | |
| def test_generator_func(): | |
| for msg in ["generator", "function"]: | |
| yield msg | |
| @processify | |
| def test_deadlock(): | |
| return range(30000) | |
| @processify | |
| def test_exception(): | |
| raise RuntimeError('xyz') | |
| def test(): | |
| print(os.getpid()) | |
| print(test_function()) | |
| print(list(test_generator_func())) | |
| print(len(test_deadlock())) | |
| test_exception() | |
| if __name__ == '__main__': | |
| test() |
For some reason it doesn't work for me:
AttributeError: Can't pickle local object 'processify.<locals>.process_func'
May be because of Windows. Tried on Python 3.5 and Pyton 3.7. Same error.
Maybe it is Windows, I've only tried this on Linux.
Thanks for this - proved very useful for hacking around a CUDA memory leak that slowly accrued over many iterations of my code. Launching the code in the subprocess clears the memory when it terminates. Now I should probably figure out what is causing the leak ...
np, it really just built on the original by @schlamar
This should probably be inside a library, I haven't had to use it since I modified it in 2016, but it was handy at the time for providing isolation between my new code and a whole load of knarly old code that I didn't fully trust to not do something weird and break my own.
It does feel like this should be in a library, if that hasn't happened already.
Python 3 + Support for generator functions.