python之线程进程协成
线程与进程
什么是线程
线程是进程一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源,但是它可与同属一个线程的其他的线程所拥有的全部资源。每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。
- 线程可以被抢占(中断)。
- 在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) -- 这就是线程的退让。
多线程
多线程类似于同时执行多个不同程序,多线程运行有如下优点
- 使用线程可以把占据长时间的程序中的任务放到后台去处理。
- 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
- 程序的运行速度可能加快
- 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。
注:线程间的数据是共享的
什么是进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
注:进程间的数据是不共享的
那么问题来了,线程进程能不能并发的去处理问题?答案是肯定的,但是在python中确不支持多线程的并发。而这并不是python语言本身的缺陷,而是python解释器(CPython)的问题,所以说无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行。
在python解释器(CPython)中由于引入了GIL的概念,才有了上述问题的存在。但是我们首先要明确,这并不是python语言本身的缺陷。
说了这么多,线程进程有什么联系和区别?
二者关系
- 一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以互相并发执行。
- 相对进程而言,线程是更加接近与执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
二者区别
进程和线程的主要区别在于他们是不同的操作系统资源管理方式。进程拥有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响,而线程只是
一个进程中的不同执行路径,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉后就等于整个进程都死掉。但是进程切换时,耗费资源
较大,效率更差一些。
- 简而言之,一个程序至少有一个进程,一个进程至少有一个线程
- 线程的划分尺度小于进程,使得多线程程序的并发性高
- 进程在执行过程中拥有独立的内存单元,而对个线程共享内存,从而极大的提高了程序的运行效率
- 线程在执行过程中与进程还是有区别的,每个独立的线程有一个程序运行的入口、执行顺序序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 从逻辑角度看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行,但是操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。
那么二者有什么优缺点呢?
进程:
- 优点:同时利用多个CPU,能够同时进行多个操作
- 缺点:耗费资源(重新开辟内存空间)
线程:
- 优点:共享内存。IO操作时,创造并发操作
- 缺点:抢占资源
说了那么多开始学习线程进程了
线程模块
Python通过两个标准库thread和threading提供对线程的支持。thread提供了低级别的、原始的线程以及一个简单的锁。
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
- run(): 用以表示线程活动的方法。
- start():启动线程活动。
- join(): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
- getName(): 返回线程名。
- setName(): 设置线程名。
使用线程有两种调用方式直接调用和继承式调用
直接调用
import threading import time def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(1) if __name__ == '__main__': t1 = threading.Thread(target=sayhi,args=(1,)) #生成一个线程实例 t2 = threading.Thread(target=sayhi,args=(2,)) #生成另一个线程实例 t1.start() #启动线程 t2.start() #启动另一个线程 print(t1.getName()) #获取线程名 print(t2.getName())
继承式调用
import threading import time class MyThread(threading.Thread): def __init__(self,num): threading.Thread.__init__(self) self.num = num def run(self):#定义每个线程要运行的函数 print("running on number:%s" %self.num) time.sleep(3) if __name__ == '__main__': t1 = MyThread(1) t2 = MyThread(2) t1.start() t2.start()
线程锁(互斥锁)(不好)
开始我们说了,线程间的数据是共享的,如果同时有多个线程去修改数据,就会造成数据的错误,所以这时就需要我们给单个线程加把锁去管理。
import time import threading def addNum(): global num #在每个线程中都获取这个全局变量 print('--get num:',num ) time.sleep(1) num -=1 #对此公共变量进行-1操作 num = 100 #设定一个共享变量 thread_list = [] for i in range(100): t = threading.Thread(target=addNum) t.start() thread_list.append(t) for t in thread_list: #等待所有线程执行完毕 t.join() print('final num:', num )
import time import threading def addNum(): global num #在每个线程中都获取这个全局变量 print('--get num:',num ) time.sleep(1) lock.acquire() #修改数据前加锁 num -=1 #对此公共变量进行-1操作 lock.release() #修改后释放 num = 100 #设定一个共享变量 thread_list = [] lock = threading.Lock() #生成全局锁 for i in range(100): t = threading.Thread(target=addNum) t.start() thread_list.append(t) for t in thread_list: #等待所有线程执行完毕 t.join() print('final num:', num )
Rlock递归锁 (优先选择)
import threading import time globals_num = 0 lock = threading.RLock() def Func(): lock.acquire() #获取锁 global globals_num globals_num += 1 time.sleep(2) print(globals_num) lock.release() #释放锁 for i in range(10): t = threading.Thread(target=Func) t.start()
前面我们说了,在python解释器中不是已经默认给我加了一把GIL锁吗?那为什么我们还要自己去加锁,这不是多次一举吗?当然是否定的。Python的GIL来保证同一时间只能有一个线程来执行了,而这里的lock是用户级的lock,跟那个GIL没关系 。
信号量Semaphore
互斥锁中只允许一个线程修改数据,而信号量同时允许一定数量的线程修改数据。
import threading import time class MyThread(threading.Thread): def run(self): if semaphore.acquire(): print(self.name) time.sleep(2) semaphore.release() if __name__ == '__main__': semaphore = threading.BoundedSemaphore(5) thrs = [] for i in range(20): thrs.append(MyThread()) for t in thrs: t.start()
import threading from random import randint import time class Produce(threading.Thread): def run(self): global L while True: val = randint(0,100) print("生产者", self.name,"Apped " + str(val),L) if lock_conn.acquire(): L.append(val) lock_conn.notify() lock_conn.release() time.sleep(3) class Consuner(threading.Thread): def run(self): global L while True: lock_conn.acquire() if len(L) == 0: lock_conn.wait() print("消费者", self.name,"Daleta" + str(L[0]),L) del L[0] lock_conn.release() time.sleep(1) if __name__ =="__main__": L = [] lock_conn = threading.Condition() threads = [] for i in range(5): threads.append(Produce()) threads.append(Consuner()) for t in threads: t.start() for t in threads: t.join()
同步条件Events
可以通过events实现两个线程或多个线程间的交互。如下例子
import threading import time class Boss(threading.Thread): def run(self): print("今天加班到22:00") event.isSet() or event.set() time .sleep(5) print("下班") event.isSet() or event.set() class Worker(threading.Thread): def run(self): event.wait() print("哎") time.sleep(1) event.clear() event.wait() print("终于下班了") if __name__ == '__main__': event = threading.Event() threads = [] for i in range(5): threads.append(Worker()) threads.append(Boss()) for t in threads: t.start() for t in threads: t.join()
队列queue
python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。
Queue模块中的常用方法:
q=queue.Queue()
- q.qsize() 返回队列的大小
- q.empty() 如果队列为空,返回True,反之False
- q.full() 如果队列满了,返回True,反之False
- q.full 与 maxsize 大小对应
- q.get([block[, timeout]])获取队列,timeout等待时间
- q.get_nowait() 相当Queue.get(False)
- q.put(item) 写入队列,timeout等待时间
- q.put_nowait(item) 相当Queue.put(item, False)
- q.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
- q.join() 实际上意味着等到队列为空,再执行别的操作
import queue import threading message = queue.Queue(10) #创建为10的队列 def producer(i): #定义生产者模型 print("put:", i) message.put(i) def consumer(i): #定义消费者模型 msg = message.get() print(msg) for i in range(10): t = threading.Thread(target=producer, args=(i,)) t.start() for i in range(10): t = threading.Thread(target=consumer, args=(i,)) t.start()
import queue import threading import time exitFlag = 0 class myThread (threading.Thread): def __init__(self, threadID, name, q): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.q = q def run(self): print("Starting " + self.name) process_data(self.name, self.q) print("Exiting " + self.name) def process_data(threadName, q): while not exitFlag: queueLock.acquire() if not workQueue.empty(): data = q.get() queueLock.release() print("%s processing %s" % (threadName, data)) else: queueLock.release() time.sleep(1) threadList = ["Thread-1", "Thread-2", "Thread-3"] nameList = ["One", "Two", "Three", "Four", "Five"] queueLock = threading.Lock() workQueue = queue.Queue(10) threads = [] threadID = 1 # 创建新线程 for tName in threadList: thread = myThread(threadID, tName, workQueue) thread.start() threads.append(thread) threadID += 1 # 填充队列 queueLock.acquire() for word in nameList: workQueue.put(word) queueLock.release() # 等待队列清空 while not workQueue.empty(): pass # 通知线程是时候退出 exitFlag = 1 # 等待所有线程完成 for t in threads: t.join() print("Exiting Main Thread")
多进程
在python中可以调用multiprocessing模块来创建多进程,multiprocessing模块可以给指定的机器充分的去利用CPU资源。
下面创建一个简单的进程
from multiprocessing import Process import time import os def info(title): print(title) print("父进程", os.getppid()) print("子进程", os.getpid()) if __name__ == "__main__": info("\033[32;1m main is in \033[0m") time.sleep(1) p = Process(target=info, args=("hello",)) p.start() p.join()
from multiprocessing import Process import time class MyType(Process): def __init__(self): super(MyType,self).__init__() def run(self): time.sleep(1) print("hell",self.name) if __name__ == "__main__": p_list = [] for i in range(3): p = MyType() p.start() p_list.append(p) for p in p_list: p.join() print("end")
注:在window系统下,需要注意的是要想启动一个子进程,必须加上那句if __name__ == "main",进程相关的要写在这句下面。
进程间通信
由于进程是不共享的,要想实现进程间的数据共享,在python中提供了两种方法 queues 和pipes
queues
用法和threading里的queue差不多
from multiprocessing import Process, Queue import os, time, random # 写数据进程执行的代码: def write(q): print('Process to write: %s' % os.getpid()) for value in ['A', 'B', 'C']: print('Put %s to queue...' % value) q.put(value) time.sleep(random.random()) # 读数据进程执行的代码: def read(q): print('Process to read: %s' % os.getpid()) while True: value = q.get(True) print('Get %s from queue.' % value) if __name__ == '__main__': # 父进程创建Queue,并传给各个子进程: q = Queue() pw = Process(target=write, args=(q,)) pr = Process(target=read, args=(q,)) pw.start()# 启动子进程pw,写入 pr.start()# 启动子进程pr,读取 pw.join()# 等待pw结束 pr.terminate() # pr进程里是死循环,无法等待其结束,只能强行终止
pipes
from multiprocessing import Process, Pipe def f(conn): conn.send([42, None, 'hello']) conn.close() if __name__ == '__main__': parent_conn, child_conn = Pipe() p = Process(target=f, args=(child_conn,)) p.start() print(parent_conn.recv()) # prints "[42, None, 'hello']" p.join()
进程同步
from multiprocessing import Process, Lock def f(l, i): l.acquire() #获取锁 try: print('hello world', i) finally: l.release() #释放锁 if __name__ == '__main__': lock = Lock() for num in range(10): Process(target=f, args=(lock, num)).start()
进程池
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。
如果需要启动大量的子进程,可以用进程池方式批量创建子进程。
两个方法
- apply
- apply_async
from multiprocessing import Pool def f1(i): print(i) return 10 def f2(args): print(args) if __name__ == "__main__": pool = Pool(5) for i in range(20): # pool.apply(func=f1,args=(i,))# apply 串行 pool.apply_async(func=f1, args=(i,), callback=f2) #callback 回调函数 apply_async,并行 pool.close() pool.join()
对Pool
对象调用join()
方法会等待所有子进程执行完毕,调用join()
之前必须先调用close()
,调用close()
之后就不能继续添加新的Process
了。
附:在Unix/Linux下,multiprocessing
模块封装了fork()
调用,使我们不需要关注fork()
的细节。由于Windows没有fork
调用,因此,multiprocessing
需要“模拟”出fork
的效果,父进程所有Python对象都必须通过pickle序列化再传到子进程去,所有,如果multiprocessing
在Windows下调用失败了,要先考虑是不是pickle失败了
协成
协程,又称微线程,纤程。英文名Coroutine。协程是一种用户态的轻量级线程。
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。
子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
协成和线程想比的优势
- 协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
- 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
那么协成的劣势
- 无法利用多核资源:因为协成是由单线程执行,如果要利用多核CPU,最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
- 进行阻塞操作时,会阻塞整个程序。
首先来看yield来实现协成例子
def consumer(): r = '' while True: n = yield r if not n: return print('Consuming %s...' % n) r = '200 OK' def produce(c): c.send(None) n = 0 while n < 5: n = n + 1 print('Producing %s...' % n) r = c.send(n) print('Consumer return: %s' % r) c.close() c = consumer() produce(c)
代码解读
- 首先调用
c.send(None)
启动生成器; - 然后,一旦生产了东西,通过
c.send(n)
切换到consumer
执行; consumer
通过yield
拿到消息,处理,又通过yield
把结果传回;produce
拿到consumer
处理的结果,继续生产下一条消息;produce
决定不生产了,通过c.close()
关闭consumer
,整个过程结束。
gevent
在python中提供Gevent第三方库来实现同步或异步编程
1 import gevent #第三方库文件 2 import time 3 4 def foo(): 5 print("Start") 6 gevent.sleep(1) #模拟IO阻塞 7 8 print("Start 2") 9 10 def bar(): 11 print("Satrt 3") 12 gevent.sleep(2) 13 print("Strart 4") 14 15 gevent.joinall([ 16 gevent.spawn(foo), 17 gevent.spawn(bar) 18 ])
协成实现的一个简单的爬虫特列
from gevent import monkey monkey.patch_all() #最大程度利用IO阻塞 import gevent from urllib.request import urlopen import time def f(url): print("GET: %s" % url) resp = urlopen(url) data = resp.read() print("%d bytes received from %s " % (len(data), url)) start = time.time() gevent.joinall([ gevent.spawn(f, 'https://www.python.org/'), gevent.spawn(f, 'https://www.yahoo.com/') ]) print(time.time() - start)
通过gevent实现单线程下的socket并发
import sys import socket import time import gevent from gevent import socket,monkey monkey.patch_all() def server(port): s = socket.socket() s.bind(('0.0.0.0', port)) s.listen(500) while True: cli, addr = s.accept() gevent.spawn(handle_request, cli) def handle_request(conn): try: while True: data = conn.recv(1024) print("recv:", data) conn.send(data) if not data: conn.shutdown(socket.SHUT_WR) except Exception as ex: print(ex) finally: conn.close() if __name__ == '__main__': server(8001)
import socket HOST = 'localhost' # The remote host PORT = 8001 # The same port as used by the server s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) while True: msg = bytes(input(">>:"),encoding="utf8") s.sendall(msg) data = s.recv(1024) #print(data) print('Received', repr(data)) s.close()
最后一句话总结协成的特点就是:
子程序就是协成的一种特例
附:
daemon:
将线程声明为守护线程,必须在start() 方法调用之前设置, 如果不设置为守护线程程序会被无限挂起。这个方法基本和join是相反的。当我们 在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成想退出时,会检验子线程是否完成。如 果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是 只要主线程完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法啦
IO密集型:(不用CPU)
适合用多线程
计算密集型:(用CPU)
适合用多进程