并发通信(进程与线程)

进程与线程在通信中都会遇到各自的问题,这是因他们各自的性质而产生的。每个进程有自己的地址空间,两个进程中的地址即使值相同,实际指向的位置也不同,故进程间无法直接就能通信。虽然同一进程的线程共享全局变量和内存,使得线程之间共享数据很容易也很方便,但会带来某些共享数据的互斥问题。下面用代码来值观地展示各自的问题。

进程通信限制:

import multiprocessing
def func():
    global a
    a = 2
if __name__ == '__main__':#window系统用进程线程要加上这句
    a = 1
    proc = multiprocessing.Process(target=func)
    proc.start()
    proc.join()#确保子进程结束再打印a值
    print(a)
1

可以发现,主进程的变量,即使在子进程中声名为全局变量,但由于互不干扰的内存地址空间而造成两个进程间完全失去了联系,故在子进程中是始终都无法修改到全局变量a的值。

进程通信解决方法:

为解决这个问题,在进程模块中引入了一个管理器,这个管理器可以开辟一个进程间共享的内存空间,这个空间的类型可以是列表、字典、队列。最常用的类型就是队列了,它是专门用于进程线程通信的数据结构,后面会详细介绍到。在开启进程时,我们可以将这个共享内存空间当作参数传入进程中,就可以对其操作,达到进程间相互通信。

from multiprocessing import Process,Manager
def func(que):
    a = que.get()
    a = 2
    que.put(a)

if __name__ == '__main__':#window系统用进程线程要加上这句
    a = 1
    mgr = Manager() #实例化生成一个管理器
    que = mgr.Queue()#管理器开辟一个共享空间,这个类型是队列
    que.put(a)#队列进的操作
    proc = Process(target=func,args=(que,))
    proc.start()
    proc.join()#确保子进程结束再打印a值
    a = que.get()#队列出的操作
    print(a)
2

上述代码在主进程中传入一个全局变量a到队列中,然后在子进程中取出a,并把a的值变为2,再放回队列中,最后在主进程中取出a的值并赋予给a,这样就间接实现了进程间的通信。

线程通信的限制:

线程是共享内存空间,所以同一个进程下的所有线程都可以直接通信,但存在资源竞争的情况,这是什么意思呢?下面给出一个代码示例。

from threading import Thread
def decr(n):
    global a
    for i in range(n): #对全局变量做n次减一
        a -= 1
def incr(n):
    global a
    for i in range(n):#对全局变量做n次加一
        a += 1
if __name__ == '__main__':#window系统用进程线程要加上这句
    a = 1
    thre1 = Thread(target=decr,args=(100000,))
    thre2 = Thread(target=incr,args=(100000,))
    thre1.start()
    thre2.start()
    thre1.join()#确保子线程结束再打印a值
    thre2.join()#确保子线程结束再打印a值
    print(a)
-56445

上述代码是对1进行十万次加一操作和十万次减一操作,按道理来说,最后结果应该还是为1,可为什么是奇怪的-56445呢?我们都知道线程的轮询调度是python解释器调用cpu来进行的,可在计算机中,加减操作往往不是一步就能做到的,细分的话其分为三步,分别是取值、运算、赋值。即先取出a=1,再进行运算a+1,最后将结果2赋值给a,而在cpu高速的轮询调度中,很有可能会在三步没有做完的情况下就调到另一个线程中去了,这就会导致这一次运算失效。

线程通信解决方法:

在线程模块中,为解决这个问题引入了一个方法叫互斥锁,它可以控制共享资源的访问,既可以将计算问题如加一操作上锁,这样当轮询发现上锁的话就不会 轮询到别的线程,这样就能保证一个完整的运算操作能操作完,很明显,互斥锁虽然可以解决这个问题,但同时也会导致性能下降,因为这将导致轮询次数上升。故在线程中一般不用于计算,而互斥锁能不加就不加。

from threading import Thread,Lock
def decr(n):
    global a
    for i in range(n): #对全局变量做n次减一
        lock.acquire()#上锁
        a -= 1
        lock.release()#解锁
def incr(n):
    global a
    for i in range(n):#对全局变量做n次加一
        lock.acquire()
        a += 1
        lock.release()
if __name__ == '__main__':#window系统用进程线程要加上这句
    a = 1
    lock = Lock()#对互斥锁进行实例化
    thre1 = Thread(target=decr,args=(100000,))
    thre2 = Thread(target=incr,args=(100000,))
    thre1.start()
    thre2.start()
    thre1.join()#确保子线程结束再打印a值
    thre2.join()#确保子线程结束再打印a值
    print(a)
1

公共资源——队列

队列是专门用于进程线程通信的数据结构,在线程中导入queue模块来创建队列,在进程中用进程模块的管理器生成队列。下面是队列的基本方法:

  • 入队:put(item),当队列满了,进行put操作会阻塞,队列未满解阻塞,put一次,队列计数器会加一。
  • 出队:get(),当队列空了,进行get操作会阻塞,队列非空解阻塞。
  • 测试空:empty(),空的话返回False。
  • 测试满:full(),满的话返回True。
  • 返回队列长度:qsize()
  • 任务结束:task_done(),执行此操作,队列计数器会减一。
  • 等待完成:join(),遇到join就阻塞,当队列计数器为0,解阻塞。

task_done和join方法是线程独有。

实例化:

  • 线程实例化:queue.Queue([num]),num是指队列的容量,不写参数默认无限大,所以最好加上参数。
  • 进程实例化:multiprocessing.Manager.Queue([num])

生产者与消费者模式

所谓的生产者与消费者模式就是将进程或线程的通信问题分开考虑,生产者只需要往队列里扔东西,消费者只需要往队列里拿东西,而生产者和消费者没任何关系,相互独立。这种模式在进程和线程中得到了广泛的使用。

下面我们开启两个线程,一个充当生产者,另一个充当消费者。

from threading import Thread
from queue import Queue
import random
def get(que):
    while True:
        i = que.get()
        print("消费者消费了%d"%i)
        que.task_done() #使计数器减一 
def put(que):
    while True:
        i = random.randint(1,100)
        que.put(i)
        print("生产者生产了%d"%i)
if __name__ == '__main__':#window系统用进程线程要加上这句
    que = Queue()
    thre1 = Thread(target=put,args=(que,)) #生产者
    thre2 = Thread(target=get,args=(que,)) #消费者
    thre1.start()
    thre2.start()
    que.join()#计数器为0,解阻塞
生产者生产了32
消费者消费了54
生产者生产了63
消费者消费了61
生产者生产了100
消费者消费了26
生产者生产了85
消费者消费了51
生产者生产了96
消费者消费了70
生产者生产了98
消费者消费了27
生产者生产了68
消费者消费了9
生产者生产了10
消费者消费了38
生产者生产了75
消费者消费了41
生产者生产了16
消费者消费了96
生产者生产了3
消费者消费了39
生产者生产了68
消费者消费了34
生产者生产了45
消费者消费了29
生产者生产了70
消费者消费了58
生产者生产了96
消费者消费了10
生产者生产了65
.......

下面用面向对象化的思想来重写上面的代码

from threading import Thread
from queue import Queue
import random

if __name__ == '__main__':#window系统用进程线程要加上这句
    class Product(Thread):#充当生产者
        def __init__(self,que):
            super().__init__()
            self.que = que
            self.start()
        def run(self):
            while True:
                i = random.randint(1, 100)
                que.put(i)
                print("生产者生产了%d"%i)
    class Consumer(Thread):#充当消费者
        def __init__(self,que):
            super().__init__()
            self.que = que
            self.start()
        def run(self):
            while True:
                i = que.get()
                print("消费者消费了%d"%i)
                que.task_done()
    que = Queue()
    Product(que)
    Consumer(que)
    que.join()
生产者生产了85
消费者消费了51
生产者生产了96
消费者消费了70
生产者生产了98
消费者消费了27
生产者生产了68
消费者消费了9
生产者生产了10
消费者消费了38
生产者生产了75
消费者消费了41
生产者生产了16
消费者消费了96
生产者生产了3
消费者消费了39
生产者生产了68
消费者消费了34
生产者生产了45
消费者消费了29
.............

 

posted @ 2018-11-01 15:03  龙~白  阅读(709)  评论(0编辑  收藏  举报