Python3之进程和线程
多进程
multiprocessing
如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。
由于Windows没有fork调用,
难道在Windows上无法用Python编写多进程的程序?
由于Python是跨平台的,自然也应该提供一个跨平台的多进程支持。
multiprocessing模块就是跨平台版本的多进程模块。
multiprocessing模块提供了一个Process类来代表一个进程对象,
下面的例子演示了启动一个子进程并等待其结束:
# 子进程要执行的代码
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
# 创建子进程对象
p = Process(target=run_proc, args=('test',))
创建子进程时,只需要传入一个执行函数和一个参数,
创建一个Process实例,
start()方法:启动进程对象,这样创建进程比fork()还要简单;
join()方法:可以等待子进程结束后再继续往下运行,
进程间通信
Process之间肯定是需要通信的,
操作系统提供了很多机制来实现进程间的通信。
【事件、临界区、互斥体、读写锁、信号量】
Python的multiprocessing模块包装了底层的机制,
提供了Queue、Pipes等多种方式来交换数据。
我们以Queue为例,在父进程中创建两个子进程,
一个往Queue里写数据,一个从Queue里读数据:
# 写数据进程执行的代码:
def write(q):
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())
# 读数据进程执行的代码:
def read(q):
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()
pr.start()
原理分析
Linux:
multiprocessing模块封装了fork()调用,
使我们不需要关注fork()的细节。
Windows:
本身没有fork调用,
multiprocessing需要“模拟”出fork的效果,
父进程所有Python对象通过pickle序列化再传到子进程去
多线程
线程简介
多线程类似于同时执行多个不同程序,
多线程运行有如下优点:
- 使用线程可以把占据长时间的程序中的任务放到后台去处理。
- 用户界面可以更加吸引人,比如弹出一个进度条来显示处理的进度
- 程序的运行速度可能加快
线程在执行过程中与进程还是有区别的:
每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。
但是线程不能够独立存在,必须依存在应用程序中,
由应用程序提供多个线程执行控制。
每个线程都有他自己的一组CPU寄存器,
称为线程的上下文,
该上下文反映了线程上次运行该线程的CPU寄存器的状态。
线程可以被抢占(中断),
在其他线程正在运行时,
线程可以暂时搁置(也称为睡眠)。
Python线程:_thread
Python中使用线程有两种方式:
- 函数方式
- 类方式
Python通过两个标准库_thread和threading提供对线程的支持。
_thread提供了低级别的、原始的线程以及一个简单的锁。
使用_thread模块创建线程
函数式:调用_thread模块中的start_new_thread()函数来产生新线程。
语法如下:
thread.start_new_thread ( function, args[, kwargs] )
参数说明:
function - 线程函数。
args - 传递给线程函数的参数,必须是个tuple类型。
kwargs - 可选参数。
from _thread import start_new_thread
# 为线程定义一个入口函数
def print_time(threadName, delay):
count = 0
while count < 5:
time.sleep(delay)
count += 1
print( "%s: %d" % (threadName, count ) )
# 创建线程
try:
start_new_thread(print_time, ("Thread-1", 2))
except:
print("Error: unable to start thread")
Python线程:Threading
使用Threading模块创建线程,
直接从threading.Thread继承,
然后重写__init__方法和run方法:
Python 在thread的基础上还提供了一个高级的线程控制库,
就是之前提到过的threading。
Python的threading module是在建立在thread module基础之上的一个module,
在threading module中,暴露了许多thread module中的属性。
在thread module中,python提供了用户级的线程同步工具:Lock对象。
而在threading module中,python提供了Lock对象的变种: RLock对象。
RLock对象内部维护着一个Lock对象,它是一种可重入的对象。
threading模块提供的类:
Thread, Lock, Rlock, Condition, [Bounded]Semaphore, Event, Timer, local。
threading 模块提供的方法:
threading.currentThread(): 返回当前的线程变量。
threading.enumerate(): 返回一个包含正在运行的线程的list。
正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount(): 返回正在运行的线程数量。
threading线程模块同样提供了Thread类来处理线程,
Thread类提供了以下方法:
- run(): 用以表示线程活动的方法。
- start():启动线程活动。
- join([time]): 等待至线程中止。
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
案例分析如下:
# 线程入口函数
def print_time(thread_name, delay, counter):
while counter:
if exitFlag:
threading.Thread.exit()
time.sleep(delay)
print("%s: %d" % (thread_name, counter))
counter -= 1
# 继承父类threading.Thread
class myThread(threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
# 把要执行的代码写到run函数里面 线程在创建后会直接运行run函数
def run(self):
print("Starting " + self.name)
print_time(self.name, self.counter, 3)
print("Exiting " + self.name )
# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
# 开启线程
thread1.start()
线程同步
如果多个线程共同对某个数据修改,则可能出现不可预料的结果,
为了保证数据的正确性,需要对多个线程进行同步。
多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。
但是当线程需要共享数据时,可能存在数据不同步的问题。
为了避免这种情况,引入了锁的概念。
锁有两种状态——锁定和未锁定。
每当一个线程A要访问共享数据时,必须先获得锁定;
如果已经有别的线程B已经获得锁了,
那么就让线程A暂停,也就是同步阻塞;
等到线程B访问完毕,释放锁以后,再让线程A继续。
使用Thread对象的Lock和Rlock可以实现简单的线程同步,
这两个对象都有acquire方法和release方法,
对于那些需要每次只允许一个线程操作的数据,
可以将其操作放到acquire和release方法之间。
如下:
class myThread (threading.Thread):
def __init__(self, threadID, name, counter):
…
def run(self):
# 获得锁,成功获得锁定后返回True
# 可选的timeout参数不填时将一直阻塞直到获得锁定
# 否则超时后将返回False
threadLock.acquire()
print_time(self.name, self.counter, 3)
# 释放锁
threadLock.release()
# 获取threading的Lock对象
threadLock = threading.Lock()
线程优先级队列( Queue)
Python的Queue模块中提供了同步的、线程安全的队列类:
- FIFO(先入先出)队列Queue
- LIFO(后入先出)队列LifoQueue
- 优先级队列PriorityQueue
这些队列都实现了锁原语,能够在多线程中直接使用。
可以使用队列来实现线程间的同步。
Queue模块中的常用方法:
Queue.qsize() 返回队列的大小
Queue.empty() 如果队列为空,返回True,反之False
Queue.full() 如果队列满了,返回True,反之False
Queue.full 与 maxsize 大小对应
Queue.get([block[, timeout]])获取队列,timeout等待时间
Queue.get_nowait() 相当Queue.get(False)
Queue.put(item) 写入队列,timeout等待时间
Queue.put_nowait(item) 相当Queue.put(item, False)
Queue.task_done() 在完成一项工作之后,
Queue.task_done()函数向任务已经完成的队列发送一个信号
Queue.join() 实际上意味着等到队列为空,再执行别的操作
案例分析如下:
class myThread (threading.Thread):
def __init__(self, threadID, name, q):
…
def run(self):
…
def process_data(threadName, q):
while not exitFlag:
queueLock.acquire()
if not workQueue.empty():
# 读取数据,相当于“消费者线程”
data = q.get()
queueLock.release()
# 创建对象:锁、队列
queueLock = threading.Lock()
workQueue = Queue.Queue(10)
# 创建新线程
for tName in threadList:
thread = myThread(threadID, tName, workQueue)
thread.start()
# 填充队列,相当于“生产者线程”
queueLock.acquire()
for word in nameList:
workQueue.put(word)
queueLock.release()
ThreadLocal
我们知道多线程环境下,每一个线程都可以使用所属进程的全局变量。
如果一个线程对全局变量进行了修改,将会影响到其他所有的线程。
为了避免多个线程同时对变量进行修改,
引入了线程同步机制,通过互斥锁,条件变量或者读写锁来控制对全局变量的访问。
只用全局变量并不能满足多线程环境的需求,
很多时候线程还需要拥有自己的私有数据,这些数据对于其他线程来说不可见。
因此线程中也可以使用局部变量,局部变量只有线程自身可以访问,
同一个进程下的其他线程不可访问。
有时候使用局部变量不太方便,
因此 python 还提供了 ThreadLocal 变量,
它本身是一个全局变量,
但是每个线程却可以利用它来保存属于自己的私有数据,
这些私有数据对其他线程也是不可见的。
下图给出了线程中这几种变量的存在情况:
分析:
一个ThreadLocal变量虽然是全局变量,
但每个线程都只能读写自己线程的独立副本,互不干扰。
ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。
分布式进程
在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。
小结
Python的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下。
注意Queue的作用是用来传递任务和接收结果,每个任务的描述数据量要尽量小。比如发送一个处理日志文件的任务,就不要发送几百兆的日志文件本身,而是发送日志文件存放的完整路径,由Worker进程再去共享的磁盘上读取文件。
所谓分布式进程就是让两个进程运行在不同的电脑上面通过网络来进行数据交流,下面是在windows下通过managers模块来进行的分布式进程!
import random,time,queue
from multiprocessing.managers import BaseManager
from multiprocessing import freeze_support
task_queue = queue.Queue()
result_queue = queue.Queue()
def return_task():
return task_queue
def return_result():
return result_queue
class QueueManager(BaseManager):
pass
def test():
QueueManager.register('get_task',callable = return_task)
QueueManager.register('get_result',callable = return_result)
manager = QueueManager(address = ('127.0.0.1',5000),authkey = b'abc')#绑定到端口5000,并且设置验证码'abc'
manager.start()
#获得经过封装之后的task和result
task,result = manager.get_task(),manager.get_result()
#放一些任务
for i in range(5):
print('put %d in task'%i)
task.put(i)
#获取结果
print('try to get result')
for i in range(5):
r = result.get(timeout = 10)
print('get the result %d'%r)
manager.shutdown()
print('master exit')
if __name__ == '__main__':
freeze_support()
test()
在worker模块里面,由于manager从网络获取相应数据,因此我们只需要给这个manager注册上获取数据的方法就可以了.
from multiprocessing.managers import BaseManager
from multiprocessing import freeze_support
class QueueManager(BaseManager):
pass
if __name__ == '__main__':
QueueManager.register('get_task')
QueueManager.register('get_result')
server_adr = '127.0.0.1'
print('connect to the server%s',server_adr)
manager = QueueManager(address = (server_adr,5000),authkey = b'abc')
manager.connect()
print('connect successfuly')
task = manager.get_task()
result = manager.get_result()
for i in range(5):
try:
t = task.get(timeout = 1)
print('now process the task%d'%t)
result.put(t*t)
except Queue.Empty:
print('the task queue is empty, maybe some task lost?')
print('wordker exit')