Implementación de aprendizaje por refuerzo: búsqueda de puertos inactivos basada en mecanismo de bloqueo en escenarios competitivos

Este artículo fue publicado por primera vez en: Walker AI

En el campo de los juegos del aprendizaje por refuerzo, a menudo consideramos los juegos reales con una lógica compleja como una caja negra y utilizamos la comunicación en red para interactuar con sus datos para lograr el propósito del entrenamiento. Para la eficiencia del entrenamiento, a menudo se usan múltiples procesos en Python para desempeñar los roles de múltiples Actores para interactuar con la caja negra del juego; por lo tanto, en tal entorno, es muy probable que múltiples procesos compitan por el mismo puerto de comunicación (escenario de carrera). ), es fácil hacer que el proceso informe de un error.

Basado en los antecedentes anteriores, este artículo primero brinda una solución convencional, luego analiza las limitaciones del método convencional, luego presenta la solución basada en el mecanismo de bloqueo y finalmente brinda la rutina real de Python. Este artículo se divide principalmente en las siguientes tres partes:

(1) Soluciones convencionales

(2) Búsqueda y uso de puertos inactivos basados ​​en el mecanismo de bloqueo

(3) Implementación del código Python

1. Soluciones convencionales

Para el problema anterior, hay una solución en la que es más fácil pensar: cada vez que un proceso llama a un puerto, verifique si el número de puerto ya está en uso, si es así, omita y continúe buscando el siguiente puerto hasta que aparezca un número de puerto sin usar. se encuentra y se devuelve a la persona que llama. Simplemente implementamos este proceso con python:

import socket 

def get_free_port(ip, init_port): 
    while True: 
        try:
            sock = socket.socket()
            sock.bind((ip, init_port))
            ip, port = sock.getnameinfo()
            sock.close()
            return port
        except Exception:
            init_port += 1
            continue

复制代码

Después de un pequeño análisis de las ideas anteriores, se puede dividir aproximadamente en 3 pasos:

(1) Use la sock.bind()función para vincular automáticamente el puerto para determinar si el puerto está disponible;

(2) Si el puerto puede vincularse para probar que el puerto está disponible, use y sock.close()libere el puerto; si no puede vincularse, prueba que el puerto ha sido utilizado, luego agregue 1 al número de puerto y continúe intentando enlace hasta que tenga éxito, y luego use lo mismo para sock.close()liberar el puerto. ;

(3) Devuelva el número de puerto liberado al programa que necesita ser llamado.

La idea anterior parece funcionar, pero no es suficiente. El uso de esta idea solo puede evitar que el puerto se limite en un período de tiempo muy corto, y es difícil cumplir con el uso en escenarios de competencia. En un entorno de carrera, a medida que aumenta el número de procesos, se presentan con alta probabilidad las siguientes situaciones:


Como se muestra en la figura anterior, el proceso A y el proceso B utilizan el mismo número de puerto X. Para el proceso B, el progreso de la verificación es ligeramente más lento que el del proceso A. Cuando el proceso A libera el puerto X y se prepara para volver al programa para vincularlo, el proceso B vincula el puerto X para su inspección y el proceso A ya no podrá vincular el puerto X. causar un error.

因此可以看出,竞态场景中如何返回一个安全的端口号并不简单,即使利用随机种子随机初始端口号也不能彻底避免这种情况;我们需要一种机制保证一个未绑定的端口号不能被其它进程任意绑定。

2. 基于锁机制的闲置端口查用

使用lock file能够保证即使端口号未绑定,在未拿到锁之前也不会被其他进程绑定。利用fasteners.process_lock.InterProcessLock对端口加锁,具体思路如下:

(1)sock.bind()自动绑定检测端口是否可用;

(2)若端口可用则用InterProcessLock对端口加锁;否则继续检查,直到检测到可用端口后加锁;

(3)解除之前的端口绑定;

(4)安全返回这个端口号;

(5)对该端口解锁;

流程如下:

回顾上述过程,在端口X被绑定后的每一个步骤之间,端口都是安全的。

3. python代码实现

首先定义两个类class BindFreePort()class FreePort(),前者用于搜索并检测可用端口;后者输入前者的检测结果,使用第2章所述的机制将端口安全的输出给需要的程序。

import os
import fasteners
import threading


class BindFreePort(object):
    def __init__(self, start, stop):
        self.port = None

        import random, socket

        self.sock = socket.socket()

        while True:
            port = random.randint(start, stop)
            try:
                self.sock.bind(('127.0.0.1', port))
                self.port = port
				import time
				time.sleep(0.01)
                break
            except Exception:
                continue

    def release(self):
        assert self.port is not None
        self.sock.close()
    

class FreePort(object):
    used_ports = set()

    def __init__(self, start=4000, stop=6000):
        self.lock = None
        self.bind = None
        self.port = None

        from fasteners.process_lock import InterProcessLock
        import time
        pid = os.getpid()

        while True:
            bind = BindFreePort(start, stop)

            print(f'{pid} got port : {bind.port}')

            if bind.port in self.used_ports:
                print(f'{pid} will release port : {bind.port}')
                bind.release()
                continue

            '''
            Since we cannot be certain the user will bind the port 'immediately' (actually it is not possible using
            this flow. We must ensure that the port will not be reacquired even it is not bound to anything
            '''
            lock = InterProcessLock(path='/tmp/socialdna/port_{}_lock'.format(bind.port))
            success = lock.acquire(blocking=False)

            if success:
                self.lock = lock
                self.port = bind.port
                self.used_ports.add(bind.port)
                bind.release()
                break

            bind.release()
            time.sleep(0.01)

    def release(self):
        assert self.lock is not None
        assert self.port is not None
        self.used_ports.remove(self.port)
        self.lock.release()
复制代码

然后,基于这两个类的功能,我们简单测试一下:

def get_and_bind_freeport(*args):
    freeport = FreePort(start=4000, stop=4009)
	import time
	time.sleep(0.5)
    return freeport.port

def test():
    from multiprocessing.pool import Pool
    jobs = 10
    p = Pool(jobs)
    ports = p.map(get_and_bind_freeport, range(jobs))
    print(f'[ports]: {ports}')
    assert len(ports) == len(set(ports))
    p.close()

if __name__ == '__main__':
    test()
复制代码

上述代码中,我们构建了函数get_and_bind_freeport()按照第2章所述机制返回一个端口,用time.sleep(0.5)模拟进程内的时间扰动,其中端口的搜索范围是4000~4009;函数test()从进程池中启动10个进程,每个进程映射一个函数get_and_bind_freeport()4000~4009中搜索一个端口号并将之安全返回。

如果整个过程中端口号是安全的,那么返回结果应当是len(ports) == len(set(ports))即10个端口分别被10个进程查用,不存在多个进程返回同一端口号的情况。

4. 总结

Este documento compara dos métodos para resolver el fenómeno de la competencia multiproceso por puertos de comunicación en el entrenamiento de aprendizaje reforzado de juegos reales; a través del análisis y comparación de principios y experimentos prácticos, llegamos a la conclusión de que el número de puerto de comunicación puede ser verificado y utilizado en el escenario de competencia, en comparación con la idea convencional de control de puerto basado en el mecanismo de bloqueo, se puede aplicar de manera más segura a este escenario. Al mismo tiempo, también nos recuerda que habrá muchos problemas prácticos en el proceso de implementar el aprendizaje por refuerzo en el campo del juego. Solo mediante prácticas continuas y resúmenes en la ingeniería real podemos lograr nuestros objetivos.

5. Referencia

[1] Sujetadores

[2] Sincronización entre procesos


PD: Para productos secos más técnicos, preste atención a [Cuenta pública | xingzhe_ai], ¡y hable con los caminantes!

Supongo que te gusta

Origin juejin.im/post/6966068413567500318
Recomendado
Clasificación