Python自动化 【第九篇】:Python基础-线程、进程及python GIL全局解释器锁

时间:2021-07-25 01:54:53

本节内容:

  1. 进程与线程区别
  2. 线程
  • a)  语法
  • b)  join
  • c)  线程锁之Lock\Rlock\信号量
  • d)  将线程变为守护进程
  • e)  Event事件 
  • f)   queue队列
  • g)  生产者消费者模型

  3. python GIL全局解释器锁

1. 进程与线程区别

  线程:是操作系统能够进行运算和调度的最小单位,是一堆指令的集合。线程被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程就是cpu执行时所需要的一堆上下文关系。

  进程:以一个整体的形式暴露给操作系统管理,里面包含对各种资源的调用,内存的管理,网络接口的调用等,对各种资源管理的集合就可称为 进程。进程要操作cpu, 必须先创建一个线程。

  进程和线程的区别:

  • 线程共享内存空间,进程的内存是独立的
  • 线程共用数据,进程数据独立
  • 同一个进程的线程之间可以直接交流,两个进程必须通过中间代理实现通信
  • 新线程容易创建,新进程需要克隆父进程
  • 一个线程可以控制和操作同一进程里的其他线程, 进程只能操作子进程

  修改主线程有可能影响到其他线程的行为,对父进程修改不会影响子进程。

2. 线程(threading模块)

a)  语法

  先写一个简单的线程:

  
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",))
t1.start()
t2.start()

print(t1.getName) #获取线程名

print(t2.getName)

  继承式调用:

  
import threading
import time

class MyThread(threading.Thread):
    def __init__(self, num):
        threading.Thread.__init__(self)
        self.num = num

    def run(self):  # 定义每个线程要运行的函数

        print("running on number:%s" % self.num)

        time.sleep(3)

if __name__ == '__main__':
    t1 = MyThread(1)
    t2 = MyThread(2)
    t1.start()
    t2.start()

继承式调用

  启动多个线程:

  
import threading

import time

def run(n):

    print("task" ,n)

    time.sleep(2)

for i in range(50):

    t = threading.Thread(target=run, args=("t_%s" % i,))

    t.start()

启用多线程

b)  join 

  join & Daemon用法:

  默认情况主线程不会等子线程执行完毕,但是join可以做到

  
import threading

import time

def run(n):

    print("task" ,n)

    time.sleep(2)

    print("task done ", n)

start_time = time.time()

t_objs = []

for i in range(50):

    t = threading.Thread(target=run, args=("t_%s" % i,))
   t.start()

    t_objs.append(t)

for t in t_objs:

    t.join()

print("cost_time:",time.time()-start_time)

join用法

  主线程是程序本身

  

     threading.current_thread()和threading.active_count()用法:

  
import threading

import time

def run(n):

    print("task" ,n)

    time.sleep(2)

    print("task done ", n,threading.current_thread())

start_time = time.time()

t_objs = []

for i in range(50):

    t = threading.Thread(target=run, args=("t_%s" % i,))

    t.start()

    t_objs.append(t)

# for t in t_objs:

#     t.join()

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

print("cost_time:",time.time()-start_time)

threading.current_thread()和threading.active_count()

c)  信号量:

  threading.BoundedSemaphore(n)

  互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 。

  threading.BoundedSamphore(n)

  

d) 把子线程变成守护线程setDaemod()方法:

  
#!/usr/bin/env python

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

# Author: zhoujunlong

import threading

import time

def run(n):

    print("task" ,n)

    time.sleep(2)

    print("task done ", n,threading.current_thread())

start_time = time.time()

for i in range(50):

    t = threading.Thread(target=run, args=("t_%s" % i,))

    t.setDaemon(True) # 把当前线程设置为守护线程 , 在start之前

    t.start()

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

print("cost_time:",time.time()-start_time)

set Daemod()

e)  事件:

  事件是一个简单地同步对象

  event = threading.event()

  event.wait() 等待标志位被设定

  event.set() 设置标志位

  event.clear() 清除标志位

  event.is_set() 判断标志位是否设定

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

  
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: zhoujunlong
import threading
import time
event = threading.Event()
def lighter():
    count = 0
    event.set()
    while True:
        if 10>=count > 5:#change_to_red_light
            event.clear() # 清空标志位
            print("\033[41;1m红灯了\033[0m")
        elif count > 10:
            event.set() #change_to_green_light
            count = 0
        else:print("\033[42;1m绿灯了\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" % name)
            event.wait()
            print("\033[34;1m[%s] 绿灯了,gogogo\033[0m"%name)
light = threading.Thread(target=lighter)
light.start()
car1 = threading.Thread(target=car,args=("QQ",))

car2 = threading.Thread(target=car,args=("TT",))
car1.start()
car2.start()

示例1-红绿灯

  这里还有一个event使用的例子,员工进公司门要刷卡, 我们这里设置一个线程是“门”, 再设置几个线程为“员工”,员工看到门没打开,就刷卡,刷完卡,门开了,员工就可以通过。

  
import threading
import time
import random

def door():
    door_open_time_counter = 0
    while True:
        if door_swiping_event.is_set():
            print("\033[32;1mdoor opening....\033[0m")
            door_open_time_counter +=1
        else:
            print("\033[31;1mdoor closed...., swipe to open.\033[0m")
            door_open_time_counter = 0 #清空计时器
            door_swiping_event.wait()
        if door_open_time_counter > 3:#门开了已经3s了,该关了
            door_swiping_event.clear()
        time.sleep(0.5)

def staff(n):
    print("staff [%s] is comming..." % n )
    while True:
        if door_swiping_event.is_set():
            print("\033[34;1mdoor is opened, passing.....\033[0m")
            break
        else:
            print("staff [%s] sees door got closed, swipping the card....." % n)
            print(door_swiping_event.set())
            door_swiping_event.set()
            print("after set ",door_swiping_event.set())
        time.sleep(0.5)
door_swiping_event  = threading.Event() #设置事件
door_thread = threading.Thread(target=door)
door_thread.start()
for i in range(5):
    p = threading.Thread(target=staff,args=(i,))
    time.sleep(random.randrange(3))
    p.start()

示例2-员工进门刷卡

f)  queue(队列)

  class queue.Queue(maxsize=0) #先入先出

  class queue.LifoQueue(maxsize=0) #先入先出 #last  in  first  out

  class queue.PriorityQueue(maxsize=0) #存储数据时可设置优先级的队列

  • 实现程序的解耦
  • 提高运行效率
  
>>> import queue

>>> q = queue.Queue

>>> q.put("disk1")

>>> q.put("disk2")

>>> q.put("disk3")

>>q.qsize()

3

>>>q.get()

'disk1'

>>>q.get()

'disk2'

>>>q.get()

'disk3'

>>>q.get_nowait() 

queue

  抛异常,不会卡住

  maxsize()方法:

  
>>>q = queue.Queue(maxsize=3)

>>>q.put(1)

>>>q.put(2)

>>>q.put(3)

>>>q.put(4)

maxsize()方法

  再put时会卡住

  Lifo

  
import queue
q = queue.LifoQueue()
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())

Lifo

  程序输出:

  
3

2

1

程序输出

  Priority方法:

  
import queue
q = queue.PriorityQueue()
q.put(("-1a1"))
q.put(("5, a2"))
q.put(("2, a3"))
print(q.get())
print(q.get())
print(q.get())

Priority方法

  程序输出:

  
-1, a1

2, a3

5, a2

程序输出

  Queue.task_done() 以下为解释:

  Indicate that a formerly enqueued task is complete. Used by queue consumer threads. For each get() used to fetch a task, a subsequent call to task_done() tells the queue that the processing on the task is complete.

  If a join() is currently blocking, it will resume when all items have been processed (meaning that a task_done() call was received for every item that had been put() into the queue).

  Raises a ValueError if called more times than there were items placed in the queue.

g)  生产者消费者模型

  在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。

  为什么要使用生产者和消费者模式?

  在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

  什么是生产者消费者模式?

  生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

  下面来学习一个最基本的生产者消费者模型的例子:

  
import threading
import queue

def producer():
    for i in range(10):
        q.put("骨头 %s" % i )

    print("开始等待所有的骨头被取走...")
    q.join()
    print("所有的骨头被取完了...")

def consumer(n):
    while q.qsize() >0:
        print("%s 取到" %n, q.get())
        q.task_done() #告知这个任务执行完了

q = queue.Queue()

p = threading.Thread(target=producer,)
p.start()

c1 = consumer("Jack")

示例1-生产者消费者模型

  再来一个:

  
import time,random
import queue,threading
q = queue.Queue()
def Producer(name):
  count = 0
  while count <20:
    time.sleep(random.randrange(3))
    q.put(count)
    print('Producer %s has produced %s baozi..' %(name, count))
    count +=1
def Consumer(name):
  count = 0
  while count <20:
    time.sleep(random.randrange(4))
    if not q.empty():
        data = q.get()
        print(data)
        print('\033[32;1mConsumer %s has eat %s baozi...\033[0m' %(name, data))
    else:
        print("-----no baozi anymore----")
    count +=1
p1 = threading.Thread(target=Producer, args=('A',))
c1 = threading.Thread(target=Consumer, args=('B',))
p1.start()
c1.start()

示例2

  

3.  GIL全局解释器锁(面试必会)

   Python中无论几核,同一时间只有一个线程在执行。

  线程锁(互斥锁)

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

  
import threading

import time

def run(n):

    global num

    time.sleep(2)

    num += 1

num = 0

t_objs = []

for i in range(1000):

    t = threading.Thread(target=run, args=("t_%s" % i,))

    t.start()

    t_objs.append(t)

for t in t_objs:

    t.join()

print("===all threads has finished")

print("num:", num)

线程锁

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

 

  为了让上边代码每次都输出num是1000的话,必须再加一把锁:

  
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)

if __name__ == '__main__':

    num, num2 = 0, 0
    lock = threading.RLock()
    for i in range(10):
        t = threading.Thread(target=run3)
        t.start()

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

输出num为1000

  加上锁后程序就变串行了,为了避免程序变慢,别在子线程里加sleep等类似操作!

  GIL VS Lock 

  既然Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock?注意,这里的lock是用户级的lock,跟那个GIL没关系 ,具体我们通过下图来看一下:

Python自动化 【第九篇】:Python基础-线程、进程及python GIL全局解释器锁

  递归锁:

  threading.RLock()不会出现锁死情况,说白了就是在一个大锁中还要再包含子锁。

  
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)

if __name__ == '__main__':

    num, num2 = 0, 0
    lock = threading.RLock()
    for i in range(10):
        t = threading.Thread(target=run3)
        t.start()

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

threading.RLock()