执行Python脚本时,脚本块会在线程上阻塞,但在交互模式下不会。

20

TL;DR: 为什么从交互模式(如myprocess.start())启动的线程进程可以按预期运行(即分离的Python线程),但从shell启动(如python myprocess.py)时会在子线程上阻塞?


背景:我为我的类创建了一个子类threading.Thread,它还调用了另外两个Thread类型的子类。它看起来像这样:

class Node(threading.Thread):
    def __init__(self, gps_device):
        threading.Thread.__init__(self)
        self.daemon = False

        logging.info("Setting up GPS service")
        self.gps_svc = gps.CoordinateService(gps_device)
        self.gps_svc.daemon = True

        logging.info("Setting up BLE scanning service")
        # TODO: This is blocking when run in terminal (aka how we do on Raspberry Pi)
        self.scan_svc = scan.BleMonitor()
        self.scan_svc.daemon = True

        logging.info("Node initialized - ready for start")

    def run(self):
        self.gps_svc.start()
        self.scan_svc.start()  # blocks here in terminal

        do stuff...
两个服务(gps_svcscan_svc)在交互模式下像node = Node(...); node.start()一样正常工作。当我使用脚本调用解释器时,gps_svc启动并运行,但scan_svc在监听蓝牙设备的特定行处阻塞。
BLE扫描仪如下所示(有点长)。这是BleMonitor的父类 - 内部没有任何区别,只是添加了几个实用函数。
问题:为什么会这样?我能够运行/与进程交互而不是线程吗(即:调用类的方法并实时获取数据)?
class Monitor(threading.Thread):
    """Continously scan for BLE advertisements."""

    def __init__(self, callback, bt_device_id, device_filter, packet_filter):
        """Construct interface object."""
        # do import here so that the package can be used in parsing-only mode (no bluez required)
        self.bluez = import_module('bluetooth._bluetooth')

        threading.Thread.__init__(self)
        self.daemon = False
        self.keep_going = True
        self.callback = callback

        # number of the bt device (hciX)
        self.bt_device_id = bt_device_id
        # list of beacons to monitor
        self.device_filter = device_filter
        self.mode = get_mode(device_filter)
        # list of packet types to monitor
        self.packet_filter = packet_filter
        # bluetooth socket
        self.socket = None
        # keep track of Eddystone Beacon <-> bt addr mapping
        self.eddystone_mappings = []

    def run(self):
        """Continously scan for BLE advertisements."""
        self.socket = self.bluez.hci_open_dev(self.bt_device_id)

        filtr = self.bluez.hci_filter_new()
        self.bluez.hci_filter_all_events(filtr)
        self.bluez.hci_filter_set_ptype(filtr, self.bluez.HCI_EVENT_PKT)
        self.socket.setsockopt(self.bluez.SOL_HCI, self.bluez.HCI_FILTER, filtr)

        self.toggle_scan(True)

        while self.keep_going:
            pkt = self.socket.recv(255)
            event = to_int(pkt[1])
            subevent = to_int(pkt[3])
            if event == LE_META_EVENT and subevent == EVT_LE_ADVERTISING_REPORT:
                # we have an BLE advertisement
                self.process_packet(pkt)

    def toggle_scan(self, enable):
        """Enable and disable BLE scanning."""
        if enable:
            command = "\x01\x00"
        else:
            command = "\x00\x00"
        self.bluez.hci_send_cmd(self.socket, OGF_LE_CTL, OCF_LE_SET_SCAN_ENABLE, command)

    def process_packet(self, pkt):
        """Parse the packet and call callback if one of the filters matches."""
        # check if this could be a valid packet before parsing
        # this reduces the CPU load significantly
        if (self.mode == MODE_BOTH and \
                (pkt[19:21] != b"\xaa\xfe") and (pkt[19:23] != b"\x4c\x00\x02\x15")) \
                or (self.mode == MODE_EDDYSTONE and (pkt[19:21] != b"\xaa\xfe")) \
                or (self.mode == MODE_IBEACON and (pkt[19:23] != b"\x4c\x00\x02\x15")):
            return

        bt_addr = bt_addr_to_string(pkt[7:13])
        rssi = bin_to_int(pkt[-1])
        # strip bluetooth address and parse packet
        packet = parse_packet(pkt[14:-1])

        # return if packet was not an beacon advertisement
        if not packet:
            return


        # we need to remember which eddystone beacon has which bt address
        # because the TLM and URL frames do not contain the namespace and instance
        self.save_bt_addr(packet, bt_addr)
        # properties hold the identifying information for a beacon
        # e.g. instance and namespace for eddystone; uuid, major, minor for iBeacon
        properties = self.get_properties(packet, bt_addr)

        if self.device_filter is None and self.packet_filter is None:
            # no filters selected
            self.callback(bt_addr, rssi, packet, properties)

        elif self.device_filter is None:
            # filter by packet type
            if is_one_of(packet, self.packet_filter):
                self.callback(bt_addr, rssi, packet, properties)
        else:
            # filter by device and packet type
            if self.packet_filter and not is_one_of(packet, self.packet_filter):
                # return if packet filter does not match
                return

            # iterate over filters and call .matches() on each
            for filtr in self.device_filter:
                if isinstance(filtr, BtAddrFilter):
                    if filtr.matches({'bt_addr':bt_addr}):
                        self.callback(bt_addr, rssi, packet, properties)
                        return

                elif filtr.matches(properties):
                    self.callback(bt_addr, rssi, packet, properties)
                    return

    def save_bt_addr(self, packet, bt_addr):
        """Add to the list of mappings."""
        if isinstance(packet, EddystoneUIDFrame):
            # remove out old mapping
            new_mappings = [m for m in self.eddystone_mappings if m[0] != bt_addr]
            new_mappings.append((bt_addr, packet.properties))
            self.eddystone_mappings = new_mappings

    def get_properties(self, packet, bt_addr):
        """Get properties of beacon depending on type."""
        if is_one_of(packet, [EddystoneTLMFrame, EddystoneURLFrame, \
                              EddystoneEncryptedTLMFrame, EddystoneEIDFrame]):
            # here we retrieve the namespace and instance which corresponds to the
            # eddystone beacon with this bt address
            return self.properties_from_mapping(bt_addr)
        else:
            return packet.properties

    def properties_from_mapping(self, bt_addr):
        """Retrieve properties (namespace, instance) for the specified bt address."""
        for addr, properties in self.eddystone_mappings:
            if addr == bt_addr:
                return properties
        return None

    def terminate(self):
        """Signal runner to stop and join thread."""
        self.toggle_scan(False)
        self.keep_going = False
        self.join()

7
我认为类定义不足以帮助你。我的建议是:拿起你的代码,并从中逐行删除代码,直到找到使进程阻塞的最少量的代码。此时你可能只剩下很少的代码,然后你可以将整个代码贴到这里,以便其他人可以复现你的问题并尝试进行严肃的调试。现在我猜想一下:除了线程外,你是否还在使用 multiprocessing?混合使用这两种方式可能会导致死锁。详情请见:https://bugs.python.org/issue27422 - Bakuriu
4
如果没有一个主循环,你不能在终端上运行脚本(进程无法成为主循环)。系统会终止任何无法解决的子进程。 - dsgdfg
4
以下是我翻译的内容:这是我解决问题的方式,供参考。简而言之,编写一个带有#!path/to/python的独立python.py脚本并直接运行它。但真正的答案是,我从未找出问题所在。 - PANDA Stack
19
@PANDAStack,也许你应该回答自己的问题并关闭它,因为你已经解决了这个问题。 - ramazan polat
3
您的问题不太清楚,提供的信息也不足。是什么让您得出这个线程被阻塞的结论呢?我建议添加打印语句和/或使用线程感知的调试器来跟踪问题。例如 PyCharm 内置的优秀调试器。 - EvertW
显示剩余7条评论
1个回答

1
Python文档中得知,当涉及到线程时,交互模式下的解释器违反了以下规定:
在CPython中,全局解释器锁(GIL)是一个互斥锁,用于保护对Python对象的访问,防止多个线程同时执行Python字节码。这个锁主要是必要的,因为CPython的内存管理不是线程安全的。(然而,由于GIL的存在,其他特性已经依赖于它所强制执行的保证。)
因此,存在这样的规则:只有获取了GIL的线程才能操作Python对象或调用Python/C API函数。为了模拟执行并发,解释器定期尝试切换线程(参见sys.setswitchinterval())。该锁还会在可能阻塞I/O操作(如读取或写入文件)时释放,以便其他Python线程可以同时运行。
我需要进一步研究这个问题,但我的怀疑指向GIL和线程化对象管理之间的冲突。希望这有所帮助,或者有人能补充更多信息。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接