【问题标题】:Threading pool similar to the multiprocessing Pool?线程池类似于多处理池?
【发布时间】:2021-08-28 06:04:44
【问题描述】:

是否有用于工作线程线程的 Pool 类,类似于多处理模块的 Pool class?

例如,我喜欢并行化地图函数的简单方法

def long_running_func(p):
    c_func_no_gil(p)

p = multiprocessing.Pool(4)
xs = p.map(long_running_func, range(100))

但是我想这样做而不需要创建新进程的开销。

我知道 GIL。但是,在我的用例中,该函数将是一个 IO 绑定的 C 函数,python 包装器将在实际函数调用之前为其释放 GIL。

我必须自己写线程池吗?

【问题讨论】:

标签: python multithreading missing-features


【解决方案1】:

如果你不介意执行别人的代码,这里是我的:

注意:您可能需要删除许多额外的代码[添加以更好地说明和演示其工作原理]

注意: Python 命名约定用于方法名和变量名,而不是 camelCase。

工作流程:

  1. MultiThread 类将通过共享锁、工作队列、退出标志和结果在没有线程实例的情况下启动。
  2. MultiThread 创建所有实例后将启动 SingleThread。
  3. 我们可以使用 MultiThread 添加作品(它将负责锁定)。
  4. SingleThreads 将使用中间锁来处理工作队列。
  5. 工作完成后,您可以销毁所有具有共享布尔值的线程。
  6. 在这里,工作可以是任何东西。它可以使用给定的参数自动导入(取消注释 import 行)和处理模块。
  7. 结果将被添加到结果中,我们可以使用 get_results 获得

代码:

import threading
import queue


class SingleThread(threading.Thread):
    def __init__(self, name, work_queue, lock, exit_flag, results):
        threading.Thread.__init__(self)
        self.name = name
        self.work_queue = work_queue
        self.lock = lock
        self.exit_flag = exit_flag
        self.results = results

    def run(self):
        # print("Coming %s with parameters %s", self.name, self.exit_flag)
        while not self.exit_flag:
            # print(self.exit_flag)
            self.lock.acquire()
            if not self.work_queue.empty():
                work = self.work_queue.get()
                module, operation, args, kwargs = work.module, work.operation, work.args, work.kwargs
                self.lock.release()
                print("Processing : " + operation + " with parameters " + str(args) + " and " + str(kwargs) + " by " + self.name + "\n")
                # module = __import__(module_name)
                result = str(getattr(module, operation)(*args, **kwargs))
                print("Result : " + result + " for operation " + operation + " and input " + str(args) + " " + str(kwargs))
                self.results.append(result)
            else:
                self.lock.release()
        # process_work_queue(self.work_queue)

class MultiThread:
    def __init__(self, no_of_threads):
        self.exit_flag = bool_instance()
        self.queue_lock = threading.Lock()
        self.threads = []
        self.work_queue = queue.Queue()
        self.results = []
        for index in range(0, no_of_threads):
            thread = SingleThread("Thread" + str(index+1), self.work_queue, self.queue_lock, self.exit_flag, self.results)
            thread.start()
            self.threads.append(thread)

    def add_work(self, work):
        self.queue_lock.acquire()
        self.work_queue._put(work)
        self.queue_lock.release()

    def destroy(self):
        self.exit_flag.value = True
        for thread in self.threads:
            thread.join()

    def get_results(self):
        return self.results


class Work:
    def __init__(self, module, operation, args, kwargs={}):
        self.module = module
        self.operation = operation
        self.args = args
        self.kwargs = kwargs


class SimpleOperations:
    def sum(self, *args):
        return sum([int(arg) for arg in args])

    @staticmethod
    def mul(a, b, c=0):
        return int(a) * int(b) + int(c)


class bool_instance:
    def __init__(self, value=False):
        self.value = value

    def __setattr__(self, key, value):
        if key != "value":
            raise AttributeError("Only value can be set!")
        if not isinstance(value, bool):
            raise AttributeError("Only True/False can be set!")
        self.__dict__[key] = value
        # super.__setattr__(key, bool(value))

    def __bool__(self):
        return self.value

if __name__ == "__main__":
    multi_thread = MultiThread(5)
    multi_thread.add_work(Work(SimpleOperations(), "mul", [2, 3], {"c":4}))
    while True:
        data_input = input()
        if data_input == "":
            pass
        elif data_input == "break":
            break
        else:
            work = data_input.split()
            multi_thread.add_work(Work(SimpleOperations(), work[0], work[1:], {}))
    multi_thread.destroy()
    print(multi_thread.get_results())

【讨论】:

    【解决方案2】:

    是的,有一个类似于多处理池的线程池,但是它有些隐藏并且没有正确记录。您可以通过以下方式导入它:-

    from multiprocessing.pool import ThreadPool
    

    我只是举个简单的例子

    def test_multithread_stringio_read_csv(self):
            # see gh-11786
            max_row_range = 10000
            num_files = 100
    
            bytes_to_df = [
                '\n'.join(
                    ['%d,%d,%d' % (i, i, i) for i in range(max_row_range)]
                ).encode() for j in range(num_files)]
            files = [BytesIO(b) for b in bytes_to_df]
    
            # read all files in many threads
            pool = ThreadPool(8)
            results = pool.map(self.read_csv, files)
            first_result = results[0]
    
            for result in results:
                tm.assert_frame_equal(first_result, result) 
    

    【讨论】:

      【解决方案3】:

      另一种方法是将进程添加到线程队列池中

      import concurrent.futures
      with concurrent.futures.ThreadPoolExecutor(max_workers=cpus) as executor:
          for i in range(10):
              a = executor.submit(arg1, arg2,....)
      

      【讨论】:

        【解决方案4】:

        这是我最终使用的结果。这是上面 dgorissen 的类的修改版本。

        文件:threadpool.py

        from queue import Queue, Empty
        import threading
        from threading import Thread
        
        
        class Worker(Thread):
            _TIMEOUT = 2
            """ Thread executing tasks from a given tasks queue. Thread is signalable, 
                to exit
            """
            def __init__(self, tasks, th_num):
                Thread.__init__(self)
                self.tasks = tasks
                self.daemon, self.th_num = True, th_num
                self.done = threading.Event()
                self.start()
        
            def run(self):       
                while not self.done.is_set():
                    try:
                        func, args, kwargs = self.tasks.get(block=True,
                                                           timeout=self._TIMEOUT)
                        try:
                            func(*args, **kwargs)
                        except Exception as e:
                            print(e)
                        finally:
                            self.tasks.task_done()
                    except Empty as e:
                        pass
                return
        
            def signal_exit(self):
                """ Signal to thread to exit """
                self.done.set()
        
        
        class ThreadPool:
            """Pool of threads consuming tasks from a queue"""
            def __init__(self, num_threads, tasks=[]):
                self.tasks = Queue(num_threads)
                self.workers = []
                self.done = False
                self._init_workers(num_threads)
                for task in tasks:
                    self.tasks.put(task)
        
            def _init_workers(self, num_threads):
                for i in range(num_threads):
                    self.workers.append(Worker(self.tasks, i))
        
            def add_task(self, func, *args, **kwargs):
                """Add a task to the queue"""
                self.tasks.put((func, args, kwargs))
        
            def _close_all_threads(self):
                """ Signal all threads to exit and lose the references to them """
                for workr in self.workers:
                    workr.signal_exit()
                self.workers = []
        
            def wait_completion(self):
                """Wait for completion of all the tasks in the queue"""
                self.tasks.join()
        
            def __del__(self):
                self._close_all_threads()
        
        
        def create_task(func, *args, **kwargs):
            return (func, args, kwargs)
        

        使用游泳池

        from random import randrange
        from time import sleep
        
        delays = [randrange(1, 10) for i in range(30)]
        
        def wait_delay(d):
            print('sleeping for (%d)sec' % d)
            sleep(d)
        
        pool = ThreadPool(20)
        for i, d in enumerate(delays):
            pool.add_task(wait_delay, d)
        pool.wait_completion()
        

        【讨论】:

        • 对其他读者的注解:此代码是 Python 3 (shebang #!/usr/bin/python3)
        • 为什么使用for i, d in enumerate(delays):,然后忽略i的值?
        • @martineau - 可能只是开发的遗物,他们可能想在运行期间打印i。
        • 为什么create_task 在那里?它有什么用?
        • 我无法相信并以 4 票回答 SO 是在 Python 中执行 ThreadPooling 的方式。官方python发行版中的Threadpool还是坏掉了?我错过了什么?
        【解决方案5】:

        对于一些非常简单和轻量级的东西(从here稍微修改):

        from Queue import Queue
        from threading import Thread
        
        
        class Worker(Thread):
            """Thread executing tasks from a given tasks queue"""
            def __init__(self, tasks):
                Thread.__init__(self)
                self.tasks = tasks
                self.daemon = True
                self.start()
        
            def run(self):
                while True:
                    func, args, kargs = self.tasks.get()
                    try:
                        func(*args, **kargs)
                    except Exception, e:
                        print e
                    finally:
                        self.tasks.task_done()
        
        
        class ThreadPool:
            """Pool of threads consuming tasks from a queue"""
            def __init__(self, num_threads):
                self.tasks = Queue(num_threads)
                for _ in range(num_threads):
                    Worker(self.tasks)
        
            def add_task(self, func, *args, **kargs):
                """Add a task to the queue"""
                self.tasks.put((func, args, kargs))
        
            def wait_completion(self):
                """Wait for completion of all the tasks in the queue"""
                self.tasks.join()
        
        if __name__ == '__main__':
            from random import randrange
            from time import sleep
        
            delays = [randrange(1, 10) for i in range(100)]
        
            def wait_delay(d):
                print 'sleeping for (%d)sec' % d
                sleep(d)
        
            pool = ThreadPool(20)
        
            for i, d in enumerate(delays):
                pool.add_task(wait_delay, d)
        
            pool.wait_completion()
        

        要支持任务完成时的回调,您只需将回调添加到任务元组即可。

        【讨论】:

        • 如果线程无条件无限循环,它们如何加入?
        • @JosephGarvin 我已经测试过了,线程一直阻塞在一个空队列上(因为对Queue.get()的调用是阻塞的)直到程序结束,之后它们会自动终止。跨度>
        • @JosephGarvin,好问题。 Queue.join() 实际上会加入任务队列,不是工作线程。因此,当队列为空时,wait_completion 返回,程序结束,线程被操作系统回收。
        • 如果所有这些代码都封装到一个简洁的函数中,即使队列为空并且pool.wait_completion() 返回,它似乎也不会停止线程。结果是线程继续构建。
        【解决方案6】:

        是的,它似乎(或多或少)具有相同的 API。

        import multiprocessing
        
        def worker(lnk):
            ....    
        def start_process():
            .....
        ....
        
        if(PROCESS):
            pool = multiprocessing.Pool(processes=POOL_SIZE, initializer=start_process)
        else:
            pool = multiprocessing.pool.ThreadPool(processes=POOL_SIZE, 
                                                   initializer=start_process)
        
        pool.map(worker, inputs)
        ....
        

        【讨论】:

        • ThreadPool 的导入路径与 Pool 不同。正确的导入是from multiprocessing.pool import ThreadPool。
        • 奇怪的是,这不是一个记录在案的 API,multiprocessing.pool 只是简单地提到提供 AsyncResult。但它在 2.x 和 3.x 中可用。
        • 这就是我要找的。这只是一条导入线,对我现有的池线进行了小改动,并且运行良好。
        【解决方案7】:

        您好,要在 Python 中使用线程池,您可以使用这个库:

        from multiprocessing.dummy import Pool as ThreadPool
        

        然后为了使用,这个库就是这样做的:

        pool = ThreadPool(threads)
        results = pool.map(service, tasks)
        pool.close()
        pool.join()
        return results
        

        线程是您想要的线程数,任务是最能映射到服务的任务列表。

        【讨论】:

        • 谢谢,这是一个很好的建议!来自文档: multiprocessing.dummy 复制了多处理的 API,但只不过是线程模块的包装器。一个更正 - 我想你想说池 api 是 (function,iterable)
        • 我们错过了.close() 和.join() 调用,这导致.map() 在所有线程完成之前完成。只是一个警告。
        【解决方案8】:

        没有内置的基于线程的池。但是,使用 Queue 类实现生产者/消费者队列会非常快。

        来自: https://docs.python.org/2/library/queue.html

        from threading import Thread
        from Queue import Queue
        def worker():
            while True:
                item = q.get()
                do_work(item)
                q.task_done()
        
        q = Queue()
        for i in range(num_worker_threads):
             t = Thread(target=worker)
             t.daemon = True
             t.start()
        
        for item in source():
            q.put(item)
        
        q.join()       # block until all tasks are done
        

        【讨论】:

        【解决方案9】:

        在 Python 3 中你可以使用concurrent.futures.ThreadPoolExecutor,即:

        executor = ThreadPoolExecutor(max_workers=10)
        a = executor.submit(my_function)
        

        有关更多信息和示例,请参阅docs。

        【讨论】:

        • 为了使用向后移植的期货模块,运行sudo pip install futures
        • 这是最高效、最快的多处理方式
        • 使用ThreadPoolExecutor和multiprocessing.dummy.Pool有什么区别?
        • concurrent.futures 是 Python 3.9 的时间/3.10 的开头是一个非常有问题的库。看起来它被没有得到适当修复的错误所淹没。也许,这个图书馆的整个前提很糟糕。我更熟悉这个库中基于进程的部分,其中池永远挂起、吞下错误和以其他方式行为不端的原因没有尽头。我会尽可能远离这个图书馆。
        【解决方案10】:

        我刚刚发现multiprocessing 模块中实际上有一个基于线程的池接口,但是它有些隐藏并且没有正确记录。

        可以通过以下方式导入

        from multiprocessing.pool import ThreadPool
        

        它是使用包装python线程的虚拟进程类实现的。这个基于线程的 Process 类可以在multiprocessing.dummy 中找到,在docs 中有简要提及。这个虚拟模块据说提供了基于线程的整个多处理接口。

        【讨论】:

        • 太棒了。我在主线程之外创建 ThreadPools 时遇到问题,但是一旦创建,您就可以从子线程中使用它们。我提出了一个问题:bugs.python.org/issue10015
        • 我不明白为什么这个类没有文档。现在这样的辅助类非常重要。
        • @Wernight:它不公开主要是因为没有人提供提供它(或类似的东西)作为 threading.ThreadPool 的补丁,包括文档和测试。包含在标准库中确实是一个很好的电池,但如果没有人编写它就不会发生。这种现有实现在多处理中的一个很好的优势是,它应该使任何这样的线程补丁非常更容易编写(docs.python.org/devguide)
        • @daniel.gindi: multiprocessing.dummy.Pool/multiprocessing.pool.ThreadPool 是同一个东西,都是线程池。它们模仿进程池的接口,但它们完全是在线程方面实现的。重读文档,你倒退了。
        • @daniel.gindi: Read further: "multiprocessing.dummy 复制了 multiprocessing 的 API,但只不过是 threading 模块的封装。" multiprocessing 通常是关于进程的,但为了允许在进程和线程之间切换,它们(主要)复制了 multiprocessing.dummy 中的 multiprocessing API,但支持线程,而不是进程。目标是让您通过import multiprocessing.dummy as multiprocessing 将基于进程的代码更改为基于线程的代码。
        【解决方案11】:

        创建新进程的开销很小,尤其是当它只有 4 个时。我怀疑这是您的应用程序的性能热点。保持简单,优化你必须做的地方和分析结果指向的地方。

        【讨论】:

        • 如果提问者在 Windows 下(我不相信他指定了),那么我认为进程启动可能是一笔不小的开支。至少在我最近做的项目上是这样。 :-)
        猜你喜欢
        • 2019-04-06
        • 2018-12-04
        • 2018-12-18
        • 1970-01-01
        • 2011-11-03
        • 2015-03-23
        • 2012-03-31
        • 2012-12-06
        • 2018-07-19
        相关资源
        最近更新 更多