pytnon之线程

来源:http://www.cnblogs.com/linhaifeng/articles/7430082.html

一、线程的理论知识

1.1线程的引入

为什么引出线程理论

60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端;
一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程
二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
因此在80年代,出现了能独立运行的基本单位——线程(Threads)

注意:进程是资源分配的最小单位,线程是CPU调度的最小单位;每一个进程中至少有一个线程。线程不能脱离进程单独存在。

在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程

1.2线程的概念

线程顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程是一个进程;

python中线程的本质就是具体执行代码的过程。是计算机中能够被cpu执行的最小单位

所以说:进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。

1.3线程的特点

创建进程的开销要远大于线程?

如果我们的软件是一个工厂,该工厂有多条流水线,流水线工作需要电源,电源只有一个即cpu(单核cpu)

  • 一个车间就是一个进程,一个车间至少一条流水线(一个进程至少一个线程),创建一个进程,就是创建一个车间(申请空间,在该空间内建至少一条流水线)
  • 而建线程,就只是在一个车间内造一条流水线,无需申请空间,所以创建开销小

不同的进程之间直接是竞争关系,存在抢占资源的现象

同一个进程中的线程之间是合作关系,可以共享资源

1.4为什么使用线程

多线程指的是,在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。详细的讲分为4点:

  1. 多线程共享一个进程的地址空间
  2. 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
  3. 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。
  4. 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)

1.5 线程与进程的区别

(1)一个程序至少有一个进程,一个进程至少有一个线程。(进程可以理解成线程的容器)

(2)进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大的提高了程序的运行效率。

(3)线程执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立的执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

(4)进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的独立单位。

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和线)但是它可与同属一个进程的其他线程共享进程所拥有的全部资源。

一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。

1.6经典的线程模型

多个线程共享同一个进程的地址空间中的资源,是对一台计算机上多个进程的模拟,有时也称线程为轻量级的进程

而对一台计算机上多个进程,则共享物理内存、磁盘、打印机等其他物理资源。

多线程的运行也多进程的运行类似,是cpu在多个线程之间的快速切换

不同的进程之间是充满敌意的,彼此是抢占、竞争cpu的关系,如果迅雷会和QQ抢资源。而同一个进程是由一个程序员的程序创建,所以同一进程内的线程是合作关系,一个线程可以访问另外一个线程的内存地址,大家都是共享的,一个线程干死了另外一个线程的内存,那纯属程序员脑子有问题。

类似于进程,每个线程也有自己的堆栈

不同于进程,线程库无法利用时钟中断强制线程让出CPU,可以调用thread_yield运行线程自动放弃cpu,让另外一个线程运行。

线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:

1. 父进程有多个线程,那么开启的子进程是否需要同样多的线程

如果是,那么父进程中某个线程被阻塞,那么copy到子进程后,copy版的线程也要被阻塞吗,想一想nginx的多线程模式接收用户连接。

2. 在同一个进程中,如果一个线程关闭了问题,而另外一个线程正准备往该文件内写内容呢?

如果一个线程注意到没有内存了,并开始分配更多的内存,在工作一半时,发生线程切换,新的线程也发现内存不够用了,又开始分配更多的内存,这样内存就被分配了多次,这些问题都是多线程编程的典型问题,需要仔细思考和设计。

1.7在用户空间实现的线程(了解)

线程的实现可以分为两类:用户级线程(User-Level Thread)和内核线线程(Kernel-Level Thread),后者又称为内核支持的线程或轻量级进程。在多线程操作系统中,各个系统的实现方式并不相同,在有的系统中实现了用户级线程,有的系统中实现了内核级线程。

用户级线程内核的切换由用户态程序自己控制内核切换,不需要内核干涉,少了进出内核态的消耗,但不能很好的利用多核Cpu,目前Linux pthread大体是这么做的。

在用户空间模拟操作系统对进程的调度,来调用一个进程中的线程,每个进程中都会有一个运行时系统,用来调度线程。此时当该进程获取cpu时,进程内再调度出一个线程去执行,同一时刻只有一个线程执行。

用户级与内核级线程的对比(了解)

1.8用户级线程和内核级线程的区别

  1. 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。
  2. 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。
  3. 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。
  4. 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。
  5. 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。

内核线程的优缺点

优点:当有多个处理机时,一个进程的多个线程可以同时执行。

缺点:由内核进行调度。

用户进程的优缺点

优点

  1. 线程的调度不需要内核直接参与,控制简单。
  2. 可以在不支持线程的操作系统中实现。
  3. 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。
  4. 允许每个进程定制自己的调度算法,线程管理比较灵活。
  5. 线程能够利用的表空间和堆栈空间比内核级线程多。
  6. 同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。

缺点

  1. 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用

 1.9 混合实现(了解)

  用户级与内核级的多路复用,内核同一调度内核线程,每个内核线程对应n个用户线程

 

二、多线程之threading模块

2.1 GIL 全局解释器锁(global interpreter lock)

在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势

解释型语言中,多线程在执行同一代码块时,如果出现多个线程对共享数据进行修改,可能导致数据不安全;

所以在Cpython解释器中,默认对python解释器添加了一把锁,这把锁是针对python解释器全局的,也就是GIL:global interpreter lock,会对同一进程中所有线程在同一时间限制只能有一个线程访问执行这一段代码。所以质性python代码多线程不能同时访问多个cpu。

首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。

python也可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。

像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。

总结:所以GIL并不是Python的特性,Python完全可以不依赖于GIL

想要更加深入理解GIL原理,强烈推荐看着篇文章,深入理解 GIL:如何写出高性能及线程安全的 Python 代码

2.2threading模块介绍

python提供给我们用来开启线程的模块,使用方法同moltiprocessing模块很相似

先有threading模块,后有multiprocessing模块,multiprocessing完全模仿threading模块完成,但是multiprocessing实现了池的功能(不常用)

关于池的常用模块:concurrent.futures模块,分别实现了进程池,线程池。

2.3 开启线程的两种方式

(1)使用threading中的Thread类开启线程

#方式一
from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.start()
    print('主线程')

(2)使用自定义线程类开启线程

#方式二
from threading import Thread
import time
class Sayhi(Thread):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        time.sleep(2)
        print('%s say hello' % self.name)


if __name__ == '__main__':
    t = Sayhi('egon')
    t.start()
    print('主线程')

tip:如果只开启线程不开启进程,不需要使用if __name__ == "__main__":
因为新线程和之前的主线程共享同一段代码,不需要通过imoprt导入文件,所以也就不存在重复执行线程中的启动线程部分代码。

2.4 多线程与多进程的区别

(1)线程的开启速度要比进程快,因为线程不需要向操作系统申请空间

from threading import Thread
from multiprocessing import Process
import os

def work():
    print('hello')

if __name__ == '__main__':
    #在主进程下开启线程
    t=Thread(target=work)
    t.start()
    print('主线程/主进程')
    '''
    打印结果:
    hello
    主线程/主进程
    '''

    #在主进程下开启子进程
    t=Process(target=work)
    t.start()
    print('主线程/主进程')
    '''
    打印结果:
    主线程/主进程
    hello
    '''
子线程开启速度快

(2)子线程会共用主进程的pid,但是每一个子进程都会有自己的pid

from threading import Thread
from multiprocessing import Process
import os

def work():
    print('hello',os.getpid())

if __name__ == '__main__':
    #part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
    t1=Thread(target=work)
    t2=Thread(target=work)
    t1.start()
    t2.start()
    print('主线程/主进程pid',os.getpid())

    #part2:开多个进程,每个进程都有不同的pid
    p1=Process(target=work)
    p2=Process(target=work)
    p1.start()
    p2.start()
    print('主线程/主进程pid',os.getpid())
pid

(3).统一进程内的线程共享该进程的数据

from  threading import Thread
from multiprocessing import Process
import os
def work():
    global n
    n=0

if __name__ == '__main__':
    # n=100
    # p=Process(target=work)
    # p.start()
    # p.join()
    # print('主',n) #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100


    n=1
    t=Thread(target=work)
    t.start()
    t.join()
    print('',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据
View Code

2.5线程的其他相关方法
Thread实例对象的方法

isAlive(): # 返回线程是否活动的。
getName(): # 返回线程名。
setName(): # 设置线程名。
current_thread(): # 返回当前线程的线程变量(线程名和线程id)
isAlive(): # 返回线程是否是活动的
线程中没有terminate方法,线程无法自己终止

threading模块提供的一些方法

1 currentThread(): # 返回当前的线程变量。
2 enumerate(): # 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
3 activeCount(): # 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

使用实例

from threading import Thread
import threading
from multiprocessing import Process
import os

def work():
    import time
    time.sleep(3)
    print(threading.current_thread().getName())


if __name__ == '__main__':
    #在主进程下开启线程
    t=Thread(target=work)
    t.start()

    print(threading.current_thread().getName())
    print(threading.current_thread()) #主线程
    print(threading.enumerate()) #连同主线程在内有两个运行的线程
    print(threading.active_count())
    print('主线程/主进程')

    '''
    打印结果:
    MainThread
    <_MainThread(MainThread, started 140735268892672)>
    [<_MainThread(MainThread, started 140735268892672)>, <Thread(Thread-1, started 123145307557888)>]
    主线程/主进程
    Thread-1
    '''
实例

线程中的join方法:等待某个子线程执行结束才执行后续的代码

def music():
    print('start to listen music %s'%time.ctime())
    time.sleep(3)
    print('-->stop to listen music %s'%time.ctime())

def movie():
    print('start to watch movie %s'%time.ctime())
    time.sleep(6)
    print('--<stop to watch movie %s'%time.ctime())


if __name__ == '__main__':
    t1 = threading.Thread(target=music)
    t1.start()
    t2 = threading.Thread(target=movie)
    t2.start()
    t2.join()  # 会等t2线程执行完,在执行后续代码
    print('ending... %s'%time.ctime())
join方法

2.6 守护线程

运行完毕和终止运行

无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁

需要强调的是:运行完毕并非终止运行

  • 对主进程来说,运行完毕指的是主进程代码运行完毕
  • 对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕

守护线程理解

对于进程:只会守护到主进程代码结束

  • 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束.

对于线程:会守护到所有其他非守护线程结束

  1. 主线程不需要回收子线程的资源,线程资源属于进程,所以进程结束了,线程的资源自然就被回收了
  2. 因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
  3. 主线程在其他非守护线程运行完毕后才算运行完毕,主线程结束了,主进程就跟着结束,所有资源被回收(守护线程在此时也被回收了)。
from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.setDaemon(True) #必须在t.start()之前设置
    t.start()

    print('主线程')
    print(t.is_alive())
    '''
    主线程
    True
    '''
设置守护线程

举例:

import time
import random
from threading import Thread,currentThread

dic = {}
def calc(num):
    t = currentThread() # 获取每个线程的线程对象
    time.sleep(random.random())
    dic[t.ident] = num**2  # 添加到字典的线程id顺序是随机的,无序的,顺序取决于谁先睡完,也就是执行完。

lis = [1,2,3,4,5,6,7,8,9,10]
t_l = []
for num in lis:
    t = Thread(target=calc,args=(num,))  # 这里是一个初始化的线程对象,没有启动<Thread(Thread-1, initial)>
    t.start()  # 这一步一定是有先后顺序的
    # print(t)
    t_l.append(t) # 添加线程对象到列表中的顺序是固定的,是按照lis中顺序的

for t in t_l:
    t.join()
    print(dic[t.ident])
实例

2.7 线程互斥锁

三个注意的点

  1. 线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来
  2. join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高
  3. 一定要看本小节最后的GIL与互斥锁的经典分析

GIL VS Lock

机智的同学可能会问到这个问题,就是既然你之前说过了,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 

首先我们需要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据

然后,我们可以得出结论:保护不同的数据就应该加不同的锁。

最后,问题就很明朗了,GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock

过程分析:所有线程抢的是GIL锁,或者说所有线程抢的是执行权限

线程1抢到GIL锁,拿到执行权限,开始执行,然后加了一把Lock,还没有执行完毕,即线程1还未释放Lock,有可能线程2抢到GIL锁,开始执行,执行过程中发现Lock还没有被线程1释放,于是线程2进入阻塞,被夺走执行权限,有可能线程1拿到GIL,然后正常执行到释放Lock。。。这就导致了串行运行的效果

既然是串行,那我们执行

t1.start()
t1.join
t2.start()
t2.join()

这也是串行执行啊,为何还要加Lock呢,需知join是等待t1所有的代码执行完,相当于锁住了t1的所有代码,而Lock只是锁住一部分操作共享数据的代码。

因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题,  这可以说是Python早期版本的遗留问题。 
详解
from threading import Thread
import os,time
def work():
    global n
    temp=n
    time.sleep(0.1)
    n=temp-1
if __name__ == '__main__':
    n=100
    l=[]
    for i in range(100):
        p=Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()

    print(n) #结果可能为99
例子

线程互斥锁使用

锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁:

from threading import Thread

R=Lock()
R.acquire()
'''
对公共数据的操作
'''
R.release()
from threading import Thread,Lock
import os,time
def work():
    global n
    lock.acquire()
    temp=n
    time.sleep(0.1)
    n=temp-1
    lock.release()
if __name__ == '__main__':
    lock=Lock()
    n=100
    l=[]
    for i in range(100):
        p=Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()

    print(n) #结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全
实例
分析:
  #1.100个线程去抢GIL锁,即抢执行权限
     #2. 肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire()
     #3. 极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL
    #4.直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程
GIL锁与同步锁综合分析
#不加锁:并发执行,速度快,数据不安全
from threading import current_thread,Thread,Lock
import os,time
def task():
    global n
    print('%s is running' %current_thread().getName())
    temp=n
    time.sleep(0.5)
    n=temp-1


if __name__ == '__main__':
    n=100
    lock=Lock()
    threads=[]
    start_time=time.time()
    for i in range(100):
        t=Thread(target=task)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()

    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:0.5216062068939209 n:99
'''


#不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全
from threading import current_thread,Thread,Lock
import os,time
def task():
    #未加锁的代码并发运行
    time.sleep(3)
    print('%s start to run' %current_thread().getName())
    global n
    #加锁的代码串行运行
    lock.acquire()
    temp=n
    time.sleep(0.5)
    n=temp-1
    lock.release()

if __name__ == '__main__':
    n=100
    lock=Lock()
    threads=[]
    start_time=time.time()
    for i in range(100):
        t=Thread(target=task)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:53.294203758239746 n:0
'''

#有的同学可能有疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,就不用加锁了啊,也是串行的效果啊
#没错:在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的,但问题是
#start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的
#单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.
from threading import current_thread,Thread,Lock
import os,time
def task():
    time.sleep(3)
    print('%s start to run' %current_thread().getName())
    global n
    temp=n
    time.sleep(0.5)
    n=temp-1


if __name__ == '__main__':
    n=100
    lock=Lock()
    start_time=time.time()
    for i in range(100):
        t=Thread(target=task)
        t.start()
        t.join()
    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 start to run
Thread-2 start to run
......
Thread-100 start to run
主:350.6937336921692 n:0 #耗时是多么的恐怖
'''
互斥锁与join的区别

2.8 死锁现象与递归锁

死锁现象(DeadLock)

进程也有死锁与递归锁,在进程那里忘记说了,放到这里一起说明

所谓死锁: 是指多个进程或线程在执行过程中,因争夺资源而造成的一种互相等待的僵局现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程/线程。

死锁的现实实例!

# 科学家吃面产生的死锁现象:
import time
from threading import Lock, Thread

noodle_lock = Lock()  # 面条锁
fork_lock = Lock()  # 叉子锁


def eat1(name):
    noodle_lock.acquire()   # 阻塞 宝元等面
    time.sleep(0.5)
    print('%s拿到面了'%name)
    fork_lock.acquire()
    print('%s拿到叉子了' % name)
    print('%s吃面'%name)
    fork_lock.release()
    print('%s放下叉子了' % name)
    noodle_lock.release()
    print('%s放下面了' % name)

def eat2(name):
    fork_lock.acquire()    # 阻塞 wusir等叉子
    time.sleep(0.5)
    print('%s拿到叉子了' % name)
    noodle_lock.acquire()
    print('%s拿到面了'%name)
    print('%s吃面'%name)
    noodle_lock.release()
    print('%s放下面了' % name)
    fork_lock.release()
    print('%s放下叉子了' % name)


# 三个线程(科学家)需同时获取叉子和面条才能吃到面条
Thread(target=eat1, args=('Annie',)).start()
Thread(target=eat2, args=('Lisa',)).start()
科学家吃面

解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

死锁产生的四个必要条件

  1. 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
  2. 不可剥夺:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
  3. 请求和保持:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
  4. 循环等待:进程之间形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所申请地资源。

递归锁

递归锁RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

上面的例子如果使用RLock代替Lock,则不会发生死锁:

mutexA=mutexB=threading.RLock() 

#一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止

 2.9 信号量Semaphore

同进程的一样,Semaphore管理一个内置的计数器,每当调用acquire()时内置计数器-1;调用release() 时内置计数器+1;计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):

from threading import Thread,Semaphore
import threading
import time
# def func():
#     if sm.acquire():
#         print (threading.currentThread().getName() + ' get semaphore')
#         time.sleep(2)
#         sm.release()
def func():
    sm.acquire()
    print('%s get sm' %threading.current_thread().getName())
    time.sleep(3)
    sm.release()
if __name__ == '__main__':
    sm=Semaphore(5)
    for i in range(23):
        t=Thread(target=func)
        t.start()
例子

注意:信号量与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是最大只允许几个线程/进程,但是是一直在创建不同的进程/线程。

互斥锁与信号量推荐博客:http://url.cn/5DMsS9r

2.10 线程中的队列Queue

queue队列 :使用import queue,用法与进程Queue一样

queue is especially useful in threaded programming when information must be exchanged safely between multiple threads.

先进先出队列

class queue.Queue(maxsize=0) 

import queue

q=queue.Queue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
结果(先进先出):
first
second
third
'''
View Code

堆栈-后进先出队列

class queue.LifoQueue(maxsize=0) #last in fisrt out 

from queue import LifoQueue  # 线程安全的列队
# 栈和后进先出的场景都可以用

lfq = LifoQueue()
lfq.put(1)
lfq.put('abc')
lfq.put({1,2,3})

print(lfq.get())
print(lfq.get())
print(lfq.get())

'''
结果
{1, 2, 3}
abc
1
'''
View Code

优先级队列

class queue.PriorityQueue(maxsize=0) 

from queue import PriorityQueue  # 优先级列队

#put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))

print(q.get()[1]) # 获得的也是一个元组,索引1的为元素本身
print(q.get())
print(q.get())
'''
结果(数字越小优先级越高,优先级高的优先出队):
('b') 
(20, 'a')
(30, 'c')
'''
View Code

2.11python标准模块--concurrent.futures

官方文档https://docs.python.org/dev/library/concurrent.futures.html

模块介绍

concurrent.futures模块提供了高度封装的异步调用接口

ThreadPoolExecutor:线程池,提供异步调用

ProcessPoolExecutor: 进程池,提供异步调用

Both implement the same interface, which is defined by the abstract Executor class.

方法介绍

1.获取进程池/线程池方法

  • pool = ProcessPoolExecutor(maxsize)  # 获取进程池对象
  • pool = ThreadPoolExecutor(maxsize)  # 获取线程池对象

2.进程/线程池对象基本方法 

submit(fn, *args, **kwargs)   # 异步提交任务,获得一个任务对象
task=pool.submit(func,*args,**kwargs)

map(func, *iterables, timeout=None, chunksize=1)
取代for循环submit的操作,返回一个包含结果的迭代器(惰性运算),可以直接for取值

shutdown(wait=True)  # 相当于进程池的pool.close()+pool.join()操作 
wait=True,等待池内所有任务执行完毕回收完资源后才继续 
wait=False,立即返回,并不会等待池内的任务执行完毕 
但不管wait参数为何值,整个程序都会等到所有任务执行完毕 submit和map必须在shutdown之前 

3.任务对象的方法

result(timeout=None)  # 取得结果
task.result()  # 获取任务对象的返回值(阻塞方法)

add_done_callback(fn)  # 回调函数

创建进程池

#介绍

The ProcessPoolExecutor class is an Executor subclass that uses a pool of processes to execute calls asynchronously. 
ProcessPoolExecutor uses the multiprocessing module, which allows it to side-step the Global Interpreter Lock but
also means that only picklable objects can be executed and returned. class concurrent.futures.ProcessPoolExecutor(max_workers=None, mp_context=None) An Executor subclass that executes
calls asynchronously using a pool of at most max_workers processes. If max_workers is None or not given, it will
default to the number of processors on the machine. If max_workers is lower or equal to 0, then a ValueError will
be raised.

 

import time
from concurrent.futures import ProcessPoolExecutor

def make(i):
    time.sleep(1)
    # print('%s制作%s'%(os.getpid(),i))
    return i**2

if __name__ == '__main__':
    p = ProcessPoolExecutor(4)  # 创建一个进程池
    # for i in range(1000):
    #     p.submit(make,i)  # 向进程池提交任务
    # p.shutdown()  # 阻塞 直到所有任务被进程处理完
    # print('所有的螺丝都制作完成了')
    # p.map(make,range(100))  # submit的简单用法,将可迭代对象中的每一个元素取出交给进程池中的进程,进程去调用make执行

    # 接受返回值(方法一)
    # ret_l = []
    # for i in range(100):
    #     ret = p.submit(make,i)
    #     ret_l.append(ret)  # 将结果异步添加到列表
    #
    # for r in ret_l:
    #     print(r.result())  # 遍历结果列表,打印返回值

    # 接受返回值(方法二)
    ret = p.map(make,range(100))  # 返回一个包含所有结果的生成器
    for i in ret:
        print(i)
用法

创建线程池

#介绍

ThreadPoolExecutor is an Executor subclass that uses a pool of threads to execute calls asynchronously. 
class concurrent.futures.ThreadPoolExecutor(max_workers=None, thread_name_prefix='') An Executor subclass
that uses a pool of at most max_workers threads to execute calls asynchronously. Changed
in version 3.5: If max_workers is None or not given, it will default to the number of processors
on the machine, multiplied by 5, assuming that ThreadPoolExecutor is often used to overlap I/O instead
of CPU work and the number of workers should be higher than the number of workers for ProcessPoolExecutor. New in version 3.6: The thread_name_prefix argument was added to allow users to control the threading.Thread
names for worker threads created by the pool for easier debugging.

用法与进程池一样

map的用法

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

import os,time,random
def task(n):
    print('%s is runing' %os.getpid())
    time.sleep(random.randint(1,3))
    return n**2

if __name__ == '__main__':

    executor=ThreadPoolExecutor(max_workers=3)

    # for i in range(11):
    #     future=executor.submit(task,i)

    executor.map(task,range(1,12)) #map取代了for+submit
map

回调函数

对线程的返回结果调用某个方法进行处理。

使用场景

  1. 不能有多少个任务就开多少个进程,开销过大
  2. 用有限的进程执行无限的任务,多个被开启的进程重复利用,节省的是开启/销毁/多个进程切换的时间
import time
import random
from concurrent.futures import ProcessPoolExecutor
def func1(n):
    time.sleep(random.random())
    print('in func1 %s'%n)
    return n**2

def call_back(args):
    print(args.result()*2,args)

if __name__ == '__main__':
    p = ProcessPoolExecutor(4)
    for i in range(51):
        ret = p.submit(func1,i)  # 获得线程池调用func1处理i的结果
        ret.add_done_callback(call_back)  # 将结果立即交给call_back处理

    ret_l = []
    for i in range(10):
        ret = p.submit(func1,i) # 将结果异步添加到列表
        ret_l.append(ret)

    for r in ret_l: 遍历结果列表,打印返回值
        call_back(r)

'''
    这种方法,是异步发起了10个函数,每个函数去完成自己的功能,每个函数完成自己任务的时间是不同的,
    但是任务添加到列表中的顺序是依次的,而for循环执行call_back函数,必须列表中的任务挨个执行call_back函数,
    所以虽然任务被执行完顺序不是提交顺序,但是处理完任务走人的顺序是提交任务的顺序,也就是谁先被发起任务,谁必定先调用call_back方法。
    这种效率不如上面的回调函数效率
'''
回调函数

 

posted @ 2019-09-15 10:31  changzd123  阅读(197)  评论(0编辑  收藏  举报