python advanced programming (Ⅲ)

IO编程


IO在计算机中指Input/Output。由于程序和运行时数据是在内存中驻留,由CPU来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。

IO编程中,Stream(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。即Output又Input就需要两根水管。

  • 打开浏览器访问新浪首页,浏览器这个程序就需要通过网络IO获取新浪的网页。浏览器首先会发送数据给新浪服务器,告诉它我想要首页的HTML,这个动作是往外发数据,叫Output。随后新浪服务器把网页发过来,这个动作是从外面接收数据,叫Input。所以,通常,程序完成IO操作会有Input和Output两个数据流。也有只用一个的情况,比如,从磁盘读取文件到内存,就只有Input操作,反过来,把数据写到磁盘文件里,就只是一个Output操作。

由于CPU和内存的速度远远高于外设的速度,所以,在IO编程中,就存在速度严重不匹配的问题。比如要把100M的数据写入磁盘,CPU输出100M的数据只需要0.01秒,可是磁盘要接收这100M数据可能需要10秒,有两种办法解决:

  1. CPU等着,也就是程序暂停执行后续代码,等100M的数据在10秒后写入磁盘,再接着往下执行,这种模式称为同步IO
  2. CPU不等待,只是告诉磁盘,慢慢写不着急,写完通知我,我接着干别的事去了,于是后续代码可以接着执行,这种模式称为异步IO

很明显,使用异步IO来编写程序性能会远远高于同步IO,但是异步IO的缺点是编程模型复杂。

  • 想想看,你得知道什么时候通知你“汉堡做好了”,而通知你的方法也各不相同。如果是服务员跑过来找到你,这是回调模式,如果服务员发短信通知你,你就得不停地检查手机,这是轮询模式。

操作IO的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。


读写文件是最常见的IO操作。Python内置了读写文件的函数,用法和C是兼容的。

  • 在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。

读文件

1  f = open('/Users/michael/test.txt', 'r')    #第一步打开文件,传入文件路径名和标示符
2  f.read()    #第二步若文件打开成功,一次读取文件的全部内容到内存,用一个str对象表示
3  f.close()    #最后一步关闭文件,停止资源占用
4 
5 #为了防止产生IOError后导致f.close()失败资源占用,用以下代码自动关闭以保证
6 with open('/path/to/file', 'r') as f:
7     print(f.read())

read(size):每次最多读取size个字节的内容,不能确定文件大小时反复调用,以防一次读取过量内存爆炸

readlines():每次读取一行内容返回list

默认都是读取UTF-8文本文件。

读取二进制文件比如图片、视频等,用'rb'标示符模式打开即可。

读取非UTF-8编码的文本文件,需要给open()函数传入encoding参数,例如读取GBK编码的文件。

1  f = open('/Users/michael/gbk.txt', 'r', encoding='gbk')

open()函数还接收一个errors参数,表示如果遇到非法编码错误后直接忽略:errors='ignore'

写文件

写文件和读文件一样,唯一区别是调用open()函数时传入标识符'w'或者'wb'表示写文本文件或写二进制文件。

反复调用write()来写入文件,这也必须加上面的with语句。

写文件也支持encoding。


数据读写不一定是文件,也可以在内存中读写,StringIO顾名思义就是在内存中读写str。

要把str写入StringIO,需要先创建一个StringIO,然后,像文件一样写入即可:

1 from io import StringIO
2 f = StringIO()
3 >>>f.write('hello')
4 5                                      #写入了5个字符
5 >>> print(f.getvalue())    #getvalue()方法用于获得写入后的str。
6 hello world!

 读取StringIO的一种方法:

 1 >>> from io import StringIO
 2 >>> f = StringIO('Hello!
Hi!
Goodbye!')   #初始化StringIO
 3 >>> while True:
 4 ...     s = f.readline()
 5 ...     if s == '':
 6 ...         break
 7 ...     print(s.strip())
 8 ...
 9 Hello!
10 Hi!
11 Goodbye!

使用BytesIO可以操作二进制数据在内存中读写bytes。

创建一个BytesIO,然后写入一些bytes:

>>> from io import BytesIO
>>> f = BytesIO()
>>> f.write('中文'.encode('utf-8'))    #写入的不是str而是经过UTF-8编码的bytes
6
>>> print(f.getvalue())
b'xe4xb8xadxe6x96x87'

读取与StringIO类似


在Python程序中的操作文件和目录,内置的os模块可以直接调用操作系统提供的接口函数(就像操作系统命令行调用操作系统提供的接口函数)。

>>> import os
>>> os.name    # 操作系统类型
'posix'               #如果是posix说明是Linux、Unix或Mac OS X,如果是nt是Windows
>>> os.uname()    #获取详细的系统信息(windows中没有)
    ... ...

>>> os.environ    #查看环境变量
    ... ...

>>> os.environ.get('PATH')    #获取某个环境变量的值(这里举例PATH)
    ... ...

#以下是查看、创建和删除目录
>>> os.path.abspath('.')                  #查看当前目录的绝对路径
'/Users/michael'
# 在某个目录下创建一个新目录前把完整的路径表示出来
>>> os.path.join('/Users/michael', 'testdir') 
'/Users/michael/testdir'                          
>>> os.mkdir('/Users/michael/testdir')    # 然后创建一个目录
>>> os.rmdir('/Users/michael/testdir')    # 删掉一个目录

os.path.join():两个路径合成一个

os.path.split():拆分路径

os.path.splitext():获得文件扩展名

Python操作文件和目录的函数一部分放在os模块中,一部分放在os.path模块中,但是没有复制文件的函数,但在补充模块shutil(里面包含很多实用函数)中提供了copyfile()的函数。

过滤文件(例子):

1 >>> [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py']
2 ['apis.py', 'config.py', 'models.py', 'pymonitor.py', 'test_db.py', 'urls.py', 'wsgiapp.py']

在程序运行的过程中所有的变量都是在内存中,动态语言可以随时修改变量,但是一旦程序结束变量所占用的内存就被操作系统全部回收,如果修改后的变量没有存储到磁盘上,下次运行程序变量值不会改变。变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中有别的叫法。序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling

Python提供了pickle模块来实现序列化:

 1 #把一个对象序列化并写入文件
 2 import pickle           
 3 d = dict(name='Bob', age=20, score=88)
 4 pickle.dumps(d)      #pickle.dumps()方法把任意对象序列化成一个bytes,便于写入文件  
 5                                
 6 f = open('dump.txt', 'wb')
 7 pickle.dump(d, f)
 8 f.close()
 9 
10 #把对象从磁盘读到内存
11 f = open('dump.txt', 'rb')
12 d = pickle.load(f)    #pickle.loads()反序列化出对象
13 f.close()
14 
15 d
16 {'age': 20, 'score': 88, 'name': 'Bob'}

Pickle只能用于Python并且不同版本不兼容。如果要在不同的编程语言之间传递对象,就必须把对象序列化为标准格式,比如XML,但更好的方法是序列化为JSON,因为JSON表示出来就是一个字符串,可以被所有语言读取,也可以方便地存储到磁盘或者通过网络传输。而且比XML更快,而且可以直接在Web页面中读取。

JSON表示的对象就是标准的JavaScript语言的对象,JSON和Python内置的数据类型对应如下:

Python内置的json模块提供了非常完善的Python对象到JSON格式的转换:

 1 #把python对象变成一个JSON
 2 import json
 3 d = dict(name='Bob', age=20, score=88)
 4 json.dumps(d)
 5 '{"age": 20, "score": 88, "name": "Bob"}'
 6 
 7 #把JSON反序列化为Python对象
 8 json_str = '{"age": 20, "score": 88, "name": "Bob"}'
 9 json.loads(json_str)
10 {'age': 20, 'score': 88, 'name': 'Bob'}

将class序列化必须先把class对象转化为dict,然后python才能把dict转化为JSON,dumps()方法的default参数可以把任意一个对象变成一个可序列为JSON的对像。

 1 import json
 2 
 3 class Student(object):                               #建立一个类
 4     def __init__(self, name, age, score):
 5         self.name = name
 6         self.age = age
 7         self.score = score
 8 
 9 def student2dict(stdent):        #为Student专门写一个转换函数
10     return {
11         'name': std.name,
12         'age': std.age,
13         'score': std.score
14     }
15 
16 >>> print(json.dumps(s, default=student2dict))   #将转换函数传进去
17 {"age": 20, "name": "Bob", "score": 88}
18 
19 #这样,Student实例首先被student2dict()函数转换成dict,然后再被顺利序列化为JSON

同样的道理,如果我们要把JSON反序列化为一个Student对象实例,loads()方法首先转换出一个dict对象,然后,我们传入的object_hook函数负责把dict转换为Student实例

1 def dict2student(d):
2     return Student(d['name'], d['age'], d['score'])
3 
4 >>> json_str = '{"age": 20, "score": 88, "name": "Bob"}'
5 >>> print(json.loads(json_str, object_hook=dict2student))

进程和线程


 现代操作系统比如Mac OS X,UNIX,Linux,Windows等,都是支持“多任务”的操作系统。多任务简单地说,就是操作系统可以同时运行多个任务(程序软件)。多核CPU已经非常普及了,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。真正的并行执行多任务只能在多核CPU上实现,但是由于任务数量远远多于CPU的核心数量,所以操作系统也会自动把很多任务轮流调度到每个核心上执行。

    • 一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
    • 一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程
    • 有些进程在同一时间不止干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。这些一个进程内的这些“子任务”称为线程(Thread)。

多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。

多任务的实现有3种方式:

  • 多进程模式;
  • 一个进程多线程模式;
  • 多进程+多线程模式(复杂实际采用少)

同时执行多个任务通常各个任务之间需要相互通信和协调,有时任务1必须暂停等待任务2完成后才能继续执行,有时任务3和任务4又不能同时执行,所以多进程和多线程的程序的复杂度要远远高于前面所写单进程单线程的程序。


实现多进程

Unix/Linux操作系统提供了一个fork()系统调用,普通的函数调用,调用一次返回一次,但是fork()调用一次返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后分别在父进程和子进程内返回。子进程永远返回0,父进程返回子进程的ID。这样做的理由是:一个父进程可以fork出很多子进程,所以父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。

 1 #os模块封装了常见的系统调用,包括fork,可以在Python程序中轻松创建子进程:
 2 import os
 3 print('Process (%s) start...' % os.getpid())
 4 pid = os.fork()    #getpid()获取进程识别码
 5 if pid == 0:
 6     print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
 7 else:
 8     print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
 9 
10 #运行
11 Process (876) start...
12 I (876) just created a child process (877).
13 I am child process (877) and my parent is 876.

有了fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。

由于Python是跨平台的,Windows没有fork调用,而multiprocessing模块就是跨平台版本的多进程模块。multiprocessing模块提供了一个Process类来代表一个进程对象。

 1 #演示了启动一个子进程并等待其结束
 2 from multiprocessing import Process
 3 import os
 4 # 子进程要执行的代码
 5 def run_proc(name):
 6     print('Run child process %s (%s)...' % (name, os.getpid()))
 7 
 8 if __name__=='__main__':
 9     print('Parent process %s.' % os.getpid())
10     p = Process(target=run_proc, args=('test',))
11     print('Child process will start.')
12     p.start()
13     p.join()
14     print('Child process end.')
1  #运行
2  Parent process 928.
3  Process will start.
4  Run child process test (929)...
5  Process end.
View Code

创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。

如果要启动大量的子进程,可以用Pool(进程池)的方式批量创建子进程:

 1 from multiprocessing import Pool
 2 import os, time, random
 3 
 4 def long_time_task(name):
 5     print('Run task %s (%s)...' % (name, os.getpid()))
 6     start = time.time()    #进程开始时间
 7     time.sleep(random.random() * 3)
 8     end = time.time()    #进程结束时间
 9     print('Task %s runs %0.2f seconds.' % (name, (end - start)))
10 
11 if __name__=='__main__':
12     print('Parent process %s.' % os.getpid())
13     p = Pool(4)    #进程池参数设4最多同时执行4个进程
14     for i in range(5):    #所以这执行5个进程,进程4就要等0123执行完了才执行
15         p.apply_async(long_time_task, args=(i,))
16     print('Waiting for all subprocesses done...')
17     p.close()
18     p.join()    #join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。
19     print('All subprocesses done.')    
 1 Parent process 669.
 2 Waiting for all subprocesses done...
 3 Run task 0 (671)...
 4 Run task 1 (672)...
 5 Run task 2 (673)...
 6 Run task 3 (674)...
 7 Task 2 runs 0.14 seconds.
 8 Run task 4 (673)...
 9 Task 1 runs 0.27 seconds.
10 Task 3 runs 0.86 seconds.
11 Task 0 runs 1.41 seconds.
12 Task 4 runs 1.91 seconds.
13 All subprocesses done.
View Code

很多时候子进程并不是自身,而是一个外部进程。创建了子进程后,还需要控制子进程的输入和输出。subprocess模块可以非常方便地实现以上两点。(相当于输入命令行后继续输入子命令)

进程间通信是通过multiprocessing模块中的QueuePipes等实现的。

 1 #以Queue为例在父进程中创建两个子进程,一个往q里写数据,一个从q里读数据
 2 from multiprocessing import Process, Queue
 3 import os, time, random
 4 def write(q):    # 写数据进程执行的代码:
 5     print('Process to write: %s' % os.getpid())
 6     for value in ['A', 'B', 'C']:
 7         print('Put %s to queue...' % value)
 8         q.put(value)
 9         time.sleep(random.random())
10 def read(q):    # 读数据进程执行的代码:
11     print('Process to read: %s' % os.getpid())
12     while True:
13         value = q.get(True)
14         print('Get %s from queue.' % value)
15 
16 if __name__=='__main__':
17     q = Queue()    # 父进程创建Queue,并传给各个子进程:
18     pw = Process(target=write, args=(q,))
19     pr = Process(target=read, args=(q,))
20     pw.start()     # 启动子进程pw,写入:
21     pr.start()     # 启动子进程pr,读取:
22     pw.join()      # 等待pw结束:
23     pr.terminate()    # pr进程里是死循环,无法等待其结束,只能强行终止:
1 #运行
2 Process to write: 50563
3 Put A to queue...
4 Process to read: 50564
5 Get A from queue.
6 Put B to queue...
7 Get B from queue.
8 Put C to queue...
9 Get C from queue.
View Code

注:

父进程所有Python对象都必须通过pickle序列化再传到子进程去,所有,如果multiprocessing在Windows下调用失败了,要先考虑是不是pickle失败了。


多进程实现

多任务可以由多进程完成,也可以由一个进程内的多线程完成。

Python的标准库提供了两个模块:常用的threading模块,和封装在里面的_thread模块。

启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:

1 import time, threading
3 t = threading.Thread(target=loop, name='LoopThread'  #loop为要执行的函数,用模块中方法在进程名下创建线程实例并命名为Loo                                                          pThread

4 t.start() 5 t.join() 6 print('thread %s ended.' % threading.current_thread().name)

任何一个进程默认会启动一个线程,称为主线程,主线程又可以启动新的线程,主线程实例的名字叫MainThread,子线程的名字在创建时指定,如果不起名字Python就自动给线程命名为Thread-1Thread-2……threading模块有个current_thread()函数,它永远返回当前线程的实例。

多进程和多线程最大的不同在于,多进程中,同一个变量各自有一份拷贝存在于每个进程中互不影响,而多线程中,所有变量都由所有线程共享,所以任何一个变量都可以被任何一个线程修改,因此线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。解决的办法就是通过threading.Lock()创建一个,当多个线程同时执行时,只有一个线程能成功地获取锁,其他线程就继续等待直到获得锁为止。

1 balance = 0
2 lock = threading.Lock()
3 def run_thread(n):
4     for i in range(100000):
5         lock.acquire()              # 先要获取锁
6         try:
7             change_it(n)            # 放心地改吧
8         finally:
9             lock.release()          # 改完了一定要释放锁

多核CPU上执行python的多个死循环线程代码,Windows的Task Manager可以监控某个进程的CPU使用率,会发现CPU使用率只有100%,而不像C++或Java的核数×100%,这是由于Python解释器设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。(可以重写个不带GIL的解释器)

在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见不会影响其他线程,而全局变量的修改必须加锁,但是局部变量在函数调用挺麻烦。ThreadLocal应运而生,ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。

 1 import threading
 2 local_school = threading.local()    # 创建全局ThreadLocal对象
 3 
 4 def process_student():
 5     std = local_school.student       # 获取当前线程关联的student
 6     print('Hello, %s (in %s)' % (std, threading.current_thread().name))
 7 
 8 def process_thread(name):
 9     local_school.student = name    # 绑定ThreadLocal的student:
10     process_student()
11 
12 t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
13 t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
14 t1.start()
15 t2.start()
16 t1.join()
17 t2.join()
18 
19 #全局变量local_school就是一个ThreadLocal对象,每个线程对它都可以读写student属性互不影响。可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。也可以理解为全局变量local_school是一个字典,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。

进程VS.线程 首先要实现多任务,通常设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。

多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(除非主进程挂了)。多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的。

多线程模式通常比多进程快一点点,但是多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。

多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。

是否采用多任务的第二个考虑是任务的类型。计算密集型任务的特点是要进行大量的计算,全靠CPU的运算能力。计算密集型任务同时进行的数量应当等于CPU的核心数。计算密集型任务对代码运行效率要求高,最好用C语言编写。第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,99%时间都在等待IO操作完成(IO的速度远远低于CPU和内存的速度)。最好用代码量最少的脚本语言编写。

在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上(分布式进程)。Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信(已封装,不必了解)。

Exp如果已经有一个通过Queue通信的多进程程序在同一台机器上运行,现在由于处理任务的进程任务繁重,希望把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现?

原有的Queue可以继续使用,但是,通过managers模块把Queue通过网络暴露出去,就可以让其他机器的进程访问Queue了。

我们先看服务进程,服务进程负责启动Queue,把Queue注册到网络上,然后往Queue里面写入任务:

 1 import random, time, queue                              #task_worker.py
 2 from multiprocessing.managers import BaseManager
 3 
 4 # 发送任务的队列:
 5 task_queue = queue.Queue()
 6 # 接收结果的队列:
 7 result_queue = queue.Queue()
 8 
 9 # 从BaseManager继承的QueueManager:
10 class QueueManager(BaseManager):
11     pass
12 
13 # 把两个Queue都注册到网络上, callable参数关联了Queue对象:
14 QueueManager.register('get_task_queue', callable=lambda: task_queue)
15 QueueManager.register('get_result_queue', callable=lambda: result_queue)
16 
17 # 绑定端口5000, 设置验证码'abc':
18 manager = QueueManager(address=('', 5000), authkey=b'abc')
19 # 启动Queue:
20 manager.start()
21 # 获得通过网络访问的Queue对象:
22 task = manager.get_task_queue()    #在分布式多进程环境下,必须通过manager.get_task_queue()获得的Queue接口添加,不能将创建的
23 result = manager.get_result_queue()    #Queue拿来直接使用。
24 # 放几个任务进去:
25 for i in range(10):
26     n = random.randint(0, 10000)
27     print('Put task %d...' % n)
28     task.put(n)
29 # 从result队列读取结果:
30 print('Try get results...')
31 for i in range(10):
32     r = result.get(timeout=10)
33     print('Result: %s' % r)
34 # 关闭:
35 manager.shutdown()
36 print('master exit.')
然后,在另一台机器上启动任务进程(本机上启动也可以):
#task_master.py
import
time, sys, queue from multiprocessing.managers import BaseManager # 创建类似的QueueManager: class QueueManager(BaseManager): pass # 由于这个QueueManager只从网络上获取Queue,所以注册时只提供名字: QueueManager.register('get_task_queue') QueueManager.register('get_result_queue') # 连接到服务器,也就是运行task_master.py的机器: server_addr = '127.0.0.1' print('Connect to server %s...' % server_addr) # 端口和验证码注意保持与task_master.py设置的完全一致: m = QueueManager(address=(server_addr, 5000), authkey=b'abc') # 从网络连接: m.connect() # 获取Queue的对象: task = m.get_task_queue() result = m.get_result_queue() # 从task队列取任务,并把结果写入result队列: for i in range(10): try: n = task.get(timeout=1) print('run task %d * %d...' % (n, n)) r = '%d * %d = %d' % (n, n, n*n) time.sleep(1) result.put(r) except Queue.Empty: print('task queue is empty.') # 处理结束: print('worker exit.')

#任务进程要通过网络连接到服务进程,所以要指定服务进程的IP。

这个简单的Master/Worker模型有什么用?其实这就是一个简单但真正的分布式计算,把代码稍加改造,启动多个worker,就可以把任务分布到几台甚至几十台机器上,比如把计算n*n的代码换成发送邮件,就实现了邮件队列的异步发送。

Queue对象存储在task_master.py进程中,而Queue之所以能通过网络访问,就是通过QueueManager实现的。由于QueueManager管理的不止一个Queue,所以,要给每个Queue的网络调用接口起个名字,比如get_task_queue。如果task_worker.pyauthkeytask_master.pyauthkey不一致,肯定连接不上。防止其他机器恶意干扰。

注:Queue的作用是用来传递任务和接收结果,每个任务的描述数据量要尽量小。比如发送一个处理日志文件的任务,就不要发送几百兆的日志文件本身,而是发送日志文件存放的完整路径,由Worker进程再去共享的磁盘上读取文件。


异步IO


关于CPU高速执行能力和IO设备的龟速严重不匹配的问题,多线程和多进程只是解决这一问题的一种方法。

另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果然后就去执行其他代码了。一段时间后当IO返回结果时,再通知CPU进行处理。

异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程:

1 loop = get_event_loop()
2 while True:
3     event = loop.get_event()
4     process_event(event)

在异步IO模型下,一个线程就可以同时处理多个IO请求,并且不用切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。

协程,又称微线程,特点是一个线程执行。协程看上去也是子程序(函数),但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候(可以又中断)再返回来接着执行(类似CPU的中断)。协程执行效率极高。因为子程序切换不是线程切换,没有线程切换开销,也不需要多线程的锁机制。

因为协程是一个线程执行,如果要利用多核CPU。最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

Python对协程的支持是通过generator实现的。generator中yield不但可以在生成器中返回一个值,它还可以接收调用者发出的参数。

EXP传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。

如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

 1 def consumer():
 2     r = ''
 3     while True:
 4         n = yield r    #第三步consumer通过yield拿到消息,处理,又通过yield把结果传回
 5         if not n:
 6             return
 7         print('[CONSUMER] Consuming %s...' % n)
 8         r = '200 OK'
 9 
10 def produce(c):
11     c.send(None)    #C就是consumer,第一步首先调用c.send(None)启动生成器
12     n = 0
13     while n < 5:
14         n = n + 1    #第四步produce拿到consumer处理的结果,继续生产下一条消息
15         print('[PRODUCER] Producing %s...' % n)
16         r = c.send(n)    #第二步通过c.send(n)切换到consumer执行
17         print('[PRODUCER] Consumer return: %s' % r)
18     c.close()    #produce决定不生产了,通过c.close()关闭consumer,整个过程结束
19 
20 c = consumer()               #注意到consumer函数是一个generator,把一个consumer传入produce21 produce(c)
#执行结果
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
View Code

整个流程无锁,由一个线程执行,produceconsumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务

                                                                     套用Donald Knuth的一句话总结协程的特点

                                                                                “子程序就是协程的一种特例。”

asyncio是内置了对异步IO支持的标准库(模块),用其提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。

 1 import asyncio
 2 @asyncio.coroutine    #把一个generator标记为coroutine类型
 3 def hello():
 4     print("Hello world!")
 5     r = yield from asyncio.sleep(1)    # 异步调用asyncio.sleep(1):
 6     print("Hello again!")
 7 
 8 loop = asyncio.get_event_loop()    # 获取EventLoop
 9 loop.run_until_complete(hello())    #coroutine扔到EventLoop中执行
10 loop.close()

hello()会首先打印出Hello world!,然后,yield from语法可以让我们方便地调用另一个generator——asyncio.sleep()也是一个coroutine,所以线程不会等待,而是直接中断并去执行EventLoop中其他可以执行的coroutine了。当asyncio.sleep()返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。

多个coroutine可以封装成一组Task然后并发执行(异步IO并发)。用Task封装两个coroutine试试:

 1 import threading asyncio
 2 ... ...
 3 loop = asyncio.get_event_loop()
 4 tasks = [hello(), hello()]
 5 loop.run_until_complete(asyncio.wait(tasks))
 6 loop.close()
 7 
 8 #执行过程:
 9 Hello world! (<_MainThread(MainThread, started 140735195337472)>)
10 Hello world! (<_MainThread(MainThread, started 140735195337472)>)
11 (暂停约1秒)
12 Hello again! (<_MainThread(MainThread, started 140735195337472)>)
13 Hello again! (<_MainThread(MainThread, started 140735195337472)>)

由打印的当前线程名称可以看出,两个coroutine是由同一个线程并发执行的。可以把asyncio.sleep()换成真正的IO操作(读写),就实现了异步IO操作。

以上内容还有更简单的写法,用语法asyncawait,两步替换:

  1. @asyncio.coroutine替换为async
  2. yield from替换为await;
1 async def hello():    #其余代码不变
2     print("Hello world!")
3     r = await asyncio.sleep(1)
4     print("Hello again!")

asyncio可以实现单线程并发IO操作。如果仅用在客户端发挥的威力不大。如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+coroutine实现多用户的高并发支持。

asyncio实现了TCP、UDP、SSL等协议,aiohttp则是基于asyncio实现的HTTP框架。

pip install aiohttp    #安装

 

 

原文地址:https://www.cnblogs.com/Real-Ying/p/6678026.html