【问题标题】:What is the best way to stop a program in Python and save the data?在 Python 中停止程序并保存数据的最佳方法是什么?
【发布时间】:2021-01-17 18:25:17
【问题描述】:

我对在 Python 中运行程序时停止它不感兴趣,当然,control c 可以做到这一点。我感兴趣的是以下情况:假设您有一个运行 5 小时的程序。你让它运行了两个小时,然后决定你到目前为止所做的事情值得保存,但你仍然不想继续。那么保存数据和退出程序的最佳方法是什么?直到现在,我所做的是将一个布尔值存储在一个泡菜中,然后我用每个循环打开泡菜并检查它的值。如果布尔值为真,则程序继续运行,如果为假,则程序停止并保存数据并退出。我可以使用不同的程序更改布尔值。然而,即使泡菜只由一个布尔值组成,它仍然会严重减慢程序速度,可能多达 10 倍,因为泡菜需要很长时间才能打开。我考虑过其他解决方案,并且知道 pdb_trace() 工具,但我真的不知道在这种情况下如何使用它。我在想也许设置一个环境变量可能会有所帮助,但我对设置环境变量不是很好。任何建议将不胜感激。

【问题讨论】:

  • 你为什么要用泡菜?检查文件的存在(比如/tmp/stop_my_program)还不够吗?
  • 这在很大程度上取决于您正在执行的操作的上下文。什么样的任务需要这么长时间才能运行?
  • @bobsmith76 您可以检查的不是最内部的循环,而是内部的第三个循环。这样您的检查平均在 5-10 秒内进行一次。这样你就不会减慢你的程序。
  • 更好的是,让你的程序在启动时创建一个文件(例如/tmp/foo_is_running),并保存状态并在它被删除时终止。这样您就不会在程序退出后留下任何工件。会有竞争条件,但我想这对您的目的并不重要。
  • @bobsmith76 为了确保您不经常检查,您可以在循环中的单独线程中进行检查,time.sleep(5)(睡眠 5 秒)。

标签: python


【解决方案1】:

答案包括检查环境中的变量和文件等内容。这些都行,但你能做到吗:

try:
  main()
except KeyboardInterrupt:
  save()

或者,如果保存过程与您在 main 完成后使用的过程相同,那么更强大的策略将是

try:
  main()
finally:
  save()

在这里,save() 将运行任何错误,KeyboardInterrupt 或其他。如果main() 成功,它也会运行。

如果您尝试使用单独的程序将其关闭,您可以发送信号。

【讨论】:

  • 是的,这可能是最好的方法。
  • @bobsmith76 我认为 except KeyboardInterrupt: 应该替换为 finally: 以便始终将工作至少保存到一些临时文件中,不仅在键盘中断时应该保存,而且在良好退出并在发生任何错误时保存以前的工作。恕我直言。
【解决方案2】:

为了您的有趣任务,我决定实现相当复杂但通用的异步处理任何命令的解决方案。命令在cmds.txt 文件中提供,每行一个命令。现在只支持两个命令saveexitsave 可以在空格之后包含第二个可选参数,要保存到的文件名(默认为 save.txt)。

如果程序异常退出(未提供exit 命令),则工作将保存到临时文件save.txt.tmp

cmds.txt 文件在单独的线程中处理,文件每秒检查一次,检查速度非常快,因此不占用 CPU,检查只是测试文件修改时间是否已更改。每个新命令都应添加到文件末尾的新行,不应删除已处理的行。在程序启动命令文件被清理。

主线程只检查has_cmds bool 变量(如果有新命令)它非常快并且可以经常完成,例如在处理 10-20 毫秒之类的最小任务之后。没有互斥体,因此所有的工作都非常快。

使用示例主线程在随机时间点产生任务处理的结果并将结果存储到数组中。在保存命令时,此结果数组保存为 JSON。

程序将有关其所做操作的所有信息打印到包含时间戳的控制台中。

接下来要测试程序:

  1. 启动程序。它立即开始处理计算工作。
  2. 在任何文本编辑器中打开cmds.txt
  3. 使用save 字符串添加新行。保存文件。
  4. 程序应打印出save 命令已被识别、处理并且工作已保存到文件save.txt
  5. 在编辑器save other.txt 中添加另一行。保存文件
  6. 程序应打印出已将工作保存到save.txt
  7. 添加新行exit并保存。
  8. 程序应该退出。
  9. 再次尝试运行程序。
  10. 尝试在程序控制台中按Ctrl+C
  11. 程序应捕获此键盘中断并说明这一点,并将工作保存到临时文件save.txt.tmp 并退出程序。

在最简单的情况下,为了节省键盘中断的工作,应该像in this answer那样完成。

您还可以通过像this solution 中处理 SIGINT 来优雅地实现进程终止。 SIGINT可以使用this program windows-kill发送给程序,语法windows-kill -SIGINT PID,其中PID可以通过microsoft's pslist获得。

import threading, random, os, json, time, traceback

cmds = []
has_cmds = False
cmds_fname = 'cmds.txt'
save_fname = 'save.txt'
save_fname_tmp = 'save.txt.tmp'

def CurTimeStr(*, exact = False):
    from datetime import datetime
    return (datetime.now(), datetime.utcnow())[exact].strftime(('[%H:%M:%S]', '[%Y-%m-%d %H:%M:%S.%f UTC]')[exact])

def Print(*pargs, **nargs):
    print(CurTimeStr(), *pargs, flush = True, **nargs)
    
def AddCmd(c, *, processed = False):
    global cmds, has_cmds
    cmds.append({**{'processed': threading.Event()}, **c})
    if processed:
        cmds[-1]['processed'].set()
    has_cmds = True
    return cmds[-1]

def ExternalCommandsThread():
    global cmds, has_cmds
    Print('Cmds thread started.')
    first, next_line, mtime = True, 0, 0.
    while True:
        try:
            if first:
                Print(f'Cleaning cmds file "{cmds_fname}".')
                with open(cmds_fname, 'wb') as f:
                    pass
                first = False
            if os.path.exists(cmds_fname) and abs(os.path.getmtime(cmds_fname) - mtime) > 0.0001 and os.path.getsize(cmds_fname) > 0:
                Print(f'Updated cmds file "{cmds_fname}". Processing lines starting from {next_line + 1}.')
                with open(cmds_fname, 'r', encoding = 'utf-8-sig') as f:
                    data = f.read()
                lines = list(data.splitlines())
                try:
                    mtime = os.path.getmtime(cmds_fname)
                    for iline, line in zip(range(next_line, len(lines)), lines[next_line:]):
                        line = line.strip()
                        if not line:
                            continue
                        if line[0] not in ['[', '{', '"']:
                            cmd = line.split()
                        else:
                            cmd = json.loads(line)
                        pargs = []
                        if type(cmd) is list:
                            cmd, *pargs = cmd
                        cmd = {'cmd': cmd, 'pargs': pargs}
                        assert 'cmd' in cmd, 'No "cmd" in command line!'
                        c = cmd['cmd']
                        if c in ['save']:
                            assert len(set(cmd.keys()) - {'cmd', 'fname', 'pargs'}) == 0
                            AddCmd({'cmd': 'save', 'fname': cmd.get('fname', (cmd['pargs'] or [save_fname])[0])})
                        elif c == 'exit':
                            AddCmd({'cmd': 'exit'})
                        else:
                            assert False, f'Unrecognized cmd "{c}"!'
                        Print(f'Parsed cmd "{c}" on line {iline + 1}.')
                        next_line = iline + 1
                except (json.decoder.JSONDecodeError, AssertionError) as ex:
                    traceback.print_exc()
                    Print(f'Failed to parse cmds line {iline + 1} with text "{line}"!')
                except:
                    raise
            for i, c in enumerate(cmds):
                if c is None:
                    continue
                if not c['processed'].is_set():
                    has_cmds = True
                while not c['processed'].wait(10):
                    Print(f'Timed out waiting for cmd "{c["cmd"]}" to be processed, continuing waiting!')
                Print(f'Processed cmd "{c["cmd"]}".')
                cmds[i] = None
                if c['cmd'] == 'exit':
                    Print('Exit cmd. Cmds thread finishes.')
                    return
            has_cmds = False
            time.sleep(1)
        except Exception as ex:
            traceback.print_exc()
            Print(f'Exception ^^^^^ in Cmds thread!')
            AddCmd({'cmd': 'exit'})
            time.sleep(3)

def Main():
    global cmds, has_cmds
    
    Print('Main thread started.')
    
    threading.Thread(target = ExternalCommandsThread, daemon = False).start()
    
    results = []
    
    def SaveWork(fname):
        with open(fname, 'w', encoding = 'utf-8') as f:
            f.write(json.dumps(results, ensure_ascii = False, indent = 4))
        Print(f'Work saved to "{fname}".')
        
    def ProcessCmds():
        # Returns False only if program should exit
        for c in cmds:
            if c is None or c['processed'].is_set():
                continue
            if c['cmd'] == 'save':
                SaveWork(c['fname'])
            elif c['cmd'] == 'exit':
                Print('Exit cmd. Main thread finishes...')
                c['processed'].set()
                return False
            else:
                assert False, 'Unknown cmd "c["cmd"]"!'
            c['processed'].set()
        return True

    try:    
        # Main loop of tasks processing
        for i in range(1000):
            for j in range(10):
                if has_cmds and not ProcessCmds(): # Very fast check if there are any commands
                    return # Exit
                # Emulate small work of 0-200 ms long.
                time.sleep(random.random() * 0.2)
                # Store results of work in array
                results.append({'time': CurTimeStr(exact = True), 'i': i, 'j': j})
        assert False, 'Main finished without exit cmd!'
    except BaseException as ex:
        traceback.print_exc()
        Print(f'Exception ^^^^^ in Main thread!')
        SaveWork(save_fname_tmp)
        AddCmd({'cmd': 'exit'}, processed = True)
    
if __name__ == '__main__':
    Main()

示例输出 1:

[08:15:16] Main thread started.
[08:15:16] Cmds thread started.
[08:15:16] Cleaning cmds file "cmds.txt".
[08:15:21] Updated cmds file "cmds.txt". Processing lines starting from 1.
[08:15:21] Parsed cmd "save" on line 1.
[08:15:21] Work saved to "save.txt".
[08:15:21] Processed cmd "save".
[08:15:31] Updated cmds file "cmds.txt". Processing lines starting from 2.
[08:15:31] Parsed cmd "save" on line 2.
[08:15:31] Work saved to "other.txt".
[08:15:31] Processed cmd "save".
[08:15:35] Updated cmds file "cmds.txt". Processing lines starting from 3.
[08:15:35] Parsed cmd "exit" on line 3.
[08:15:35] Exit cmd. Main thread finishes...
[08:15:35] Processed cmd "exit".
[08:15:35] Exit cmd. Cmds thread finishes.

上面的输出对应的命令文件cmds.txt

save
save other.txt
exit

示例输出 2:

[08:14:39] Main thread started.
[08:14:39] Cmds thread started.
[08:14:39] Cleaning cmds file "cmds.txt".
Traceback (most recent call last):
  File "stackoverflow_64165394_processing_commands_in_prog.py", line 127, in Main
    time.sleep(random.random() * 0.2)
KeyboardInterrupt
[08:14:40] Exception ^^^^^ in Main thread!
[08:14:40] Work saved to "save.txt.tmp".
[08:14:41] Processed cmd "exit".
[08:14:41] Exit cmd. Cmds thread finishes.

例子save.txt:

[
    {
        "time": "[2020-10-02 05:15:16.836030 UTC]",
        "i": 0,
        "j": 0
    },
    {
        "time": "[2020-10-02 05:15:16.917989 UTC]",
        "i": 0,
        "j": 1
    },
    {
        "time": "[2020-10-02 05:15:17.011129 UTC]",
        "i": 0,
        "j": 2
    },
    {
        "time": "[2020-10-02 05:15:17.156579 UTC]",
        "i": 0,
        "j": 3
    },

    ................

【讨论】:

  • 谢谢。不是每个使用长程序的程序员都必须做类似的事情吗?我通过谷歌搜索找不到任何东西。为什么?无论如何,我需要一段时间才能读完这篇文章。当我了解到踩踏模块不会提高速度并且找不到它的用处时,我放弃了。另外,从未使用过回溯模块。但再次感谢您的努力。
  • @bobsmith76 在 Python 中几乎总是可以使用多处理来代替线程,例如在我的代码而不是threading.Thread(...).start() 中,您几乎可以不更改multiprocessing.Process(...).start()。线程通常使用起来更简单。是的,你是对的,一般来说,多处理在 Python 中更快,因为所有线程只使用一个 CPU 内核,但所有进程使用不同。
  • @bobsmith76 但是因为线程更容易使用,它们仍然可以在许多情况下使用:1)像我在线程中做一些非常轻量级的工作时,只是偶尔 2)还有线程共享全局/非局部变量,这可能是需要的,而进程只能通过序列化并通过Manager 发送数据来共享数据。 3) 在大量使用非 python 代码的情况下,线程可能与进程一样高效,即使是繁重的工作,例如一些 C++ 库,或磁盘上的输入/输出。
  • @bobsmith76 对于在 python 中调用 C++ 函数时使用上述 C++ 代码的情况,它可以使用所有内核进行多线程并且效率很高,GIL 也在 C++ 函数中发布,因此有他们没有减速。
  • @bobsmith76 但是如果你有一些繁重的计算,肯定应该使用进程而不是线程。我的其他答案 like this one 关于如何正确使用多处理以及何时使用。可能像我上面在我的答案中实现的东西应该已经在一些开源库中以非常定性的方式完成了,但它们不是很受欢迎,因此像我这样的程序员多次重新实现相同的代码。
猜你喜欢
  • 2021-09-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-05
  • 1970-01-01
  • 2018-06-16
  • 1970-01-01
  • 2020-11-03
  • 2016-11-21
相关资源
最近更新 更多