【问题标题】:Python multi-thread multi-interpreter C APIPython 多线程多解释器 C API
【发布时间】:2014-11-21 13:37:08
【问题描述】:

我正在使用 Python 的 C API,但很难理解一些极端情况。我可以测试它,但它似乎容易出错且耗时。所以我来这里看看是否有人已经这样做了。

问题是,在线程和子解释器之间没有直接关系的情况下,管理具有子解释器的多线程的正确方法是什么?

Py_Initialize();
PyEval_InitThreads(); /* <-- needed? */
_main = PyEval_SaveThread(); /* <-- acquire lock? does it matter? */
/* maybe do I not need it? */
i1 = Py_NewInterpreter();
i2 = Py_NewInterpreter();

我是否使用互斥锁?是否需要使用锁?线程函数应该是这样的:(线程是非python,可能是POSIX线程)

线程1

_save = PyThreadState_Swap(i1);
  // python work 
PyThreadState_Restore(_save);

Thread2(几乎相同)

_save = PyThreadState_Swap(i1);
  // python work 
PyThreadState_Restore(_save);

Thread3(几乎相同,但带有子解释器i2

_save = PyThreadState_Swap(i2);
  // python work 
PyThreadState_Restore(_save);

这是正确的吗?这是我想要实现的一般情况吗?有比赛条件吗?

谢谢!

【问题讨论】:

    标签: python multithreading python-c-api


    【解决方案1】:

    只需要在@sterin 的回答中指出一个问题,部分是Using a sub interpreter from a new thread (post Python 3.3)

    • PyThreadState_New 必须在 GIL 举行时调用
    • PyEval_RestoreThread 将获取 GIL,因此不得在持有 GIL 的情况下调用它,否则会出现死锁。

    因此,在这种情况下您需要使用PyThreadState_Swap 而不是PyEval_RestoreThread

    此外,您可以验证正在使用哪个解释器

    int64_t interp_id = PyInterpreterState_GetID(interp);
    

    【讨论】:

      【解决方案2】:

      Python 中的子解释器没有很好的文档记录,甚至没有得到很好的支持。以下是我所不理解的。它似乎在实践中运作良好。

      在处理 Python 中的线程和子解释器时,需要了解两个重要概念。首先,Python 解释器并不是真正的多线程。它有一个全局解释器锁 (GIL),需要获取它才能执行几乎所有 Python 操作(此规则有一些罕见的例外)。

      其次,线程和子解释器的每个组合都必须有自己的线程状态。解释器为它管理的每个线程创建一个线程状态,但是如果你想从不是由该解释器创建的线程中使用 Python,你需要创建一个新的线程状态。

      首先你需要创建子解释器:

      初始化 Python

      Py_Initialize();
      

      初始化 Python 线程支持

      如果您计划从多个线程调用 Python,则需要)。此调用还获取 GIL。

      PyEval_InitThreads();
      

      保存当前线程状态

      我本可以使用 PyEval_SaveThread(),但它的副作用之一是释放 GIL,然后需要重新获取。

      PyThreadState* _main = PyThreadState_Get();
      

      创建子解释器

      PyThreadState* ts1 = Py_NewInterpreter();
      PyThreadState* ts2 = Py_NewInterpreter();
      

      恢复主解释线程状态

      PyThreadState_Swap(_main);
      

      我们现在有两个子解释器的线程状态。这些线程状态仅在创建它们的线程中有效。每个想要使用其中一个子解释器的线程都需要为该线程和解释器的组合创建一个线程状态。

      在新线程中使用子解释器

      这是一个示例代码,用于在不是由子解释器创建的新线程中使用子解释器。新线程必须获取 GIL,为线程和解释器组合创建新的线程状态,并使其成为当前线程状态。最后必须进行相反的清理。

      void do_stuff_in_thread(PyInterpreterState* interp)
      {
          // acquire the GIL
          PyEval_AcquireLock(); 
      
          // create a new thread state for the the sub interpreter interp
          PyThreadState* ts = PyThreadState_New(interp);
      
          // make ts the current thread state
          PyThreadState_Swap(ts);
      
          // at this point:
          // 1. You have the GIL
          // 2. You have the right thread state - a new thread state (this thread was not created by python) in the context of interp
      
          // PYTHON WORK HERE
      
          // release ts
          PyThreadState_Swap(NULL);
      
          // clear and delete ts
          PyThreadState_Clear(ts);
          PyThreadState_Delete(ts);
      
          // release the GIL
          PyEval_ReleaseLock(); 
      }
      

      在新线程中使用子解释器(Python 3.3 后)

      之前的 do_stuff_in_thread() 仍然适用于所有当前的 Python 版本。但是,Python 3.3 弃用了PyEval_AcquireLock()/PyEval_ReleaseLock(),这导致了一些难题。

      唯一记录在案的释放 GIL 的方法是调用 PyEval_ReleaseThread()PyEval_SaveThread(),这两者都需要线程状态,而清理和删除当前线程状态需要保持 GIL。这意味着可以释放 GIL 或清理线程状态,但不能两者兼而有之。

      幸运的是,有一个解决方案——PyThreadState_DeleteCurrent() 删除当前线程状态,然后释放 GIL。 [这个 API 从 3.9 开始才被记录,但它至少从 Python 2.7 开始就存在了]

      修改后的do_stuff_in_thread() 也适用于所有当前的 Python 版本。

      void do_stuff_in_thread(PyInterpreterState* interp)
      {
          // create a new thread state for the the sub interpreter interp
          PyThreadState* ts = PyThreadState_New(interp);
      
          // make it the current thread state and acquire the GIL
          PyEval_RestoreThread(ts);
      
          // at this point:
          // 1. You have the GIL
          // 2. You have the right thread state - a new thread state (this thread was not created by python) in the context of interp
      
          // PYTHON WORK HERE
      
          // clear ts
          PyThreadState_Clear(ts);
      
          // delete the current thread state and release the GIL
          PyThreadState_DeleteCurrent();
      }
      

      现在每个线程都可以执行以下操作:

      线程1

      do_stuff_in_thread(ts1->interp);
      

      线程2

      do_stuff_in_thread(ts1->interp);
      

      线程3

      do_stuff_in_thread(ts2->interp);
      

      调用Py_Finalize() 会破坏所有子解释器。或者,它们可以手动销毁。这需要在主线程中完成,使用创建子解释器时创建的线程状态。最后使主解释器线程状态为当前状态。

      // make ts1 the current thread state
      PyThreadState_Swap(ts1);
      // destroy the interpreter
      Py_EndInterpreter(ts1);
      
      // make ts2 the current thread state
      PyThreadState_Swap(ts2);
      // destroy the interpreter
      Py_EndInterpreter(ts2);
      
      // restore the main interpreter thread state
      PyThreadState_Swap(_main);
      

      我希望这能让事情更清楚一些。

      我在github 和另一个also on github 上有一个用C++ 编写的小完整示例(Python 3.3 后的变体)。

      【讨论】:

      • 谢谢!这正是我一直在寻找的,一个“详细的快速入门”——比快速更详细:)。如果不是你,我肯定会错过“线程和解释器组合的线程状态”。
      • 所以总结一下:没有办法可以使用多个真正并行的python解释器实例(多核上的硬件线程)?
      • 正确。 Python 使用全局解释器锁,它一次只允许一个线程运行实际的 Python 代码。但是,执行长操作的 C 代码通常会释放锁,直到它返回 Python,以便另一个线程可以执行。这意味着实际利用率将取决于您的代码。
      • 正如所写,void do_stuff_in_thread(PyInterpreterState* interp) 不会在任何地方使用interp,而是使用看似全局的ts1 变量。这使得帖子中的代码不正确且难以理解。我认为 PyThreadState* ts = PyThreadState_New(ts1-&gt;interp); 应该是PyThreadState* ts = PyThreadState_New(interp);,但我们将不胜感激。
      • @sparc_spread 没错。 Python 有一个 GIL,由所有线程和子解释器共享。 Python 代码不能并行运行。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-03-28
      • 2010-11-15
      • 2015-04-08
      • 2015-06-16
      • 2019-05-26
      • 2011-02-27
      • 1970-01-01
      相关资源
      最近更新 更多