Python多线程

1、python并发编程之多线程

1.threading模块

multiprocess模块的彻底模仿了threading模块的接口,两者在使用层面,有很大的类似性,于是再也不详细介绍php

1.1 开启线程的两种方式(同Process)html

方式一

from threading import Thread import time def sayhi(name): time.sleep(2) print('%s say hello' %name) if __name__ == '__main__': t=Thread(target=sayhi,args=('egon',)) t.start() print('主线程')

方式二

from threading import Thread import time class Sayhi(Thread): def __init__(self,name): super().__init__() self.name=name def run(self): time.sleep(2) print('%s say hello' % self.name) if __name__ == '__main__': t = Sayhi('egon') t.start() print('主线程')

 

1.2 在一个进程下开启多个线程与在一个进程下开启多个子进程的区别python

  1. 谁的开启速度更快?
from threading import Thread from multiprocessing import Process import os def work(): print('hello') if __name__ == '__main__': #在主进程下开启线程 t=Thread(target=work) t.start() print('主线程/主进程') ''' 打印结果: hello 主线程/主进程 ''' #在主进程下开启子进程 t=Process(target=work) t.start() print('主线程/主进程') ''' 打印结果: 主线程/主进程 hello '''
  1. 瞅一瞅pid?
from threading import Thread from multiprocessing import Process import os def work(): print('hello',os.getpid()) if __name__ == '__main__': #part1:在主进程下开启多个线程,每一个线程都跟主进程的pid同样 t1=Thread(target=work) t2=Thread(target=work) t1.start() t2.start() print('主线程/主进程pid',os.getpid()) #part2:开多个进程,每一个进程都有不一样的pid p1=Process(target=work) p2=Process(target=work) p1.start() p2.start() print('主线程/主进程pid',os.getpid())

1.3 练习mysql

练习一:git

多线程并发的socket服务端

import multiprocessing import threading import socket s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.bind(('127.0.0.1',8080)) s.listen(5) def action(conn): while True: data=conn.recv(1024) print(data) conn.send(data.upper()) if __name__ == '__main__': while True: conn,addr=s.accept() p=threading.Thread(target=action,args=(conn,)) p.start()

客户端

import socket s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if not msg:continue s.send(msg.encode('utf-8')) data=s.recv(1024) print(data)

练习二:三个任务,一个接收用户输入,一个将用户输入的内容格式化成大写,一个将格式化后的结果存入文件github

from threading import Thread msg_l=[] format_l=[] def talk(): while True: msg=input('>>: ').strip() if not msg:continue msg_l.append(msg) def format_msg(): while True: if msg_l: res=msg_l.pop() format_l.append(res.upper()) def save(): while True: if format_l: with open('db.txt','a',encoding='utf-8') as f: res=format_l.pop() f.write('%s\n' %res) if __name__ == '__main__': t1=Thread(target=talk) t2=Thread(target=format_msg) t3=Thread(target=save) t1.start() t2.start() t3.start()

1.4 线程的join与setdaemonweb

与进程的方法都是相似的,实际上是multiprocessing模仿threading的接口redis

join与setdaemonsql

from threading import Thread import time def sayhi(name): time.sleep(2) print('%s say hello' %name) if __name__ == '__main__': t=Thread(target=sayhi,args=('egon',)) t.setDaemon(True) t.start() t.join() print('主线程') print(t.is_alive())

1.5 线程相关的其余方法补充编程

Thread实例对象的方法

  • isAlive(): 返回线程是否活动的。
  • getName(): 返回线程名。
  • setName(): 设置线程名。

threading模块提供的一些方法:

  • threading.currentThread(): 返回当前的线程变量。
  • threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  • threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
from threading import Thread import threading from multiprocessing import Process import os def work(): import time time.sleep(3) print(threading.current_thread().getName()) if __name__ == '__main__': #在主进程下开启线程 t=Thread(target=work) t.start() print(threading.current_thread().getName()) print(threading.current_thread()) #主线程 print(threading.enumerate()) #连同主线程在内有两个运行的线程 print(threading.active_count()) print('主线程/主进程') ''' 打印结果: MainThread <_MainThread(MainThread, started 140735268892672)> [<_MainThread(MainThread, started 140735268892672)>, <Thread(Thread-1, started 123145307557888)>] 2 主线程/主进程 Thread-1 '''

2.2 Python GIL(Global Interpreter Lock)

'''

定义:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)

'''
结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,没法利用多核优点

首先须要明确的一点是GIL并非Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就比如C++是一套语言(语法)标准,可是能够用不一样的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也同样,一样一段代码能够经过CPython,PyPy,Psyco等不一样的Python执行环境来执行。像其中的JPython就没有GIL。然而由于CPython是大部分环境下默认的Python执行环境。因此在不少人的概念里CPython就是Python,也就想固然的把GIL归结为Python语言的缺陷。因此这里要先明确一点:GIL并非Python的特性,Python彻底能够不依赖于GIL

这篇文章透彻的剖析了GIL对python多线程的影响,强烈推荐看一下:http://www.dabeaz.com/python/UnderstandingGIL.pdf

关于GIL与Lock的比较请看2.3小节,此处只需知道:有了GIL的存在,同一时刻统一进程中只有一个线程被执行

听到这里,有的同窗立马质问:进程能够利用多核,可是开销大,而python的多线程开销小,但却没法利用多核优点,也就是说python没用了,php才是最牛逼的语言?

别着急啊,老娘还没讲完呢。

要解决这个问题,咱们须要在几个点上达成一致:

  1. cpu究竟是用来作计算的,仍是用来作I/O的?

  2. 多cpu,意味着能够有多个核并行完成计算,因此多核提高的是计算性能

  3. 每一个cpu一旦遇到I/O阻塞,仍然须要等待,因此多核对I/O操做没什么用处

一个工人至关于cpu,此时计算至关于工人在干活,I/O阻塞至关于为工人干活提供所需原材料的过程,工人干活的过程当中若是没有原材料了,则工人干活的过程须要中止,直到等待原材料的到来。

若是你的工厂干的大多数任务都要有准备原材料的过程(I/O密集型),那么你有再多的工人,意义也不大,还不如一我的,在等材料的过程当中让工人去干别的活,

反过来说,若是你的工厂原材料都齐全,那固然是工人越多,效率越高

结论:

  对计算来讲,cpu越多越好,可是对于I/O来讲,再多的cpu也没用

  固然对于一个程序来讲,不会是纯计算或者纯I/O,咱们只能相对的去看一个程序究竟是计算密集型仍是I/O密集型,从而进一步分析python的多线程有无用武之地

分析:

咱们有四个任务须要处理,处理方式确定是要玩出并发的效果,解决方案能够是:

方案一:开启四个进程

方案二:一个进程下,开启四个线程

单核状况下,分析结果:

  若是四个任务是计算密集型,没有多核来并行计算,方案一徒增了建立进程的开销,方案二胜

  若是四个任务是I/O密集型,方案一建立进程的开销大,且进程的切换速度远不如线程,方案二胜

多核状况下,分析结果:

  若是四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜

  若是四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜

结论:如今的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提高,甚至不如串行(没有大量切换),可是,对于IO密集型的任务效率仍是有显著提高的。

计算密集型

from threading import Thread from multiprocessing import Process import os import time def work(): res=0 for i in range(1000000): res+=i if __name__ == '__main__': t_l=[] start_time=time.time() # for i in range(300): #串行 # work() for i in range(300): t=Thread(target=work) #在个人机器上,4核cpu,多线程大概15秒 # t=Process(target=work) #在个人机器上,4核cpu,多进程大概10秒 t_l.append(t) t.start() for i in t_l: i.join() stop_time=time.time() print('run time is %s' %(stop_time-start_time)) print('主线程')

I/O密集型

from threading import Thread from multiprocessing import Process import time import os def work(): time.sleep(2) #模拟I/O操做,能够打开一个文件来测试I/O,与sleep是一个效果 print(os.getpid()) if __name__ == '__main__': t_l=[] start_time=time.time() for i in range(1000): t=Thread(target=work) #耗时大概为2秒 # t=Process(target=work) #耗时大概为25秒,建立进程的开销远高于线程,并且对于I/O密集型,多cpu根本无论用 t_l.append(t) t.start() for t in t_l: t.join() stop_time=time.time() print('run time is %s' %(stop_time-start_time))

应用:

多线程用于IO密集型,如socket,爬虫,web
多进程用于计算密集型,如金融分析

2.3 同步锁

同进程同样

import time import threading def addNum(): global num #在每一个线程中都获取这个全局变量 #num-=1 temp=num time.sleep(0.1) num =temp-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('Result: ', num)

锁一般被用来实现对共享资源的同步访问。为每个共享资源建立一个Lock对象,当你须要访问该资源时,调用acquire方法来获取锁对象(若是其它线程已经得到了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁:

import threading R=threading.Lock() R.acquire() ''' 对公共数据的操做 ''' R.release()

GIL VS Lock

机智的同窗可能会问到这个问题,就是既然你以前说过了,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为何这里还须要lock?

首先咱们须要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据

而后,咱们能够得出结论:保护不一样的数据就应该加不一样的锁。

最后,问题就很明朗了,GIL与Lock是两把锁,保护的数据不同,前者是解释器级别的(固然保护的就是解释器级别的数据,好比垃圾回收的数据),后者是保护用户本身开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock

详细的:

由于Python解释器帮你自动按期进行内存回收,你能够理解为python解释器里有一个独立的线程,每过一段时间它起wake up作一次全局轮询看看哪些内存数据是能够被清空的,此时你本身的程序里的线程和py解释器本身的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程当中的clearing时刻,可能一个其它线程正好又从新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决相似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题, 这能够说是Python早期版本的遗留问题。 

2.4 死锁与递归锁

进程也有死锁与递归锁,在进程那里忘记说了,放到这里一切说了额

所谓死锁: 是指两个或两个以上的进程或线程在执行过程当中,因争夺资源而形成的一种互相等待的现象,若无外力做用,它们都将没法推动下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,以下就是死锁

from threading import Thread,Lock import time mutexA=Lock() mutexB=Lock() class MyThread(Thread): def run(self): self.func1() self.func2() def func1(self): mutexA.acquire() print('\033[41m%s 拿到A锁\033[0m' %self.name) mutexB.acquire() print('\033[42m%s 拿到B锁\033[0m' %self.name) mutexB.release() mutexA.release() def func2(self): mutexB.acquire() print('\033[43m%s 拿到B锁\033[0m' %self.name) time.sleep(2) mutexA.acquire() print('\033[44m%s 拿到A锁\033[0m' %self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t=MyThread() t.start() ''' Thread-1 拿到A锁 Thread-1 拿到B锁 Thread-1 拿到B锁 Thread-2 拿到A锁 而后就卡住,死锁了 '''

解决方法,递归锁,在Python中为了支持在同一线程中屡次请求同一资源,python提供了可重入锁RLock。

这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源能够被屡次require。直到一个线程全部的acquire都被release,其余的线程才能得到资源。上面的例子若是使用RLock代替Lock,则不会发生死锁:

mutexA=mutexB=threading.RLock() 一个线程拿到锁,counter加1,该线程内又碰到加锁的状况,则counter继续加1,这期间全部其余线程都只能等待,等待该线程释放全部锁,即counter递减到0为止

2.5 信号量Semahpore

同进程的同样

Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其余线程调用release()。

实例:(同时只有5个线程能够得到semaphore,便可以限制最大链接数为5):

import threading import time semaphore = threading.Semaphore(5) def func(): if semaphore.acquire(): print (threading.currentThread().getName() + ' get semaphore') time.sleep(2) semaphore.release() for i in range(20): t1 = threading.Thread(target=func) t1.start()

与进程池是彻底不一样的概念,进程池Pool(4),最大只能产生4个进程,并且从头至尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程

2.6 Event
同进程的同样

线程的一个关键特性是每一个线程都是独立运行且状态不可预测。若是程序中的其余线程须要经过判断某个线程的状态来肯定本身下一步的操做,这时线程同步问题就会变得很是棘手。为了解决这些问题,咱们须要使用threading库中的Event对象。对象包含一个可由线程设置的信号标志,它容许线程等待某事件的发生。在初始状况下,Event对象中的信号标志被设置为假。若是有线程等待一个Event对象,而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程若是将一个Event对象的信号标志设置为真,它将唤醒全部等待这个Event对象的线程。若是一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

event.isSet():返回event的状态值; event.wait():若是 event.isSet()==False将阻塞线程; event.set(): 设置event的状态值为True,全部阻塞池的线程激活进入就绪状态, 等待操做系统调度; event.clear():恢复event的状态值为False。

 

能够考虑一种应用场景(仅仅做为说明),例如,咱们有多个线程从Redis队列中读取数据来处理,这些线程都要尝试去链接Redis的服务,通常状况下,若是Redis链接不成功,在各个线程的代码中,都会去尝试从新链接。若是咱们想要在启动时确保Redis服务正常,才让那些工做线程去链接Redis服务器,那么咱们就能够采用threading.Event机制来协调各个工做线程的链接操做:主线程中会去尝试链接Redis服务,若是正常的话,触发事件,各工做线程会尝试链接Redis服务。

不了解redis能够参考mysql的例子(同样的道理)

import threading import time import logging logging.basicConfig(level=logging.DEBUG, format='(%(threadName)-10s) %(message)s',) def worker(event): logging.debug('Waiting for redis ready...') event.wait() logging.debug('redis ready, and connect to redis server and do some work [%s]', time.ctime()) time.sleep(1) def main(): readis_ready = threading.Event() t1 = threading.Thread(target=worker, args=(readis_ready,), name='t1') t1.start() t2 = threading.Thread(target=worker, args=(readis_ready,), name='t2') t2.start() logging.debug('first of all, check redis server, make sure it is OK, and then trigger the redis ready event') time.sleep(3) # simulate the check progress readis_ready.set() if __name__=="__main__": main()

mysql...

from threading import Thread,Event import threading import time,random def conn_mysql(): print('\033[42m%s 等待链接mysql。。。\033[0m' %threading.current_thread().getName()) event.wait() print('\033[42mMysql初始化成功,%s开始链接。。。\033[0m' %threading.current_thread().getName()) def check_mysql(): print('\033[41m正在检查mysql。。。\033[0m') time.sleep(random.randint(1,3)) event.set() time.sleep(random.randint(1,3)) if __name__ == '__main__': event=Event() t1=Thread(target=conn_mysql) #等待链接mysql t2=Thread(target=conn_mysql) #等待链接myqsl t3=Thread(target=check_mysql) #检查mysql t1.start() t2.start() t3.start()

threading.Event的wait方法还接受一个超时参数,默认状况下若是事件一致没有发生,wait方法会一直阻塞下去,而加入这个超时参数以后,若是阻塞时间超过这个参数设定的值以后,wait方法会返回。对应于上面的应用场景,若是Redis服务器一致没有启动,咱们但愿子线程可以打印一些日志来不断地提醒咱们当前没有一个能够链接的Redis服务,咱们就能够经过设置这个超时参数来达成这样的目的:

def conn_mysql(): count=0 while not e.is_set(): print('%s 第 <%s> 次尝试' %(threading.current_thread().getName(),count)) count+=1 e.wait(0.5) print('%s ready to conn mysql' %threading.current_thread().getName()) time.sleep(1)

修订上述mysql版本

from threading import Thread,Event import threading import time,random def conn_mysql(): while not event.is_set(): print('\033[42m%s 等待链接mysql。。。\033[0m' %threading.current_thread().getName()) event.wait(0.1) print('\033[42mMysql初始化成功,%s开始链接。。。\033[0m' %threading.current_thread().getName()) def check_mysql(): print('\033[41m正在检查mysql。。。\033[0m') time.sleep(random.randint(1,3)) event.set() time.sleep(random.randint(1,3)) if __name__ == '__main__': event=Event() t1=Thread(target=conn_mysql) t2=Thread(target=conn_mysql) t3=Thread(target=check_mysql) t1.start() t2.start() t3.start()

这样,咱们就能够在等待Redis服务启动的同时,看到工做线程里正在等待的状况。

应用:链接池

2.7 条件Condition(了解)

使得线程等待,只有知足某条件时,才释放n个线程

import threading def run(n): con.acquire() con.wait() print("run the thread: %s" %n) con.release() if __name__ == '__main__': con = threading.Condition() for i in range(10): t = threading.Thread(target=run, args=(i,)) t.start() while True: inp = input('>>>') if inp == 'q': break con.acquire() con.notify(int(inp)) con.release()
def condition_func(): ret = False inp = input('>>>') if inp == '1': ret = True return ret def run(n): con.acquire() con.wait_for(condition_func) print("run the thread: %s" %n) con.release() if __name__ == '__main__': con = threading.Condition() for i in range(10): t = threading.Thread(target=run, args=(i,)) t.start()

2.8 定时器

定时器,指定n秒后执行某操做

from threading import Timer def hello(): print("hello, world") t = Timer(1, hello) t.start() # after 1 seconds, "hello, world" will be printed

2.9 线程queue

queue队列 :使用import queue,用法与进程Queue同样

queue is especially useful in threaded programming when information must be exchanged safely between multiple threads.

class queue.Queue(maxsize=0) #先进先出

import queue q=queue.Queue() q.put('first') q.put('second') q.put('third') print(q.get()) print(q.get()) print(q.get()) ''' 结果(先进先出): first second third '''

class queue.LifoQueue(maxsize=0) #last in fisrt out

import queue q=queue.LifoQueue() q.put('first') q.put('second') q.put('third') print(q.get()) print(q.get()) print(q.get()) ''' 结果(后进先出): third second first '''

class queue.PriorityQueue(maxsize=0) #存储数据时可设置优先级的队列

import queue q=queue.PriorityQueue() #put进入一个元组,元组的第一个元素是优先级(一般是数字,也能够是非数字之间的比较),数字越小优先级越高 q.put((20,'a')) q.put((10,'b')) q.put((30,'c')) print(q.get()) print(q.get()) print(q.get()) ''' 结果(数字越小优先级越高,优先级高的优先出队): (10, 'b') (20, 'a') (30, '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被消费完毕

2.10 Python标准模块--concurrent.futures

https://docs.python.org/dev/library/concurrent.futures.html

三 补充:paramiko模块

  1. 介绍:

paramiko是一个用于作远程控制的模块,使用该模块能够对远程服务器进行命令或文件操做,值得一说的是,fabric和ansible内部的远程管理就是使用的paramiko来现实。

  1. 下载安装
    pycrypto,因为 paramiko 模块内部依赖pycrypto,因此先下载安装pycrypto
    pip3 install pycrypto
    pip3 install paramiko
    注:若是在安装pycrypto2.0.1时发生以下错误
    command 'gcc' failed with exit status 1...
    多是缺乏python-dev安装包致使
    若是gcc没有安装,请事先安装gcc

  2. 使用

SSHClient
用于链接远程服务器并执行基本命令

基于用户名密码链接:

import paramiko
  
# 建立SSH对象 ssh = paramiko.SSHClient() # 容许链接不在know_hosts文件中的主机 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # 链接服务器 ssh.connect(hostname='c1.salt.com', port=22, username='wupeiqi', password='123') # 执行命令 stdin, stdout, stderr = ssh.exec_command('df') # 获取命令结果 result = stdout.read() # 关闭链接 ssh.close()

四 协程

协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序本身控制调度的。

须要强调的是:

  1. python的线程属于内核级别的,即由操做系统控制调度(如单线程一旦遇到io就被迫交出cpu执行权限,切换其余线程运行)

  2. 单线程内开启协程,一旦遇到io,从应用程序级别(而非操做系统)控制切换

对比操做系统控制线程的切换,用户在单线程内控制协程的切换,优势以下:

  1. 协程的切换开销更小,属于程序级别的切换,操做系统彻底感知不到,于是更加轻量级

  2. 单线程内就能够实现并发的效果,最大限度地利用cpu

要实现协程,关键在于用户程序本身控制程序切换,切换以前必须由用户程序本身保存协程上一次调用时的状态,如此,每次从新调用时,可以从上次的位置继续执行

(详细的:协程拥有本身的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其余地方,在切回来的时候,恢复先前保存的寄存器上下文和栈)

为此,咱们以前已经学习过一种在单线程下能够保存程序运行状态的方法,即yield,咱们来简单复习一下:

1.yiled能够保存状态,yield的状态保存与操做系统的保存线程状态很像,可是yield是代码级别控制的,更轻量级
2.send能够把一个函数的结果传给另一个函数,以此实现单线程内程序之间的切换

#不用yield:每次函数调用,都须要重复开辟内存空间,即重复建立名称空间,于是开销很大 import time def consumer(item): # print('拿到包子%s' %item) x=11111111111 x1=12111111111 x3=13111111111 x4=14111111111 y=22222222222 z=33333333333 pass def producer(target,seq): for item in seq: target(item) #每次调用函数,会临时产生名称空间,调用结束则释放,循环100000000次,则重复这么屡次的建立和释放,开销很是大 start_time=time.time() producer(consumer,range(100000000)) stop_time=time.time() print('run time is:%s' %(stop_time-start_time)) #30.132838010787964 #使用yield:无需重复开辟内存空间,即重复建立名称空间,于是开销小 import time def init(func): def wrapper(*args,**kwargs): g=func(*args,**kwargs) next(g) return g return wrapper @init def consumer(): x=11111111111 x1=12111111111 x3=13111111111 x4=14111111111 y=22222222222 z=33333333333 while True: item=yield # print('拿到包子%s' %item) pass def producer(target,seq): for item in seq: target.send(item) #无需从新建立名称空间,从上一次暂停的位置继续,相比上例,开销小 start_time=time.time() producer(consumer(),range(100000000)) stop_time=time.time() print('run time is:%s' %(stop_time-start_time)) #21.882073879241943

缺点:

协程的本质是单线程下,没法利用多核,能够是一个程序开启多个进程,每一个进程内开启多个线程,每一个线程内开启协程

协程指的是单个线程,于是一旦协程出现阻塞,将会阻塞整个线程

协程的定义(知足1,2,3就可称为协程):

1.必须在只有一个单线程里实现并发
2.修改共享数据不需加锁
3.用户程序里本身保存多个控制流的上下文栈
4.附加:一个协程遇到IO操做自动切换到其它协程(如何实现检测IO,yield、greenlet都没法实现,就用到了gevent模块(select机制))
yield切换在没有io的状况下或者没有重复开辟内存空间的操做,对效率没有什么提高,甚至更慢,为此,能够用greenlet来为你们演示这种切换

五 Greenlet

greenlet是一个用C实现的协程模块,相比与python自带的yield,它可使你在任意函数之间随意切换,而不需把这个函数先声明为generator

from greenlet import greenlet def test1(): print('test1,first') gr2.switch() print('test1,sencod') gr2.switch() def test2(): print('test2,first') gr1.switch() print('test2,sencod') gr1=greenlet(test1) gr2=greenlet(test2) gr1.switch()

能够在第一次switch时传入参数

import time from greenlet import greenlet def eat(name): print('%s eat food 1' %name) gr2.switch('alex飞飞飞') print('%s eat food 2' %name) gr2.switch() def play_phone(name): print('%s play 1' %name) gr1.switch() print('%s play 2' %name) gr1=greenlet(eat) gr2=greenlet(play_phone) gr1.switch(name='egon啦啦啦')#能够在第一次switch时传入参数,之后都不须要

单纯的切换(在没有io的状况下或者没有重复开辟内存空间的操做),反而会下降程序的执行速度

#顺序执行 import time def f1(): res=0 for i in range(10000000): res+=i def f2(): res=0 for i in range(10000000): res*=i start_time=time.time() f1() f2() stop_time=time.time() print('run time is: %s' %(stop_time-start_time)) #1.7395639419555664 #切换 from greenlet import greenlet import time def f1(): res=0 for i in range(10000000): res+=i gr2.switch() def f2(): res=0 for i in range(10000000): res*=i gr1.switch() gr1=greenlet(f1) gr2=greenlet(f2) start_time=time.time() gr1.switch() stop_time=time.time() print('run time is: %s' %(stop_time-start_time)) #7.789067983627319

greenlet只是提供了一种比generator更加便捷的切换方式,仍然是没有解决遇到IO自动切换的问题

六 Gevent

Gevent 是一个第三方库,能够轻松经过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet所有运行在主程序操做系统进程的内部,但它们被协做式地调度。

g1=gevent.spawn()建立一个协程对象g1,

spawn括号内第一个参数是函数名,如eat,后面能够有多个参数,能够是位置实参或关键字实参,都是传给函数eat的

遇到IO阻塞时会自动切换任务

import gevent import time def eat(): print('eat food 1') gevent.sleep(2) #等饭来 print('eat food 2') def play_phone(): print('play phone 1') gevent.sleep(1) #网卡了 print('play phone 2') # gevent.spawn(eat) # gevent.spawn(play_phone) # print('主') # 直接结束 #于是也须要join方法,进程或现场的jion方法只能join一个,而gevent的join方法能够join多个 g1=gevent.spawn(eat) g2=gevent.spawn(play_phone) gevent.joinall([g1,g2]) print('主')

上例gevent.sleep(2)模拟的是gevent能够识别的io阻塞,

而time.sleep(2)或其余的阻塞,gevent是不能直接识别的须要用下面一行代码,打补丁,就能够识别了

from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块以前

或者咱们干脆记忆成:要用gevent,须要将from gevent import monkey;monkey.patch_all()放到文件的开头

from gevent import monkey;monkey.patch_all() import gevent import time def eat(): print('eat food 1') time.sleep(2) print('eat food 2') def play_phone(): print('play phone 1') time.sleep(1) print('play phone 2') g1=gevent.spawn(eat) g2=gevent.spawn(play_phone) gevent.joinall([g1,g2]) print('主')

同步与异步

import gevent def task(pid): """ Some non-deterministic task """ gevent.sleep(0.5) print('Task %s done' % pid) def synchronous(): for i in range(1,10): task(i) def asynchronous(): threads = [gevent.spawn(task, i) for i in range(10)] gevent.joinall(threads) print('Synchronous:') synchronous() print('Asynchronous:') asynchronous()

上面程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn。 初始化的greenlet列表存放在数组threads中,此数组被传给gevent.joinall 函数,后者阻塞当前流程,并执行全部给定的greenlet。执行流程只会在 全部greenlet执行完后才会继续向下走。

 

gevent线程的一些用法
g1=gevent.spawn(func,1,,2,3,x=4,y=5)

g2=gevent.spawn(func2)

g1.join() #等待g1结束

g2.join() #等待g2结束

或者上述两步合做一步:gevent.joinall([g1,g2])

g1.value #拿到func1的返回值

 

协程应用:爬虫

from gevent import monkey;monkey.patch_all() import gevent import requests import time def get_page(url): print('GET: %s' %url) response=requests.get(url) if response.status_code == 200: print('%d bytes received from %s' %(len(response.text),url)) start_time=time.time() gevent.joinall([ gevent.spawn(get_page,'https://www.python.org/'), gevent.spawn(get_page,'https://www.yahoo.com/'), gevent.spawn(get_page,'https://github.com/'), ]) stop_time=time.time() print('run time is %s' %(stop_time-start_time))

经过gevent实现单线程下的socket并发(from gevent import monkey;monkey.patch_all()必定要放到导入socket模块以前,不然gevent没法识别socket的阻塞)

服务端

from gevent import monkey;monkey.patch_all() from socket import * import gevent #若是不想用money.patch_all()打补丁,能够用gevent自带的socket # from gevent import socket # s=socket.socket() def server(server_ip,port): s=socket(AF_INET,SOCK_STREAM) s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) s.bind((server_ip,port)) s.listen(5) while True: conn,addr=s.accept() gevent.spawn(talk,conn,addr) def talk(conn,addr): try: while True: res=conn.recv(1024) print('client %s:%s msg: %s' %(addr[0],addr[1],res)) conn.send(res.upper()) except Exception as e: print(e) finally: conn.close() if __name__ == '__main__': server('127.0.0.1',8080) 服务端from gevent import monkey;monkey.patch_all() from socket import * import gevent #若是不想用money.patch_all()打补丁,能够用gevent自带的socket # from gevent import socket # s=socket.socket() def server(server_ip,port): s=socket(AF_INET,SOCK_STREAM) s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) s.bind((server_ip,port)) s.listen(5) while True: conn,addr=s.accept() gevent.spawn(talk,conn,addr) def talk(conn,addr): try: while True: res=conn.recv(1024) print('client %s:%s msg: %s' %(addr[0],addr[1],res)) conn.send(res.upper()) except Exception as e: print(e) finally: conn.close() if __name__ == '__main__': server('127.0.0.1',8080)

客户端

from socket import * client=socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if not msg:continue client.send(msg.encode('utf-8')) msg=client.recv(1024) print(msg.decode('utf-8'))

多线程并发多个客户端

from threading import Thread from socket import * import threading def client(server_ip,port): c=socket(AF_INET,SOCK_STREAM) c.connect((server_ip,port)) count=0 while True: c.send(('%s say hello %s' %(threading.current_thread().getName(),count)).encode('utf-8')) msg=c.recv(1024) print(msg.decode('utf-8')) count+=1 if __name__ == '__main__': for i in range(500): t=Thread(target=client,args=('127.0.0.1',8080)) t.start()
相关文章
相关标签/搜索