python之路——多线程(线程锁、递归锁、信号量、事件、队列等)

时间:2022-12-11 15:11:34

进程与线程

什么是进程?

程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

什么是线程?

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务

进程与线程的区别:

1、线程之间可以共享由进程创建的地址空间,进程有自己单独的地址空间;

2、线程直接访问进程的数据;进程只能从父进程那里复制数据;

3、线程可以直接与其进程的其他线程进行通信; 进程必须使用进程间通信来与兄弟进程进行通信;

4、新线程很容易创建; 新进程需要由父进程复制;

5、线程可以对相同进程的线程进行控制; 进程只能控制子进程;

6、对主线程的更改(取消,优先级更改等)可能会影响进程的其他线程的行为; 对父进程的更改不会影响子进程

GIL锁

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中,全局解释器锁(GIL)是互斥的,Python在执行的时候会淡定的在同一时刻只允许一个线程运行。 这个锁是必需的,因为CPython的内存管理不是安全的。 (但是,由于GIL存在,其他功能的发展必须依赖于保证其实施)

首先需要明确的一点是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

线程锁(互斥锁Mutex)

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

import threading,time
num = 0
def sum():
global num
num +=1
time.sleep(1)
for i in range(100):
t = threading.Thread(target=sum)
t.start()
print(num)

结果是100,但是在2.7x版本上,由于线程是并发,会发生两个线程同时取到num = 0这个值,运算得到1,这样最后结果会是99而不是100。可以在每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。

import threading,time
lock = threading.Lock() #首先要先实例化一个锁
num = 0
def sum():
global num
lock.acquire() #获得锁
num +=1
# time.sleep(1)
lock.release() #计算完释放锁

for i in range(100):
t = threading.Thread(target=sum)
t.start()
# t.join()
print(num)

结果也是100,如果加上time.sleep(1),结果就是1,但是同时加上t.join(),结果是等100s后显示结果为100

GIL VS Lock 

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

python之路——多线程(线程锁、递归锁、信号量、事件、队列等)

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

递归锁

就是在一个大锁中还要再包含子锁

import threading
lock = threading.RLock()
num = 0
def sum1():
lock.acquire()
global num
num += 1
print("from sum1")
lock.release()
return num

def sum2():
lock.acquire()
res = sum1()
lock.release()
print(res)

for i in range(10):
t = threading.Thread(target=sum2)
t.start()

信号量(Semaphore

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

import threading,time

def talk(name):
semaphone.acquire() #设置获得信号量
print(name,"is talking......")
time.sleep(1)
semaphone.release() #释放信号量

semaphone = threading.BoundedSemaphore(3) #最多三个线程同时并发
for i in range(10):
T = threading.Thread(target=talk,args=(i,))
T.start()

print("main.....")

  结果是

0 is talking......
1 is talking......
2 is talking......
main.....

1s后。。。。。。

3 is talking......
4 is talking......
5 is talking......

1s后。。。。。。

事件(events)

事件需先实例化一个事件对象-------》event = threading.Event()

事件有以下几个方法:-------》event.wait()    等待标志位被设置

            --------》event.set()    设置一个标志位

            --------》event.clear()  清楚已设置的标志位

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

import threading,time

event = threading.Event() #实例化一个事件对象
def light():
count = 0
event.set() #设置红灯
while True:
if 0 < count <= 5 : #0到5秒,绿灯,清除设置
event.clear()
print("\033[1;32m现在是绿灯,可以通行\033[0m")
elif 5 < count <10: #5s后,绿灯变红灯,设置
event.set()
print("\033[1;35m现在是红灯,禁止通行\033[0m")
elif count == 10: #10s后,红灯结束,count设置为0重新开始
count = 0
time.sleep(1)
count += 1

def car(name):
while True:
if event.is_set():
print(name,"红灯,等待。。。。。。。。。。")
time.sleep(2)
else:
print(name,"绿灯,可以通行。。。。。。。。")
time.sleep(2)

L = threading.Thread(target=light,) #红绿灯线程
L.start()
C = threading.Thread(target=car,args=("兰博基尼",)) #车辆通行线程
C.start()

队列(queue)

1、首先队列有三种类型:

  class queue.Queue(maxsize=0) #先入先出
   class queue.LifoQueue(maxsize=0) #last in fisrt out 
   class queue.PriorityQueue(maxsize=0) #存储数据时可设置优先级的队列
2、队列的方法:
  Queue.put(item, block=True, timeout=None)
  Queue.get(block=True, timeout=None)
import queue
Q1 = queue.Queue(maxsize=0) #先入先出,maxsize设置队列容量
Q1.put("alex")
Q1.put("japhi")
print(Q1.qsize()) #结果是2
print(Q1.get()) #结果是alex
# Q1.get()
# Q1.get(block=True,timeout=1) #队列容量是2,取三次取不到会卡主,可以设置block,在取不到时报错,timeout设置等待时间
print("--------------------")
Q2 = queue.LifoQueue(maxsize=0) #后入先出
Q2.put("alex")
Q2.put("japhi")
print(Q2.qsize()) #结果是2
print(Q2.get()) #结果是japhi
print("--------------------")
Q3 = queue.PriorityQueue() #设置队列优先级
Q3.put(("a","alex")) #按照abc的优先级设置
Q3.put(("c","japhi"))
Q3.put(("b","jason"))
print(Q3.qsize()) #结果是2
print(Q3.get()) #结果是alex
print(Q3.get()) #结果是jason
print(Q3.get()) #结果是japhi

生产者消费者模型

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

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

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

什么是生产者消费者模式

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

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

import threading,time,queue

q = queue.Queue(3)
def producer():
i = 0
while True:
print("生产包子",i)
q.put(i)
i += 1
time.sleep(0.5)

def cousmer(name):
while True:
print("%s正在吃包子%s..."%(name,q.get()))

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

c = threading.Thread(target=cousmer,args=("alex",))
c.start()

c1 = threading.Thread(target=cousmer,args=("japhi",))
c1.start()

定时任务(Timer)

可以通过Timer设置时间,来控制线程的运行:

import threading
def say():
print("hello world.....")

t = threading.Timer(2,say) #定时器
t.start()

2s后输出结果 hello world......