42. 使用線程池

前面實現了一個多線程的web視頻監控服務器,由於服務器資源有限(CPU,內存,帶寬),需要對請求連接數(線程數)做限制,避免因資源耗盡而癱瘓。

要求:使用線程池,替代原來的每次請求創建線程。

解決方案:

使用標準庫彙總concurrent.futures下的ThreadPoolExecutor類,對象的submit()map()方法可以用來啓動線程池中的線程執行任務。


  • 對於ThreadPoolExecutor類:
class concurrent.futures.ThreadPoolExecutor(max_workers=None, thread_name_prefix='', initializer=None, initargs=())

Executor的一個子類,使用最多max_workers個線程的線程池來異步執行調用。

initializer是在每個工作者線程開始處調用的一個可選可調用對象。initargs是傳遞給初始化器的元組參數。任何向池提交更多工作的嘗試,initializer都將引發一個異常,當前所有等待的工作都會引發一個BrokenThreadPool。

submit(fn, *args, **kwargs)

調度可調用對象fn,以fn(*args **kwargs)方式執行並返回Future對象代表可調用對象的執行。

map(func, *iterables, timeout=None, chunksize=1)

類似於map(func, *iterables),不過立即收集iterables而不是延遲再收集,另外func是異步執行的且對func的調用可以併發執行。

>>> import threading, time, random

>>> def f(a, b):
...     print(threading.current_thread().name, ':', a, b)
...     time.sleep(random.randint(5, 10))
...     return a * b
...
>>> from concurrent.futures import ThreadPoolExecutor

>>> executor = ThreadPoolExecutor(3)                #創建3個線程的線程池

>>> executor.submit(f, 2, 3)
ThreadPoolExecutor-0_0 : 2 3
<Future at 0x7f831190a4e0 state=running>

>>> future = executor.submit(f, 2, 3)
ThreadPoolExecutor-0_0 : 2 3

>>> future.result()
6
>>> executor.map(f, range(1, 6), range(2, 7))
ThreadPoolExecutor-0_1 : 1 2
<generator object Executor.map.<locals>.result_iterator at 0x7f830ef736d8>
>>> ThreadPoolExecutor-0_0 : 2 3
ThreadPoolExecutor-0_2 : 3 4
ThreadPoolExecutor-0_1 : 4 5
ThreadPoolExecutor-0_0 : 5 6

>>> list(executor.map(f, range(1, 6), range(2, 7)))
ThreadPoolExecutor-0_2 : 1 2
ThreadPoolExecutor-0_1 : 2 3
ThreadPoolExecutor-0_0 : 3 4
ThreadPoolExecutor-0_2 : 4 5
ThreadPoolExecutor-0_1 : 5 6
[2, 6, 12, 20, 30]

這裏執行map()方法時,首先3個線程執行任務,執行完畢後返回線程池,然後再次得到2個線程執行任務,直到所有任務全部執行完畢。

調用summit()是執行一個任務,而調用map()是對所有任務依次執行。


  • 方案示例:
import os, cv2, time, struct, threading
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import TCPServer, ThreadingTCPServer
from threading import Thread, RLock
from select import select
from concurrent.futures import ThreadPoolExecutor

class JpegStreamer(Thread):
    def __init__(self, camrea):
        super().__init__()
        self.cap = cv2.VideoCapture(camrea)
        self.lock = RLock()
        self.pipes = {}

    def register(self):
        pr, pw = os.pipe()
        self.lock.acquire()
        self.pipes[pr] = pw
        self.lock.release()
        return pr

    def unregister(self, pr):
        self.lock.acquire()
        pw = self.pipes.pop(pr)
        self.lock.release()
        os.close(pr)
        os.close(pw)

    def capture(self):
        cap = self.cap
        while cap.isOpened():
            ret, frame = cap.read()
            if ret:
                ret, data = cv2.imencode('.jpg', frame, (cv2.IMWRITE_JPEG_QUALITY, 40))
                yield data.tostring()

    def send_frame(self, frame):
        n = struct.pack('1', len(frame))
        self.lock.acquire()
        if len(self.pipes):
            _, pipes, _ = select([], self.pipes.values(), [], 1)
            for pipe in pipes:
                os.write(pipe, n)
                os.write(pipe, frame)
        self.lock.release()

    def run(self):
        for frame in self.capture():
            self.send_frame(frame)


class JpegRetriever:
    def __init__(self, streamer):
        self.streamer = streamer
        self.local = threading.local()

    def retrieve(self):
        while True:
            ns = os.read(self.local.pipe, 8)
            n = struct.unpack('1', ns)[0]
            data = os.read(self.local.pipe, n)
            yield data

    def __enter__(self):
        if hasattr(self.local, 'pipe'):
            raise RuntimeError()

        self.local.pipe = streamer.register()
        return self.retrieve()

    def __exit__(self, *args):
        self.streamer.unregister(self.local.pipe)
        del self.local.pipe
        return True


class WebHandler(BaseHTTPRequestHandler):
    retriever = None

    @staticmethod
    def set_retriever(retriever):
        WebHandler.retriever = retriever

    def do_GET(self):
        if self.retriever is None:
            raise RuntimeError('no retriever')

        if self.path != '/':
            return

        self.send_response(200)
        self.send_header('Content-type', 'multipart/x-mixed-replace;boundary=jpeg_frame')
        self.end_headers()

        with self.retriever as frames:
            for frame in frames:
                self.send_frame(frame)

    def send_frame(self, frame):
        sh = b'--jpeg_frame\r\n'
        sh += b'Content-Type: image/jpeg\r\n'
        sh += b'Content-Length: %d\r\n\r\n' % len(frame)
        self.wfile.write(sh)
        self.wfile.write(frame)


class ThreadingPoolTCPServer(ThreadingTCPServer):
    def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, thread_n=100):
        super().__init__(server_address, RequestHandlerClass, bind_and_activate=True)
        self.executor = ThreadPoolExecutor(thread_n)
    
    def process_request(self, request, client_address):
        self.executor.submit(self.process_request_thread, request, client_address)


if __name__ == '__main__':
    # 創建Streamer,開啓攝像頭採集
    streamer = JpegStreamer(0)
    streamer.start()

    # http服務器創建Retriever
    retriever = JpegRetriever(streamer)
    WebHandler.set_retriever(retriever)

    # 開啓http服務器
    HOST = '192.168.30.128'
    POST = 9000
    print('Start server...(http://%s:%d)' % (HOST, POST))
    httpd = ThreadingPoolTCPServer((HOST, POST), WebHandler, thread_n=3)                #線程池線程數量爲3
    httpd.serve_forever()

此時,通過瀏覽器訪問會發現,至多3個窗口可以同時訪問,即至多產生3條訪問記錄,更多訪問無法得到響應,因爲線程池中的線程數量只爲3。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章