Python实战之线程(函数多线程,类多线程,守护线程,GIL,线程lock,递归Rlock,Semaphore信号量,event)

首先须知

什么是IO?

从硬盘读块数据,从网络读块数据属于IO操作(IO操作不占用cpu)

计算占用cpu,如:1+1

Python多线程其实就是一个线程,由于利用CUP的上下文切换看起来就像是并发..上下文切换消耗资源

Python多线程 不适合CPU密集操作型的任务,适合IO操作密集型的任务

大量运算占CPU尽量少用多线程,用单进程更快

sockeserver接收多个网络并发的就是IO操作密集型的

如果一定要使用CPU密集操作型的任务呢?Python怎么解决?

使用多进程来解决CPU密集操作型的任务。

Python的线程是调用操作系统的原生线程,进程也是调用操作系统的原生进程,原生进程是由操作系统自己维护的。Python只是调用了C代码库的一个接口启动进程的,真正的进程管理还是操作系统自己完成的

线程摘要

1.线程:是操作系统最小的调度单位,是一串指令的集合,被包含在进程里面

扫描二维码关注公众号,回复: 4788825 查看本文章

2. 多线程共享创建他的进行的内存地址空间

3.同时间一核CPU只能执行一个程序,为什么可以同时上QQ和打游戏,因为利用上下文几率进行切换的,CPU速度太快所以我们肉眼感觉是同一时间执行的

4.GIL全局解释器锁称之吉尔,是一个互斥锁,禁止多个本地线程执行Python字节码(是Python的缺陷)

5.主线程可以创建子线程  子线程可以继续创建线程  但是主线程与子线程都是独立的个体

6.进程和线程哪个快?没有可比性...因为进程执行是需要线程执行的 硬要说 一样快

7 启动进程和线程哪个快?线程就一堆指令所以线程当然快

 

线程的两种方式(类,函数)

最简单的线程试验:

[root@python Part_nine]# vim shreading_01.py
__author__ = "Burgess Zheng"

import threading
import time

def run(n):#该函数当成一个任务
    print("task",n)
    time.sleep(2)#增加间隔时间测试进程启动和线程启动的效率

#启动两个线程执行上面的任务
t1 = threading.Thread(target=run,args=("t1",)) #实例化线程
t2 = threading.Thread(target=run,args=("t2",))#实例化线程
#启动两个线程 target:执行目标:run该函数 ,args实参:("tx",) 记住哪怕一个实参也需要逗号,

#线程执行
t1.start()
t2.start()

#进程执行
run("t1")
run("t2")

#最后结果,看不出都是一瞬间就显示出来,归功于CPU运算快过肉眼
#但是如果加上sleep间隔时间就可以看出线程和进程之前的区别

测试结果:

类方式线程试验:

__author__ = "Burgess Zheng"

import threading
import time

#类的方式 多线程启动试验#一般不这么写 一般都是已函数的方式写

class MyThread(threading.Thread):
    def __init__(self,n,sleep_time):
        super(MyThread,self).__init__()
        #由于把父类的构造函数重写了就需要这条语句继承父类的构造函数
        self.n = n
        self.sleep_time = sleep_time
    def run(self):#类里面被多线程执行的函数必须叫做run
        print("runnint task",self.n)
        time.sleep(self.sleep_time)
        print("task done:",self.n)

'''
t1 = MyThread("t1")
t2 = MyThread("t2")
t1.start()
t2.start() #两个线程同执行
'''

'''
t1 = MyThread("t1")
t2 = MyThread("t2")
t1.start()
t1.join()
    #python语言:join = wait  等的意思
    #把并行变成了串行,等t1.start()该线程执行完毕以后才执行t2线程和主线程
t2.start()
'''

'''
t1 = MyThread("t1")
t2 = MyThread("t2")
#我要让主线程等所有的子线程执行完毕在执行主线程
t1.start()
t2.start()
t1.join()
#等上面的线程全部执行完毕了以后,主线程才往下执行
#但是记住只是等t1执行完毕了以后,主线程就继续往下执行
#如果此时t2线程的执行时间>t1线程的执行时间,主线程不会等t2执行完毕在继续执行
print("sfasdf")
'''

#往类构造函数增加多一个实参(时间)测试线程时间不同主进程等等的时间
t1 = MyThread("t1",2)
t2 = MyThread("t2",4)
t1.start()
t2.start()
t1.join()
print("sfasdf")
#结果主线程只等到t1线程结束以后继续执行,不会等待t2线程后才执行
#所以测试执行用时的时候,只显示执行2秒,那是t1的执行用时间
#所以你如果要整个子线程执行用时计算出来
# 就应该t2.join()这样主线程才会等待2个子线程执行完才执行,计算的用时就是4秒

测试结果:

函数for循环启动多线程试验(主线程和子线程的执行原理)  

_author__ = "Burgess Zheng"

import threading
import time

def run(n):#该函数当成一个任务
    print("task",n)
    time.sleep(2)#增加间隔时间测试进程启动和线程启动的效率
    print("task done")


'''
#for循环启动多个线程,总不能我要启动一个线程就复制一个
start_time = time.time()
for i in range(10):
    t = threading.Thread(target=run,args=("t%s" %i,)) #实例化线程
    t.start()
print("---------------all threads has finished")
print("cost:",time.time() - start_time)
#线程执行的时候结果每个线程同时先执行了函数run 第一条命令print("task",n)
#但是主线程不会等子线程执行完毕在执行,而是直接继续往下执行
      #print("---------------all threads has finished")
      #print("cost:",time.time() - start_time)
#主线程启动了子线程后,子线程和主线程是独立的,主线程不会等子线程执行完在执行
#主线程和子线程执行是并行了,同时执行的,分别处理自己的事情,互补干扰
'''

#默认主线程是不会等子线程执行完在执行的,
# 但是如果有特定的场景需要计算全部子线程执行完毕的时间
# 可以利用参数做到
start_time = time.time()
t_objs = []#生产个空列表
for i in range(10):
    t = threading.Thread(target=run,args=("t%s" %i,)) #实例化线程
    t.start()
    #t.join() #如果在这里使用join就会导致启动1个线程就得到结果在启动另外一个线程就是串行了
              #而我们现在的要求是一次性50个线程进行join,需要在外面写个循环
    t_objs.append(t) #把t循环结果值追加到t_objs该列表

for t in t_objs:
    t.join()

print("---------------all threads has finished")
print("cost:",time.time() - start_time)
#这样主线程就会等所有子线程执行完毕在继续执行

测试结果:

    

结果:主线程只负责调用子线程,但是不会等待子线程执行后才执行主线程和子线程是同时并行执行的,如果注线程需要等子线程执行完才执行需要用到命令x.join()

守护线程试验(守护线程的原理)

__author__ = "Burgess Zheng"

import threading
import time

def run(n):
    print("task ",n )
    time.sleep(2)
    print("task done",n,threading.current_thread())
                            #threading.current_thread():查看是主线程执行还是子线程执行

start_time = time.time()
t_objs = [] #存线程实例
for i in range(5):
    t = threading.Thread(target=run,args=("t-%s" %i ,)) #实例化线程
    t.setDaemon(True)
#.setDaemon(True) 把当前线程设置成守护线程
# 这样主线程执行完毕,不会在乎子进程是否执行完毕,直接退出整个程序
    t.start()
    t_objs.append(t) #为了不阻塞后面线程的启动,不在这里join,先放到一个列表里

print("---all threads has finished...",threading.current_thread(),threading.active_count())
                                           #threading.current_thread() :查看执行的线程是主线程还是子
进程
                                           #threading.active_count()当前活动线程的个数
print("cost:",time.time() - start_time)

测试结果:

守护线程:主线程执行完毕直接程序关闭,不会在乎子线程是否已经执行完毕了。 我们清楚主线程只是调用子线程取执行但是和子线程是并行执行的,不存在所谓主线程等待子线程

应用场景:一个php程序,每个用户连接进来php程序会启动一个线程,php本身就是程序,启动的时候就是一个守护进程,进程是无法执行的而是依靠主线程去执行的,我总不能把PHP服务关闭掉 线程还继续执行吧?

setDaemon(True) 把当前线程设置成守护线程

threading.current_thread():查看执行的线程是主线程还是子进程

threading.active_count():当前活动线程的个数

threading.get_ident():显示线程号

 

Python GIL

Python GIL(Global Interpreter Lock)  

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

全局解释器锁在CPython,或吉尔,是一个互斥锁,防止多个本地线程执行Python字节码。这把锁是必要的,主要是因为CPython的内存管理不是线程安全的。(然而,由于吉尔存在,其他功能已经习惯于依赖保证执行)

上面的核心意思就是,无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行,擦。。。,那这还叫什么多线程呀?莫如此早的下结结论,听我现场讲。

首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL

Cpython GIL的缺陷

无论多少个CPU 多少个线程,只调用一个线程执行,其余的就候着不做事

python是C语言写的 

Python的解释器是调用C语言的接口

GIL只是Cpython的缺陷,并不是整个python的缺陷 未来的趋势PyPy

Cpython去掉GIL是不可能的。但是有折中的办法后面会讲

 

线程锁(互斥锁Mutex)

全局GIL锁的原理,虽然也是保证1个线程修改该数据,为什么还需要线程锁?但是因为多线程对该数据进行了copy修改,最后才覆盖得到结果,很多时候还是导致出问题

一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况?

正常来讲,这个num结果应该是0 但在python 2.7上多运行几次,会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢? 哈,很简单,假设你有A,B两个线程,此时都 要对num 进行减1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=100这个初始变量交给cpu去运算,当A线程去处完的结果是99,但此时B线程运算完的结果也是99,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是99。那怎么办呢? 很简单,每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。 

*注:不要在3.x上运行,不知为什么,3.x上的结果总是正确的,可能是自动加了锁

 

线程锁实验:

#-*- coding:utf-8 -*-
__author__ = "Burgess Zheng"

import threading
import time

def run(n):
    lock.acquire()#获取一把锁
    global num#修改全局变量
    num +=1
    #time.sleep(1)
#记住加锁以后只有一个线程修改才释放
# 如果sleep的话,有所少个线程操作就要等多少秒,有锁的时候最好别使用sleep
    lock.release()#释放锁

lock = threading.Lock()#全局:生成锁的实例
num = 0
t_objs = [] #存线程实例
for i in range(5):
    t = threading.Thread(target=run,args=("t-%s" %i ,)) #实例化线程
    t.start()
    t_objs.append(t) #所有线程假如列表
#所有线程都对run该函数进行了修改,但是Python3得到num结果正常,可能自动加锁了,
# Python2就出问题需要手动加锁#针对Python2的问题进行加锁

for t in t_objs: #循环线程实例列表,等待所有线程执行完毕
    t.join()

print("----------all threads has finished...",threading.current_thread(),threading.active_count())

print("num:",num)

GIL VS Lock 

就是既然你之前说过了,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 注意啦,这里的lock是用户级的lock,跟那个GIL没关系 ,具体我们通过下图来看一下+配合我现场讲给大家,就明白了。

那你又问了, 既然用户程序已经自己有锁了,那为什么C python还需要GIL呢?加入GIL主要的原因是为了降低程序的开发的复杂度,比如现在的你写python不需要关心内存回收的问题,因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题,  这可以说是Python早期版本的遗留问题。

RLock(递归锁)

说白了就是在一个大锁中还要再包含子锁就需要Rlock 而是不lock 如果lock就会导致解锁出现问题,变死循环

简单一点就是 如果多线程调用一个任务而该函数是有lock锁的 但是该函数里面又执行调用另外的函数,另外的函数也有lock锁,就会导致解锁的时候出现死循环

解决:全局生成锁的时候使用RLock递归锁

 

递归锁实验:

#-*- coding:utf-8 -*-
__author__ = "Burgess Zheng"

import threading, time


def run1():
    print("grab the first part data")
    lock.acquire()
    global num
    num += 1
    lock.release()
    return num


def run2():
    print("grab the second part data")
    lock.acquire()
    global num2
    num2 += 1
    lock.release()
    return num2


def run3():
    lock.acquire()
    res = run1()
    print('--------between run1 and run2-----')
    res2 = run2()
    lock.release()
    print(res, res2)


num, num2 = 0, 0
lock = threading.RLock()#RLock当出现多重锁的时候就需要用Rlock#否则出错死循环
for i in range(3):
    t = threading.Thread(target=run3) #循环执行run3,3次
                                  #实例化线程
    t.start()

while threading.active_count() != 1:
    print(threading.active_count())
else:
    print('----all threads done---')
    print(num, num2)

测试结果:

Semaphore(信号量)

互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。

Semaphore实验:

__author__ = "Burgess Zheng"

import threading, time


def run(n):
    semaphore.acquire()#信号量获取多把锁
    time.sleep(1)
    print("run the thread: %s\n" % n)
    semaphore.release()#信号量释放多把锁

if __name__ == '__main__':
    semaphore = threading.BoundedSemaphore(5)  # 最多允许5个线程同时运行
    for i in range(10):
        t = threading.Thread(target=run, args=(i,)) #实例化线程
        t.start()
while threading.active_count() != 1:#根join效果是一样的,线程不等于1的时候不往下走
    pass  # print threading.active_count()
else:
    print('----all threads done---')
    #print(num)

测试结果:

Events

An event is a simple synchronization object;

一个事件是一个简单的同步对象;

the event represents an internal flag, and threads

can wait for the flag to be set, or set or clear the flag themselves.

事件代表一个内部标记,和线程
可以等待标志被设置,或者设置或清除标志本身。

event = threading.Event()

event.wait()# a client thread can wait for the flag to be set客户端线程等待标记设置

event.set()# a server thread can set or reset it一个服务器线程可以设置或重置它

event.clear()

If the flag is set, the wait method doesn’t do anything.

如果设置了标记,方法不做任何事。

If the flag is cleared, wait will block until it becomes set again.

如果标志被清除,等待会阻塞,直到它再次被设置。

Any number of threads may wait for the same event.

任意数量的线程可能等待相同的事件。

通过Event来实现两个或多个线程间的交互,下面是一个红绿灯的例子,即起动一个线程做交通指挥灯,生成几个线程做车辆,车辆行驶按红灯停,绿灯行的规则

Event的作用就是标志位,如果有标志位代表通行,如果没有标志位代表阻塞

Event试验:

__author__ = "Burgess Zheng"

import time
import threading


event = threading.Event()

def lighter():
    count = 0
    event.set()#设置标志位
    while True:
        if count > 5 and count < 10:#大于5 小于10 清空标志位,变阻塞
            event.clear()#把标志位清空了,变阻塞
            print("\033[41;1mred light is on...\033[0m")
        elif count >10:#大于10就改成绿灯
            event.set()#设置标志位,变通行
            count = 0#清空标志位
        else:
            print("\033[42;1mgreen light is on...\033[0m")
        time.sleep(1)
        count +=1

def car(name):
    while True:
        if event.is_set():#判断标志位的设置状态,设置代表通行,循环执行下面命令
            print("[%s] running..." % name)
            time.sleep(1)#设置才看的出效果
        else:#如果没有标志位就循环执行下面的命令
            print("[%s] sees red light,waiting..")
            event.wait()#等待标志位,如果没有标志位,就等到有标志位才继续循环执行
            print("\033[34;1m[%s] green light is on ,start going..\033[0m" %name)

light = threading.Thread(target=lighter,)#实例化线程
light.start()
car1 = threading.Thread(target=car,args=("Tesla",))#实例化线程
car1.start()

测试结果:

 

 

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/Burgess_zheng/article/details/85791696
今日推荐