线程
一、操作系统线程理论
1.1 线程概念的引入背景
进程回顾
前面已经了解了操作系统中进程的概念,程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
有了进程为什么要有线程
进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:
- 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
- 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
线程的出现
60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
因此在80年代,出现了能独立运行的基本单位——线程(Threads)。
1.2 进程和线程的关系
线程与进程的区别可以归纳为以下4点:
- 地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
- 通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
- 在多线程操作系统中,进程不是一个可执行的实体。
知乎:线程和进程的区别是什么?以及:漫画了解线程与进程
1.3 线程的特点
在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性:
1)轻型实体:
线程中的实体基本上不拥有系统资源,只是有一点必不可少的,能保证独立运行的资源。
线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。
TCB包括以下信息,用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
线程状态。
当线程不运行时,被保存的现场资源。
一组执行堆栈。
存放每个线程的局部变量主存区。
访问同一个进程中的主存和其它资源。
2)独立调度和分派的基本单位:
在多线程操作系统中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很"轻",故线程的切换非常迅速且开销小(在同一进程中的)。
3)共享进程资源:
线程在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的进程id,这意味着,线程可以访问该进程的每一个内存资源;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
4)可并发执行:
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
1.4 使用线程的实际场景
开启一个处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程。如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。
1.5 内存中的线程
1)多个线程共享同一个进程的地址空间中的资源,是对一台计算机上多个进程的模拟,有时也称线程为轻量级的进程。
2)而对一台计算机上多个进程,则共享物理内存、磁盘、打印机等其他物理资源。多线程的运行也与多进程的运行类似,是cpu在多个线程之间的快速切换。
3)不同的进程之间是充满敌意的,彼此是抢占、竞争cpu的关系,如果迅雷会和QQ抢资源。而同一个进程是由一个程序员的程序创建,所以同一进程内的线程是合作关系,一个线程可以访问另外一个线程的内存地址,大家都是共享的。
4)类似于进程,每个线程也有自己的堆栈,不同于进程,线程库无法利用时钟中断强制线程让出CPU,可以调用 thread_yield 运行线程自动放弃cpu,让另外一个线程运行。
线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:
- 父进程有多个线程,那么开启的子线程是否需要同样多的线程;
- 在同一个进程中,如果一个线程关闭了文件,而另外一个线程正准备往该文件内写内容呢?
- 因此,在多线程的代码中,需要更多的心思来设计程序的逻辑、保护程序的数据。
小结:
进程 是 最小的 内存分配单位。
线程 是 操作系统调度的 最小单位。
线程直接被CPU执行,进程内至少含有一个线程,也可以开启多个线程:
开启一个线程所需要的时间要远远小于开启一个进程;
多个线程内部有自己的数据栈,数据不共享;
全局变量在多个线程之间是共享的。
二、线程与Python
2.1 理论知识
GIL(全局解释器锁)
在多线程环境中,Python 虚拟机按以下方式执行:
- 设置 GIL;
- 切换到一个线程去运行;
- 运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
- 把线程设置为睡眠状态;
- 解锁 GIL;
- 再次重复以上所有步骤。
2.2 python线程模块的选择
- Python提供了几个用于多线程编程的模块,包括thread、threading和Queue等。thread和threading模块允许程序员创建和管理线程。thread模块提供了基本的线程和锁的支持,threading提供了更高级别、功能更强的线程管理的功能。Queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。
- 避免使用thread模块,因为更高级别的threading模块更为先进,对线程的支持更为完善,而且使用thread模块里的属性有可能会与threading出现冲突;其次低级别的thread模块的同步原语很少(实际上只有一个),而threading模块则有很多;再者,thread模块中当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作,至少threading模块能确保重要的子线程退出后进程才退出。
- thread模块不支持守护线程,当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出。而threading模块支持守护线程,守护线程一般是一个等待客户请求的服务器,如果没有客户提出请求它就在那等着,如果设定一个线程为守护线程,就表示这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。
2.3 线程(Threading.Thread)
线程的创建
import time import threading def task(name): time.sleep(2) print(name) if __name__ == "__main__": t = threading.Thread(target=task, args=("pd",)) t.start() print("主线程")
import time import threading class MyThread(threading.Thread): def __init__(self, name): super().__init__() self.name = name def run(self): time.sleep(2) print(self.name) if __name__ == "__main__": t = MyThread("pd") t.start() print("主线程")
多线程与多进程
import os import threading import multiprocessing def task(): print(os.getpid()) if __name__ == "__main__": # 在主进程下开启多个线程,每个线程都跟主进程的pid一样 t1 = threading.Thread(target=task) t2 = threading.Thread(target=task) t1.start() t2.start() print("主进程/主线程pid", os.getpid()) # 开多个进程,每个进程都有不同的pid p1 = multiprocessing.Process(target=task) p2 = multiprocessing.Process(target=task) p1.start() p2.start() print("主进程/主线程pid", os.getpid())
import time import threading import multiprocessing def task(): pass if __name__ == "__main__": # 多线程 s = time.time() t_list = [] for i in range(100): t = threading.Thread(target=task) t.start() t_list.append(t) for t in t_list: t.join() e = time.time() t1 = e - s # 多进程 s= time.time() p_list = [] for i in range(100): t = multiprocessing.Process(target=task) t.start() p_list.append(t) for t in p_list: t.join() e = time.time() t2 = e - s print(t1, t2)
import threading import multiprocessing def task(): global n n = 0 if __name__ == "__main__": # n = 10 # p = multiprocessing.Process(target=task) # p.start() # p.join() # print("主", n) # 毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为10 n = 10 t = threading.Thread(target=task) t.start() t.join() print("主", n) # 查看结果为0, 因为同一进程内的线程之间共享进程内的数据
多线程socket
import socket import threading sk = socket.socket() sk.bind(("127.0.0.1",8080)) sk.listen(5) def task(conn): while True: data=conn.recv(1024) print(data) conn.send(data.upper()) if __name__ == "__main__": while True: conn, addr = sk.accept() t = threading.Thread(target=task,args=(conn,)) t.start()
import socket sk = socket.socket() sk.connect(("127.0.0.1",8080)) while True: inp = input(">>>").strip() if not inp: continue sk.send(inp.encode("utf-8")) msg=sk.recv(1024) print(msg)
Thread实例对象的其他方法
isAlive() 返回线程是否活动
getName() 返回线程名
setName() 设置线程名
import time import threading def task(n): time.sleep(1) print(n) if __name__ == "__main__": t = threading.Thread(target=task, args=("pd",)) t.start() print(t.getName()) # Thread-1 t.setName("Thread-1111") print(t.getName()) # Thread-1111 t.join() print(t.is_alive()) # False
threading模块提供的一些方法
threading.currentThread() 返回当前的线程变量
threading.enumerate() 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程
threading.activeCount() 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果
import time import threading def task(): time.sleep(5) # 返回当前的线程变量 print(threading.current_thread().getName()) if __name__ == "__main__": # 在主进程下开启线程 t = threading.Thread(target=task) t.start() print(threading.currentThread().getName()) print(threading.currentThread()) # 主线程 print(threading.activeCount()) # 连同主线程在内有两个运行的线程 print(threading.enumerate()) # 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
join()方法
import time import threading def task(): time.sleep(2) print("Hello World!") if __name__ == "__main__": t = threading.Thread(target=task) t.start() t.join() print("主进程") print(t.is_alive())
守护线程
无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。需要强调的是:运行完毕并非终止运行。
# 对主进程来说,运行完毕指的是主进程代码运行完毕 # 对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
- 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程)才会结束。
- 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
import time import threading def task(): time.sleep(2) print("Hello World!") if __name__ == "__main__": t = threading.Thread(target=task) t.setDaemon(True) t.start() print("主进程") print(t.is_alive())
import time import threading def foo(): print("foo.start") time.sleep(1) print("foo.end") def bar(): print("bar.start") time.sleep(3) print("bar.emd") if __name__ == "__main__": t1 = threading.Thread(target=foo) t2 = threading.Thread(target=bar) t1.setDaemon(True) t1.start() t2.start() print("主进程")
2.4 锁(Lock)
同步锁
import time import threading def task(): global n temp = n time.sleep(0.1) n = temp-1 if __name__ == "__main__": n = 100 t_list = [] for i in range(100): t = threading.Thread(target=task) t_list.append(t) t.start() for t in t_list: t.join() print(n) # 99
import time import threading def task(lock): global n lock.acquire() temp = n time.sleep(0.1) n = temp-1 lock.release() if __name__ == "__main__": lock = threading.Lock() n = 100 t_list = [] for i in range(100): t = threading.Thread(target=task, args=(lock,)) t_list.append(t) t.start() for t in t_list: t.join() print(n) # 结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全
未加锁的代码并发运行,加锁的代码串行运行:
import time import threading def task(lock): # 未加锁的代码并发运行 print(threading.current_thread().getName()) global n # 加锁的代码串行运行 lock.acquire() temp = n time.sleep(0.1) n = temp-1 lock.release() if __name__ == "__main__": lock = threading.Lock() n = 100 t_list = [] for i in range(100): t = threading.Thread(target=task, args=(lock,)) t_list.append(t) t.start() for t in t_list: t.join() print(n)
疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,就不用加锁了啊,也是串行的效果啊。
没错:在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,数据是安全的,但问题是start后立即join,任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的。单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高。
死锁与递归锁
进程也有死锁与递归锁。
死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象;若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程/线程称为死锁进程/线程。
from threading import Lock l = Lock() l.acquire() l.acquire() print(1111) # 没有打印,程序hang住了 l.release() l.release()
解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
from threading import RLock l = RLock() l.acquire() l.acquire() print(1111) # 1111 l.release() l.release()
典型示例:
import time import threading l1 = threading.Lock() l2 = threading.Lock() def eat1(name): l1.acquire() print("\033[32m%s:\033[0m拿到面条了" % name) l2.acquire() print("\033[32m%s:\033[0m拿到筷子了" % name) print("\033[31m%s:\033[0m可以吃面了" % name) l2.release() l1.release() def eat2(name): l2.acquire() print("\033[32m%s:\033[0m拿到筷子了" % name) time.sleep(1) l1.acquire() print("\033[32m%s:\033[0m拿到面条了" % name) print("\033[31m%s:\033[0m可以吃面了" % name) l1.release() l2.release() if __name__ == "__main__": for name in ["A", "B", "C"]: threading.Thread(target=eat1, args=(name,)).start() threading.Thread(target=eat2, args=(name,)).start()
import time import threading l1 = l2 = threading.RLock() def eat1(name): l1.acquire() print("\033[32m%s:\033[0m拿到面条了" % name) l2.acquire() print("\033[32m%s:\033[0m拿到筷子了" % name) print("\033[31m%s:\033[0m可以吃面了" % name) l2.release() l1.release() def eat2(name): l2.acquire() print("\033[32m%s:\033[0m拿到筷子了" % name) time.sleep(1) l1.acquire() print("\033[32m%s:\033[0m拿到面条了" % name) print("\033[31m%s:\033[0m可以吃面了" % name) l1.release() l2.release() if __name__ == "__main__": for name in ["A", "B", "C"]: threading.Thread(target=eat1, args=(name,)).start() threading.Thread(target=eat2, args=(name,)).start()
2.5 信号量(Semaphore)
同进程的一样。
- Semaphore管理一个内置的计数器,
- 每当调用acquire()时内置计数器-1;
- 调用release() 时内置计数器+1;
- 计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
import time import threading def task(n, sem): sem.acquire() time.sleep(1) print(n*n) sem.release() if __name__ == "__main__": sem = threading.Semaphore(3) # 3个3个地执行 for i in range(10): t = threading.Thread(target=task, args=(i, sem)) t.start()
# 进程池与信号量 与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程。
2.6 事件(Event)
同进程的一样。
线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步
问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件
的发生。在初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞
直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的
Event对象,那么它将忽略这个事件, 继续执行。
import threading e = thrading.Event() e.is_set() 返回 e 的状态值; e.wait() 如果 e.is_set()为False 将阻塞线程; e.set() 设置 e 的状态值为 True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度; e.clear() 恢复 e 的状态值为False。
2.7 条件(Condition)
使得线程等待,只有满足某条件时,才释放n个线程。
threading.Condition对象提供了对复杂线程同步问题的支持。Condition被称为条件变量,除了提供与Lock类似的acquire和release方法外,还提供了
wait和notify方法。线程首先acquire一个条件变量,然后判断一些条件。如果条件不满足则wait;如果条件满足,进行一些处理改变条件后,通过notify
方法通知其他线程,其他处于wait状态的线程接到通知后会重新判断条件。不断的重复这一过程,从而解决复杂的同步问题。
import os import threading def task(i, c): c.acquire() c.wait() # 等钥匙 print("%s: %s" % (os.getpid(), i)) c.release() if __name__ == "__main__": c= threading.Condition() for i in range(10): t = threading.Thread(target=task, args=(i, c)) t.start() while True: num = int(input(">>>")) c.acquire() c.notify(num) # 造钥匙 c.release()
2.8 定时器(Timer)
定时器,指定n秒后执行某个操作。
# 等待5秒后t.start()开始执行,然后每隔1秒调用task函数 import time import threading def task(): print("时间同步") if __name__ == "__main__": while True: t = threading.Timer(5, task) t.start() time.sleep(1)
2.9 线程队列
导入:import queue,用法与进程Queue一样。
import queue q = queue.Queue() q.put(1) q.put(2) q.put(3) print(q.get()) print(q.get()) print(q.get()) """ 1 2 3 """
import queue q = queue.LifoQueue() q.put(1) q.put(2) q.put(3) print(q.get()) print(q.get()) print(q.get()) """ 3 2 1 """
import queue q = queue.PriorityQueue() # put进一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高。 q.put((2, "b")) q.put((1, "a")) q.put((3, "c")) print(q.get()) print(q.get()) print(q.get()) """ (1, 'a') (2, 'b') (3, 'c') """
Constructor for a priority queue. maxsize is an integer that sets the upperbound limit on the number of items that can be placed in the queue. Insertion will block once this size has been reached, until queue items are consumed. If maxsize is less than or equal to zero, the queue size is infinite. The lowest valued entries are retrieved first (the lowest valued entry is the one returned by sorted(list(entries))[0]). A typical pattern for entries is a tuple in the form: (priority_number, data). exception queue.Empty Exception raised when non-blocking get() (or get_nowait()) is called on a Queue object which is empty. exception queue.Full Exception raised when non-blocking put() (or put_nowait()) is called on a Queue object which is full. Queue.qsize() Queue.empty() #return True if empty Queue.full() # return True if full Queue.put(item, block=True, timeout=None) Put item into the queue. If optional args block is true and timeout is None (the default), block if necessary until a free slot is available. If timeout is a positive number, it blocks at most timeout seconds and raises the Full exception if no free slot was available within that time. Otherwise (block is false), put an item on the queue if a free slot is immediately available, else raise the Full exception (timeout is ignored in that case). Queue.put_nowait(item) Equivalent to put(item, False). Queue.get(block=True, timeout=None) Remove and return an item from the queue. If optional args block is true and timeout is None (the default), block if necessary until an item is available. If timeout is a positive number, it blocks at most timeout seconds and raises the Empty exception if no item was available within that time. Otherwise (block is false), return an item if one is immediately available, else raise the Empty exception (timeout is ignored in that case). Queue.get_nowait() Equivalent to get(False). Two methods are offered to support tracking whether enqueued tasks have been fully processed by daemon consumer threads. Queue.task_done() Indicate that a formerly enqueued task is complete. Used by queue consumer threads. For each get() used to fetch a task, a subsequent call to task_done() tells the queue that the processing on the task is complete. If a join() is currently blocking, it will resume when all items have been processed (meaning that a task_done() call was received for every item that had been put() into the queue). Raises a ValueError if called more times than there were items placed in the queue. Queue.join() block直到queue被消费完毕 更多方法说明
三、Python标准模块--concurrent.futures
https://docs.python.org/dev/library/concurrent.futures.html
# 介绍 concurrent.futures 这个模块提供了高度封装的异步调用接口 ThreadPoolExecutor 线程池,提供异步调用 ProcessPoolExecutor 进程池,提供异步调用 两者都实现相同的接口,该接口由抽象的Executor类定义。 # 基本方法 # submit(func, *args, **kwargs) 异步提交任务 # map(func, *iterables, timeout=None, chunksize=1) 取代for循环submit的操作 # shutdown(wait=True) 相当于进程池的pool.close()+pool.join()操作; wait=True,等待池内所有任务执行完毕回收完资源后才继续; wait=False,立即返回,并不会等待池内的任务执行完毕; 但不管wait参数为何值,整个程序都会等到所有任务执行完毕; submit和map必须在shutdown之前。 # result(timeout=None) 取得结果 # add_done_callback(func) 回调函数
#介绍 The ProcessPoolExecutor class is an Executor subclass that uses a pool of processes to execute calls asynchronously. ProcessPoolExecutor uses the multiprocessing module, which allows it to side-step the Global Interpreter Lock but also means that only picklable objects can be executed and returned. class concurrent.futures.ProcessPoolExecutor(max_workers=None, mp_context=None) An Executor subclass that executes calls asynchronously using a pool of at most max_workers processes. If max_workers is None or not given, it will default to the number of processors on the machine. If max_workers is lower or equal to 0, then a ValueError will be raised. # 用法 import os import time import random from concurrent.futures import ProcessPoolExecutor def task(n): print("%s is runing" % os.getpid()) time.sleep(random.randint(1, 3)) return n**2 if __name__ == "__main__": executor = ProcessPoolExecutor(max_workers=3) futures = [] for i in range(10): future = executor.submit(task, i) futures.append(future) executor.shutdown(True) print("*"*20) for future in futures: print(future.result())
#介绍 ThreadPoolExecutor is an Executor subclass that uses a pool of threads to execute calls asynchronously. class concurrent.futures.ThreadPoolExecutor(max_workers=None, thread_name_prefix='') An Executor subclass that uses a pool of at most max_workers threads to execute calls asynchronously. Changed in version 3.5: If max_workers is None or not given, it will default to the number of processors on the machine, multiplied by 5, assuming that ThreadPoolExecutor is often used to overlap I/O instead of CPU work and the number of workers should be higher than the number of workers for ProcessPoolExecutor. New in version 3.6: The thread_name_prefix argument was added to allow users to control the threading.Thread names for worker threads created by the pool for easier debugging. #用法与ProcessPoolExecutor相同。 import time from concurrent.futures import ThreadPoolExecutor def task(n): time.sleep(2) print(n) return n*n TPool = ThreadPoolExecutor(max_workers=5) # 默认不要超过cpu个数的5倍 t_lst = [] for i in range(10): t = TPool.submit(task, i) # 异步提交任务,使用submit能拿到返回值 t_lst.append(t) TPool.shutdown() for t in t_lst: print("**:", t.result())
import time from concurrent.futures import ThreadPoolExecutor def task(n): time.sleep(2) print(n) tp = ThreadPoolExecutor(max_workers=5) tp.map(task, range(10)) # 使用map拿不到返回值
import time from concurrent.futures import ThreadPoolExecutor def task(n): time.sleep(2) print(n) return n*n def call_back(m): print("结果:%s" % m.result()) if __name__ == "__main__": tp = ThreadPoolExecutor(max_workers=5) # 默认不要超过cpu个数的5倍 for i in range(10): tp.submit(task, i).add_done_callback(call_back) # 回调函数 # pd.shutdown() # shutdown相当于完成了close+join功能 print("主线程") # 没有shutdown主线程第一时间执行;有shutdown需等子线程执行完后才执行。