强制线程阻止所有其他线程执行

时间:2015-03-28 12:25:51

标签: python multithreading python-3.x python-multithreading

更新:

This answer指出,截至2013年4月,我所要做的事情是不可能的。但是,这似乎与Alex Martelli在Python Cookbook中所说的相矛盾(第624页,第3版)。 :

  

返回时,PyGILState_Ensure()始终保证调用   thread具有对Python解释器的独占访问权限。这是真的   即使调用C代码正在运行不同的线程   解释者不知道。

The docs似乎也建议获得GIL,这会给我带来希望(除了我不认为我可以从纯python代码调用PyGILState_Ensure(),如果我创建一个C扩展名叫它,我不知道如何将memory_daemon()嵌入其中。

也许我误读了答案或Python Cookbook和文档。

原始问题:

我想要一个给定的线程(来自threading模块)来阻止任何其他线程在其某段代码执行时运行。实现它的最简单方法是什么?

显然,最大限度地减少其他线程中的代码更改,避免使用C和直接操作系统调用,并使其成为Windows和Linux的跨平台。但实际上,我很乐意为我的实际环境提供任何解决方案(见下文)。

环境:

  • CPython的
  • python 3.4(如果有帮助,可以升级到3.5)
  • Ubuntu 14.04

用例:

出于调试目的,我计算所有对象使用的内存(由gc.get_objects()报告),并将一些摘要报告打印到sys.stderr。我在一个单独的线程中执行此操作,因为我希望从其他线程异步传递此摘要;我将time.sleep(10)放在执行实际内存使用量计算的while True循环的末尾。但是,内存报告线程需要一段时间来完成每个报告,并且我不希望所有其他线程在内存计算完成之前继续前进(否则,内存快照将很难解释)。

示例(澄清问题):

import threading as th
import time

def report_memory_consumption():
  # go through `gc.get_objects()`, check their size and print a summary
  # takes ~5 min to run

def memory_daemon():
  while True:
    # all other threads should not do anything until this call is complete
    report_memory_consumption()
    # sleep for 10 sec, then update memory summary
    # this sleep is the only time when other threads should be executed
    time.sleep(10)


def f1():
  # do something, including calling many other functions
  # takes ~3 min to run

def f2():
  # do something, including calling many other functions
  # takes ~3 min to run


def main():
  t_mem = th.Thread(target = memory_daemon)
  t1 = th.Thread(target = f1)
  t2 = th.Thread(target = f2)
  t_mem.start()
  t1.start()
  t2.start()

# requirement: no other thread is running while t_mem is not sleeping

4 个答案:

答案 0 :(得分:3)

您应该使用线程锁在线程之间同步执行代码。给出的答案有点正确,但我会使用可重入的当地人再次检查,看看你是否确实有锁。

请勿使用另一个答案中描述的变量来检查锁定占有率。变量可能在多个线程之间被破坏。重入锁是为了解决这个问题。

此代码中的错误是,假设代码之间的代码不会抛出异常,则会释放锁定。总是在with上下文或try-catch-finally中进行。

这是一个很好的article解释Python和线程docs的同步。

编辑:回答OP在C中嵌入Python的更新

你误解了他在食谱中所说的话。如果GIL在当前python解释器中可用,则PyGILState_Ensure返回GIL,而不是python解释器不知道的C线程。

您无法强制从当前解释器中的其他线程获取GIL。想象一下,如果你能够,那么基本上你会蚕食所有其他线程。

答案 1 :(得分:2)

Python Cookbook是正确的。您可以在PyGILState_Ensure()返回时独占访问Python解释器。独占访问意味着您可以安全地调用所有CPython功能。这意味着当前的C线程也是当前活动的Python线程。如果当前的C线程之前没有相应的Python线程,PyGILState_Ensure()将自动为您创建一个。

这就是PyGILState_Ensure()之后的状态。此时你还获得了GIL。

但是,当你现在调用其他CPython函数时,例如PyEval_EvalCode()或任何其他函数,它们可以隐式地使GIL同时被释放。例如,如果隐式地将Python语句time.sleep(0.1)作为结果调用到某个地方就是这种情况。当GIL从该线程中释放时,其他Python线程可以运行。

您只能保证当PyEval_EvalCode()(或您调用的任何其他CPython函数)返回时,您将再次具有与之前相同的状态 - 即您处于相同的活动Python线程中并且您再次拥有GIL。


关于你原来的问题:目前没有办法实现这一点,即调用Python代码并避免GIL在某个地方同时被释放。这是一件好事,否则你很容易陷入死锁,例如:如果你不允许其他一些线程释放它当前持有的一些锁。

关于如何实现您的用例:唯一真正的方法是在C中。您可以调用PyGILState_Ensure()来获取GIL。此时,您必须只调用那些不会产生调用其他Python代码副作用的CPython函数。要非常小心。即使PyObj_DecRef()也可以致电__del__。最好的办法是避免调用任何CPython函数并手动遍历CPython对象。请注意,您可能不必如您所述那样复杂:有底层的CPython内存分配器,我认为您可以从那里获取信息。

阅读here关于CPython中的内存管理。

相关代码位于pymem.hobmalloc.cpyarena.c。请参阅函数_PyObject_DebugMallocStats(),尽管可能无法将其编译到CPython中。

还有tracemalloc module但是会​​增加一些开销。也许它的底层C代码(文件_tracemalloc.c)有助于更好地理解内部结构。


关于sys.setswitchinterval(1000):这只与通过Python字节码并处理它有关。这基本上是文件ceval.cPyEval_EvalFrameEx中CPython的主循环。在那里你会找到这样的部分:

if (_Py_atomic_load_relaxed(&gil_drop_request))
    ...

文件ceval_gil.h中包含切换间隔的所有逻辑。

设置高开关间隔只意味着PyEval_EvalFrameEx中的主循环不会中断更长时间。这并不意味着没有其他可能性同时释放GIL并且另一个线程可以运行。

PyEval_EvalFrameEx将执行Python字节码。我们假设这会调用time.sleep(1)。这将调用该函数的本机C实现。您会在文件timemodule.c中的time_sleep()中找到它。如果你遵循该代码,你会发现:

Py_BEGIN_ALLOW_THREADS
err = select(0, (fd_set *)0, (fd_set *)0, (fd_set *)0, &timeout);
Py_END_ALLOW_THREADS

因此,GIL同时被释放。现在,等待GIL的任何其他线程都可以选择并运行其他Python代码。

从理论上讲,你可以想一想,如果你设置一个高开关间隔并且从不调用任何Python代码,而这些代码又可以在某个时刻释放GIL,那么你就是安全的。请注意,这几乎是不可能的。例如。 GC会不时被调用,某些对象的__del__可能会产生各种副作用。

答案 2 :(得分:1)

由于Global Interpreter Lock,Python 总是一次执行一个线程。涉及multiprocessing时,它不会这样做。您可以看到this answer以了解有关CPython中GIL的更多信息。

请注意,这个伪代码因为我不知道你是如何创建线程/使用它们/你在线程中执行哪些代码。

import threading, time

l=threading.Lock()
locked=False

def worker():
    l.acquire()
    locked=True
    #do something
    l.release()

def test():
    while locked:
        time.sleep(10)
    #do something

threads = []
t = threading.Thread(target=worker)
threads.append(t)
t = threading.Thread(target=test)
threads.append(t)
for th in threads:
    th.start()
for th in threads:
    th.join()

当然,它可能写得更好,可以进行优化。

答案 3 :(得分:1)

作为一种临时解决方案(出于显而易见的原因),以下内容对我有用:

def report_memory_consumption():
  sys.setswitchinterval(1000) # longer than the expected run time
  # go through `gc.get_objects()`, check their size and print a summary
  # takes ~5 min to run
  sys.setswitchinterval(0.005) # the default value

如果有人有更好的答案,请发布。