您的位置:首页 > 编程语言 > Python开发

Python基础—线程、进程和协程

2016-07-21 23:59 609 查看

      今天已是学习Python的第十一天,来干一碗鸡汤继续今天的内容,今天的鸡汤是:超越别人对你的期望。本篇博客主要介绍以下几点内容:

  • 线程的基本使用;

  • 线程的锁机制;

  • 生产者消费之模型(队列);

  • 如何自定义线程池;

  • 进程的基本使用;

  • 进程的锁机制;

  • 进程之间如何实现数据共享;

  • 进程池;

  • 协程的基本使用。

一、线程

1、创建线程

   上篇博客已经介绍过如何创建多线程的程序,在这里在复习一下如何创建线程过程以及线程的一些方法:

 

线程的方法:

  • start:线程准备就绪,等待CPU调度;

  • setName:为线程设置名称;

  • getName:获取线程名称;

  • setDaemon(布尔值):设置为主线程是否等待子线程执行(默认False);

        如果是将setDaemon设置成True,主线程执行过程中,子线程也在进行,主线程执行完毕后,子线程不论成功与否,均停止主线程不会等子线程;

        如果值为False,主线程执行过程中,子线程也在执行,主线程执行完毕后,等待子线程也执行完成后,程序停止。

  • join(秒):表示主线程到此,会等待子线程执行,参数表示主线程在此最多等待N秒后,继续往下执行;

  • run:线程被CPU调度后自动执行线程对象的run方法。

2、线程的锁机制

    下面我们来介绍一下线程的锁机制,由于线程之间是进行随机调度,并且每个线程可能只执行N条操作,当多个线程同时修改同一条数据时可能会出现脏数据,所以出现了线程锁。在python中分为三种线程锁:互斥锁(lock,Rlock)、信号量(Semaphore)、事件(event),还有一个条件(Condition)配合线程锁来使用,下面分别介绍这几种锁:

   (1)、互斥锁(lock,Rlock)

 我们先看一下不加线程锁的程序的执行结果:

 

当我们加上线程锁后,效果就会避免上面现象的发生:

 

     (2)、信号量(Semaphore)

    上面我们介绍了互斥锁,我们发现,互斥锁同时只能允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据,比如肯德基有3个购餐的窗口,那最多只允许3个人购买,后面的人只能等前面的人买完才能购买。

     (3)、事件(event)

    Python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法:set、wait、clear。

    事件处理的机制:全局定义了一个"Flag",如果"Flag"值为Flase,那么当程序执行event.wait方法时就会阻塞,如果"Flag"值为True,那么event.wait方法时便不再阻塞。

  • event.clear:将"Flag"设置成False,(加锁);

  • event.set:将"Flag"设置成True,(解锁)。

     (4)、条件(Condition)

    使得线程等待,只有满足条件的时候,才释放N个线程去更改数据,下面通过两种方法来演示加条件的线程锁操作:

 

 

     (5)、Timer

Timer:定时器,指定N秒之后执行某操作。

 

3、生产者消费者模型(队列)

   Queue模块实现了多生产者、多消费者队列,它特别适用于多线程编程。Queue类中实现了所有需要的锁语义,Queue模块实现了四种类型的队列:

  • queue.Queue先进先出队列(FIFO),第一加入队列的任务,被第一个取出;

  • queue.LifoQueue后进先出队列(LIFO),最后加入队列的任务,被第一个取出

  • queue.PriorityQueue:优先级队列,保持队列数据有序,是根据权重判断取出顺序,最小值被先取出。

  • queue.deque:双向队列,一种支持向两端高效地插入数据、支持随机访问的容器

下面通过例子来详细介绍一下先进先出队列的使用方法:

queue.Queue(先进先出):

 

 

 

通过上面的例子,我们总结一下queue队列提供的公共方法:

  • Queue.put:向队列中放入元素,block是否阻塞(默认True),timeout阻塞时的超时时间;

  • Queue.get:移除队列中的元素,block是否阻塞,timeout阻塞时超时时间;

  • queue.Queue(Maxsize):Maxsize,设置队列支持最大的个数;

  • Queue.qsize:队列的真实个数;

  • Queue.join,Queue.task_done:阻塞进程,当队列中任务执行完毕后,不再阻塞;

  • Queue.empty:判断队列是否为空。

queue.LifoQueue(后进先出):

 

[strong]queue.PriorityQueue(优先级队列):[/strong]

 

[strong]queue.deque(高性能双向队列):[/strong]

为什么说它是高性能的队列我们来对比双向队列、普通队列和列表的处理速度我们一起来看一下:

下面结合上面的知识来写一个生产者消费者模型:

4、自定义线程池

    在使用多线程处理任务也不是线程越多越好,由于在切换线程的时候,需要切换上下文环境,依然会造成CPU的大量开销。为了解决这个问题,就引出了线程池的概念。预先创建一个批线程,然过来的任务立刻能够使用,使用完以后自动释放来去处理新的任务,在Python中,没有内置的较好的线程池模块,需要自己实现或使用第三方模块,下面我们尝试来自定义一个线程池:

初级版:

进阶版:

 二、进程

1、创建进程

注意:由于进程之间的数据需要各自持有一份,所以创建进程需要非常大的开销。

2、进程间数据共享

 进程各自持有一份数据,默认是无法共享数据的。

 

通过上面的例子可以看出,每个进程都有自己的一份数据,没有共享数据,在Python中我们通常通过调用第三方模块的方式来实现进程之间的数据共享,主要是调用multiprocessing的Queues、Array、Manager这三个模块。下面我们通过例子类看一下具体用法:

 方法一:通过调用queues共享数据

 

方法二:通过调用数组Array来共享数据

 

    当看到这端代码时有个疑问就是Array数组中的'i',是什么?在Array类在实例化的时候必须指定数组的数据类型和数组的大小,具体数据类型的对照请参考下面的对应关系:

方法三:通过调用Manager字典来共享数据

 

3、进程锁

    为了防止和多线程一样的出现数据抢夺和脏数据的问题,同样需要设置进程锁。在multprocessing里也有与线程一样支持Rlock,Lock,Event,Condition,Semaphore几种锁,用法也相同,我们来看一下进程数的例子:

 

4、进程池

    进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可使用的进程,那么程序就会等待,知道进程池中有可用进程为止。

进程池中有两个方法:

  • apply:子进程串行执行任务,达不到并发的效果;

  • apply_async:apply的异步版本,支持并发,推荐使用这个。

总结一句话:IO密集型使用多线程,计算密集型使用多进程。 

三、协程

 线程和进程的操作是由程序触发系统接口,最后的执行者是系统,而协程的操作则是程序员自己。

 协程的原理:利用一个线程,分解一个线程成为多个"微线程",程序级别。

 协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。协程,则只使用一个线程,在一个线程中规定摸个代码块执行顺序。

 协程的适用场景:当程序存在大量不需要CPU的操作时(IO),适用于协程。

 使用协协程需要调用两个模块:greenlet模块(底层)、gevent模块(高性能)。

 使用greenlet模块:

使用gevent模块:

 下面的例子我们去分别请求多个网站,遇到IO操作来实现自动切换:

​   今天的内容就到这里了,例子中少了不少的注释,还有一些自己不太理解,见谅。



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: