多线程 及 分布式进程间的通信

Posted ting-light

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程 及 分布式进程间的通信相关的知识,希望对你有一定的参考价值。

#!/usr/bin/env python3

# -*- coding: utf-8 -*-

 

#!/usr/bin/env python3

#-*- coding:utf-8 -*-

 

#多线程

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

#进程是若干线程组成,一个进程至少有一个线程

#由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,python 的线程是真正的posix thread,而不是模拟出来的线程。

#python 的标准库提供了两个模块:_thread 和threading,_thread 是低级模块,threading 是高级模块,对_thread进行了封装。通常,只需使用threading这个模块。

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

import time,threading 

#新线程执行的代码:

def loop():

    print(‘thread %s is running...‘ % threading.current_thread().name)

    n=0

    while n < 5:

        n=n+1

        print(‘thread %s >>>%s‘ % (threading.current_thread().name,n))

        time.sleep(1)

    print(‘thread %s ended‘ % threading.current_thread().name)

print(‘thread %s is running...‘ % threading.current_thread().name)

t=threading.Thread(target=loop,name=‘LoopThread‘)

t.start()

t.join()

print(‘thread %s ended ‘ % threading.current_thread().name)

 

‘‘‘

thread MainThread is running...

thread LoopThread is running...

thread LoopThread >>>1

thread LoopThread >>>2

thread LoopThread >>>3

thread LoopThread >>>4

thread LoopThread >>>5

thread LoopThread ended

thread MainThread ended

‘‘‘

#由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回

#当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,

#如果不起名字Python就自动给线程命名为Thread-1,Thread-2...

 

#Lock

#多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何

#一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

#来看看多个线程同时操作一个变量怎么把内容给改乱了:

import time,threading

balance=0

def change_it(n):

    global balance 

    balance=balance + n

    balancd=balance - n 

def run_thread(n):

    for i in range(100000):

        change_it(n)

t1=threading.Thread(target=run_thread,args=(5,))

t2=threading.Thread(target=run_thread,args=(8,))

t1.start()

t2.start()

t1.join()

t2.join()

print(balance)

#我们定义了一个共享变量balance,初始值为0,并且启动两个线程,先加后减,理论上结果应该为0,但是,由于线程的调度是由操作系统决定的,当t1/t2交替

#执行时,只要循环次数足够多    ,balance的结果就不一定是0了。

#原因是因为高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算:

#balance=balance+n 

#也分两步:

#1.计算balance+n,存入临时变量中;

#2.将临时变量的值赋给balance。    

#也可以看成是

#x=balance +n 

#balance = x 

#由于x是局部变量,两个线程各自都有自己的x,当代码正常执行时:

#t1和t2是交替运行的,如果操作系统以下面的顺序执行t1/t2:

#初始值 balance=0

‘‘‘

t1: x1=balance + 5 

 

t2: x2=balance+8

t2:balance=x2 

 

t1:balance=x1 

t1:x1=balance-5

t1:balance=x1

 

t2:x2=balance-8

t2:balance=x2 

 

结果 balance = -8

 

‘‘‘

#究其原因,是因为修改balance需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。

 

#如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行

#change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的

#冲突。创建一个锁就是通过threading.Lock()来实现:

balance=0

lock=threading.Lock()

def run_thread(n):

    for i in range(10000):

        #先要获取锁:

        lock.acquire()

        try:

            #放心的改吧:

            change_it(n)

        finally:

            #改完后要释放锁

            lock.release()

#当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续执行代码,其他线程就继续等待直到获得锁为止。

#获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。

#锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程

#模式执行,效率就大大下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并在试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,

#既不能执行,也无法结束,只能靠操作系统强制终止。

 #多核CPU

#一个死循环线程会100%占用一个CPU。如果有两个死循环,在多核CPU中,可以监控到会占用200%的CPU,也就是占用两个CPU核心。

#要想把N核CPU的核心全部跑满,就必须启动N个死循环线程。

#Python程序启动与CPU核心数量相同的N个线程,发现,在4核CPU上可以监控到CPU占用率仅有102%,也就是仅使用了一核。

#但是用C/C++/JAVA 来写相同的死循环,直接可以把全部核心跑满,4核就跑400%,8核就跑到800%,为什么Python不行呢?

#因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock ,任何Python线程执行前,必须先获得GIL锁,然后,每执行100

#条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100

#个线程跑在100核CPU上,也只能用到1个核。

#Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。

 

#多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。

#Python 解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。

 

#ThreadLocal

 

#在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的

#修改必须加锁。

#但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦。

#ThreadLocal对象可以解决这个问题

import threading

#创建全局ThreadLocal对象:

local_school=threading.local()

def process_student():

    #获取当前线程关联的student:

    std=local_school.student

    print(‘Hello,%s (in %s)‘ %(std,threading.current_thread().name))

    def process_thread(name):

    #绑定threadLocal的student:

    local_school.student=name

    process_student()

    t1=threading.Thread(target=process_thread,args=(‘Alice‘,),name=‘Thread-A‘)

t2=threading.Thread(target=process_thread,args=(‘Bob‘,),name=‘Thread-B‘)

t1.start()

t2.start()

t1.join()

t2.join()

‘‘‘

Hello,Alice (in Thread-A)

Hello,Bob (in Thread-B)

‘‘‘

 

#全局变量local_school 就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如

#local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。

#可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等

#ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

 

#一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。

 

#分布式进程

 

#在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。

 

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

#进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。

 

#举个例子:如果我们已经有一个通过Queue通信的多进程程序在同一台机器上运行,现在,由于处理任务的进程任务繁重,希望把发送任务的进程和处理任务的进程

#分布到两台机器上。怎么用分布式进程实现?

 

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

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

#task_master.py  

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.‘)

 

#请注意,当我们在一台机器上写多进程程序时,创建的Queue可以直接拿来用,但是,在分布式多进程环境下,添加任务到Queue不可以直接对原始的task_queue

#进行操作,那样就绕过了QueueManager的封装,必须通过manager.get_task_queue()获得的Queue接口添加。

#然后,在另一台机器上启动任务进程(本机上启动也可以):

 

#task_worker.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。

#现在,可以试试分布式进程的工作效果,先启动task_master.py服务进程:

 

#运行结果省略(因为没有服务器IP...)

 

#task_master.py 进程发送完任务后,开始等待result队列的结果。现在启动task_worker.py 进程:

 

#运行结果省略(因为没有服务器IP...)

 

#task_worker.py 进程结束,在task_master.py 进程中会继续打印出结果:

 

#运行结果省略(因为没有服务器IP...)

 

#这个简单的Master/Worker模型有什么用?其实这就是一个简单但真正的分布式计算,把代码稍加改造,启动多个worker,就可以把任务分布到几台甚至十几台机器上,比如

#把计算n*n的代码换成发送邮件,就实现了邮件队列的异步发送。

#Queue对象存储在哪?注意到task_worker.py中根本就没有创建Queue的代码,所以,Queue对象存储在task_master.py 进程中 :

#而Queue之所以能通过网络访问,就是通过QueueManager实现的。由于QueueManager管理的不止一个Queue,所以,要给每个Queue的网络调用接口起个名字,比如

#get_task_queue.

#authkey有什么用?这是为了保证两台机器正常通信,不被其他机器恶意干扰。如果task_worker.py 的 authkey和task_master.py的authkey不一致,肯定连接不上。

 

#Python 的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下。

 

#注意Queue的作用是用来传递任务和接收结果,每个任务的描述数量要尽量小。比如发送一个处理日志文件的任务,就不要发送几百兆的日志文件本身,而是发送日志

#文件存放的完整路径,由worker进程再去共享的磁盘上读取文件。

以上是关于多线程 及 分布式进程间的通信的主要内容,如果未能解决你的问题,请参考以下文章

进程间通信方式及特点

进程互斥同步及通信死锁问题操作系统

VC++ 进程间的通信

iOS开发多线程篇—线程间的通信

什么是分布式锁?为什么要用分布式锁?面试必问!

iOS开发多线程篇—线程间的通信