Python学习(六)

学习网址:廖雪峰的Python教程

进程VS线程

多进程和多线程是实现多任务最常用的两种方式.实现多任务,通常会设计Master-Worker模式,Master负责分配任务,worker负责执行任务.

多任务环境下,就是一个Master,多个Worker.

如果用多进程实现Master-Worker,那主线程就是Master,其他线程就是Worker.

如果用多线程实现Master-Worker,那主线程是Master,其他线程就是Worker.

  • 多进程的优势:稳定性高,一个子进程崩溃了,也不会影响其他进程.(主进程崩溃了,其他进程也就挂掉了,由于主进程主要负责分配任务,所以崩溃的几率很低).
  • 多进程的缺点:创建进程的代价大,在Windows下创建进程开销巨大.操作系统同时运行的进程数也是有限的,如果有上千个进程在运行,操作系统调度都有问题.
  • 多线程的优势就是比多进程快一点,这个一点可以忽略不计.
  • 最大的缺点就是,一旦一个线程挂掉了,可能会导致其他线程的崩溃.因为所有线程共享进程的内存.

在Windows系统下多线程的效率要比多进程要高.

线程切换

无论是多进程还是多线程,只要数量达到一定上限,效率都会下降.如果有几千个任务同时进行,操作系统就把主要的精力花费在了切换任务上,没有多少时间执行任务.

计算密集型和IO密集型

是否考虑多任务,还要考虑任务的类型.可以把任务分为计算密集型和IO密集型 .

  • 计算密集型任务的特点是要进行大量的计算,消耗CPU资源,靠的是CPU的运算能力,虽然可以用多任务完成,但任务越多,花费在切换任务的时间就越多,CPU的执行效率就会下降.要想有效的利用CPU,在此类型的多任务下,同时进行的数量应等于CPU的核心数.还有,代码的运行效率也很重要,推荐用c语言编写计算密集型任务.
  • IO密集型,主要涉及到网络、磁盘IO任务,这类任务的特点就是占用CPU资源很少,主要都在等待IO操作的完成,(因为IO速度低于CPU和内存的速度).对于此类型任务,任务越多,CPU效率越高,也有上限值.由于IO密集型任务把大部分时间花费在IO上,在CPU的时间很少,所以,对于此类任务,最合适的语言就是开发效率最高(代码量最少)的语言,首选脚本语言.

异步IO

由于CPU和IO之间存在巨大的速度差异,一个任务执行的大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,才需要多进程或多线程模型来支持多任务并发执行.

充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种叫事件驱动模型

对于Python,单线程的异步编程模型称为协程,有了它,就可以基于事件驱动编写高效的多任务程序.

分布式进程

Thread和Process中,优选Process,因为Process更稳定,而且还可以分布到多台机器上,而Thread只能分布到同一台机器的多个CPU上.

Python的multiprocessing模块不但支持多进程,managers子模块还支持把多进程分布到多台机器上,一个服务进程可以作为调度者,将任务分布到其他多个线程中 ,依靠网络通信.

managers模块封装很好,不必了解网络通信的细节,就可以编写分布式多进程程序.

现有一个通过Queue通信的多进程程序在同一台机器上运行,由于, 处理任务的进程任务越来越繁重,想把发送任务的进程和处理任务的进程分布到两台机器上.

分布式进程的方法就是,通过managers模块把Queue通过网络暴露出去,就可以让其他机器的进程访问Queue了.

服务进程,负责启动Queue,把Queue注册到网络上,然后向Queue写入任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import random, time, queue
from multiprocessing.managers import BaseManager
# 发送任务的队列:
task_queue = queue.Queue()
# 接收结果的队列:
result_queue = queue.Queue()
# 从BaseManager继承的QueueManager:
class QueueManager(BaseManager):
pass
# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
QueueManager.register('get_task_queue', callable=lambda: task_queue)
QueueManager.register('get_result_queue', callable=lambda: result_queue)
# 绑定端口5000, 设置验证码'abc':
manager = QueueManager(address=('', 5000), authkey=b'abc')
# 启动Queue:
manager.start()
# 获得通过网络访问的Queue对象:
task = manager.get_task_queue()
result = manager.get_result_queue()
# 放几个任务进去:
for i in range(10):
n = random.randint(0, 10000)
print('Put task %d...' % n)
task.put(n)
# 从result队列读取结果:
print('Try get results...')
for i in range(10):
r = result.get(timeout=10)
print('Result: %s' % r)
# 关闭:
manager.shutdown()
print('master exit.')

运行期间有出现错误:PermissionError: [WinError 5] 拒绝访问.

更改后的task_master.py的代码,运行正常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import random, time, queue
from multiprocessing.managers import BaseManager
from multiprocessing import freeze_support
# 发送任务的队列:
task_queue = queue.Queue()
# 接收结果的队列:
result_queue = queue.Queue()
# 从BaseManager继承的QueueManager:
class QueueManager(BaseManager):
pass
def return_task_queue():
global task_queue
return task_queue
def return_result_queue():
global result_queue
return result_queue
def reg():
# 把两个Queue都注册到网络上, callable参数关联了Queue对象
QueueManager.register('get_task_queue',callable=return_task_queue)
QueueManager.register('get_result_queue', allable=return_result_queue)
# 绑定端口5000, 设置验证码'abc':
manager = QueueManager(address=('127.0.0.1', 5000), authkey=b'abc')
# 启动Queue:
manager.start()
# 获得通过网络访问的Queue对象:
task = manager.get_task_queue()
result = manager.get_result_queue()
# 放几个任务进去:
for i in range(10):
n = random.randint(0, 10000)
print('Put task %d...' % n)
task.put(n)
# 从result队列读取结果:
print('Try get results...')
for i in range(10):
r = result.get(timeout=10)
print('Result: %s' % r)
# 关闭:
manager.shutdown()
print('master exit.')
if name == 'main':
freeze_support()
reg()

在一台机器上写多进程程序时,创建的Queue可以直接拿来用,但在分布式多进程环境下,添加任务到Queue不能对原始的task_queue进行操作,必须通过manager.get_task_queue()获得的Queue接口添加.

任务进程,负责处理任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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.')

如果运行task_worker.py期间出现ConnectionRefusedError: [WinError 10061]由于目标计算机积极拒绝,请把你的真实的IP地址替换掉127.0.0.1

当启动服务进程task_master.py文件时的运行结果:

1
2
3
4
5
6
7
8
9
10
11
Put task 7762...
Put task 1014...
Put task 5055...
Put task 2348...
Put task 1738...
Put task 1615...
Put task 6544...
Put task 419...
Put task 1959...
Put task 7894...
Try get results...

然后再运行任务进程take_worker.py文件,结果:

1
2
3
4
5
6
7
8
9
10
11
Connect to server 127.0.0.1...
run task 7762 * 7762...
run task 1014 * 1014...
run task 5055 * 5055...
run task 2348 * 2348...
run task 1738 * 1738...
run task 1615 * 1615...
run task 6544 * 6544...
run task 419 * 419...
run task 1959 * 1959...
run task 7894 * 7894...

然后,服务进程take_master.py就会跟着输出:

1
2
3
4
5
6
7
8
9
10
11
Result: 7762 * 7762 = 60248644
Result: 1014 * 1014 = 1028196
Result: 5055 * 5055 = 25553025
Result: 2348 * 2348 = 5513104
Result: 1738 * 1738 = 3020644
Result: 1615 * 1615 = 2608225
Result: 6544 * 6544 = 42823936
Result: 419 * 419 = 175561
Result: 1959 * 1959 = 3837681
Result: 7894 * 7894 = 62315236
master exit.

queue对象存储在task_master.py进程中:

img

Queue能通过网络访问,是通过QueueManager实现的,它管理的不只是一个Queue,要给每个Queue的网络调用接口起个名字,比如:get_task_queue.

authkey,是为了保证两台机器正常通信,不被其他机器恶意干扰.就像一个暗号,如果两边的暗号对不上,那就连接不上了.

Python的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下.Queue的作用是用来传递任务和接收结果,每个任务的描述数据量要尽量小.

正则表达式

正则表达式是一种用来匹配字符串的工具.设计思想是用一种描述性语言给字符串定义一个规则,凡是符合规则的字符串,就可以认为匹配.


字符集合 说明
\w 可以匹配任何一个字母或者数字或者下划线
\W W大写,可以匹配任何一个字母或者数字或者下划线以外的字符
\s 可以匹配空格、制表符、换页符等空白字符的其中任意一个
\S S大写,可以匹配任何一个空白字符以外的字符
\d 可以匹配任何一个 0~9 数字字符
\D D大写,可以匹配任何一个非数字字符
[ ] 用来自定义能够匹配 ‘多种字符’ 的表达式。要匹配中括号,请使用 “[“ 和 “]“
{ } 修饰匹配次数的符号。要匹配大括号,请使用 “{“ 和 “}“
. 匹配除了换行符(\n)以外的任意一个字符。要匹配小数点本身,请使用 “.“
^ 匹配输入字符串的开始位置。
$ 匹配输入字符串的结尾位置。
+ 修饰匹配次数为至少 1 次。
( ) 标记一个子表达式的开始和结束位置。
* 修饰匹配次数为 0 次或任意次。

python字符串本身也用\转义,使用Python的r前缀,就不用考虑转义了.

1
2
3
4
5
>>> import re
>>> re.match(r'^\d{3}-\d{3,8}', '010-12345')
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> re.match(r'^\d{3}\-\d{3,8}', '010 12345')
>>>

match()方法,如果匹配成功,就返回一个Match对象,如果不匹配,返回None.

切分字符串

单个空格分割:

1
2
>>> re.split(r'\s+', 'a b c')
['a', 'b', 'c']

多个空格,符号,切割:

1
2
>>> re.split(r'[\s\,\;]+', 'a,b;; c d')
['a', 'b', 'c', 'd']

分组

可以用()来进行分组

1
2
3
4
5
6
7
8
9
>>> m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345')
>>> m
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> m.group(0)
'010-12345'
>>> m.group(1)
'010'
>>> m.group(2)
'12345'

^(\d{3})-(\d{3,8})$ 定义了两个组,可以直接分出,区号和电话号码.可以用group()方法取出分组,group(0)代表要匹配的本身的字符串,group(1)就是第一个子字符串,以此类推.

贪婪匹配

正则表达式默认的是贪婪匹配,就是匹配尽可能多的字符.

1
2
>>> re.match(r'^(\d+)(0*)$', '102300').groups()
('102300', '')

^(\d+)(0*)$ 的第一个分组\d+把所有的0都匹配了,后面的0*只匹配了空字符.

加个来使用非贪婪匹配

1
2
>>> re.match(r'^(\d+?)(0*)$', '102300').groups()
('1023', '00')

很厉害的正则表达式在线匹配网站

re模块

python提供的模块,包含所有正则表达式的功能.

Python官方re模块链接

一个email的匹配:(不一定准确)

1
2
3
4
5
6
>>> import re
>>> c = re.compile(r'^(\w{3,11}\@{1}\w{3,8}.\w{3})')
>>> c.match('snowluliang@gmalil.com')
<_sre.SRE_Match object; span=(0, 22), match='snowluliang@gmalil.com'>
>>> c.match('18651528973@163.com')
<_sre.SRE_Match object; span=(0, 19), match='18651528973@163.com'>

真诚地希望能帮到你!