[并发编程 - 多线程:信号量、死锁与递归锁、时间Event、定时器Timer、线程队列、GIL锁]

信号量

信号量Semaphore:管理一个内置的计数器

每当调用acquire()时内置计数器-1;

调用release() 时内置计数器+1;

计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

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

可以把信号量理解为一种锁

相当于公共厕所,门口挂了5把钥匙,对应五个坑位,代表同时最多可以接纳五个人上厕所

from threading import Thread,Semaphore,current_thread

import time

import random

每个线程运行起来都会运行func

def func():

sm.acquire() # 运行func都会来抢这把锁

print('%s is wcing' %current_thread().getName())

time.sleep(random.randint(1,5)) # 模拟上厕所的时间

sm.release()

if name == 'main':

sm = Semaphore(5) # 创建信号量,自定义为5,相当于5把钥匙得到信号量对象

for i in range(23): # for循环了23次,开了23个线程

t = Thread(target=func)

t.start()

"""

ps:互斥锁只能acquire一次,再有人来执行acquire,如果没有释放,下一个来拿的人就只能阻在原地无法拿到acquire。而信号量一把锁可以acquire指定5次(Semaphore(5)),如果第6个来在

acquire的时候就没有了,相当于没有钥匙了,就只能在原地等着,只要5个人里面有人释放后面的人就

可以拿到钥匙

"""

与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程

死锁与递归锁

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

from threading import Thread,Lock

import time

mutexA = Lock()

mutexB = Lock()

class Mythread(Thread):

def init(self,name):

super().init()

self.name = name

def run(self) -> None:
self.f1()
self.f2() def f1(self):
mutexA.acquire()
print("%s 抢到了A锁" %self.name) mutexB.acquire()
print("%s 抢到了B锁" %self.name)
mutexB.release() mutexA.release() def f2(self):
mutexB.acquire()
print("%s 抢到了B锁" % self.name)
time.sleep(0.1) mutexA.acquire()
print("%s 抢到了A锁" % self.name)
mutexA.release() mutexB.release()

if name == 'main':

t1 = Mythread("线程1")

t2 = Mythread("线程2")

t3 = Mythread("线程3")

t4 = Mythread("线程4")

t1.start()
t2.start()
t3.start()
t4.start()
print("主线程")

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

这个RLock内部维护着一个Lock和一个计数(counter)变量,计数记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

from threading import Thread,RLock,Lock

import time

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

mutexA = mutexB = RLock()

class Mythread(Thread):

def init(self,name):

super().init()

self.name = name

def run(self) -> None:
self.f1()
self.f2() def f1(self):
mutexA.acquire()
print("%s 抢到了A锁" %self.name) mutexB.acquire()
print("%s 抢到了B锁" %self.name)
mutexB.release() mutexA.release() def f2(self):
mutexB.acquire()
print("%s 抢到了B锁" % self.name)
time.sleep(0.1) mutexA.acquire()
print("%s 抢到了A锁" % self.name)
mutexA.release() mutexB.release()

if name == 'main':

t1 = Mythread("线程1")

t2 = Mythread("线程2")

t3 = Mythread("线程3")

t4 = Mythread("线程4")

t1.start()
t2.start()
t3.start()
t4.start()
print("主线程")

事件Event

线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

Event对象的方法

event.isSet():返回event的状态值;

event.wait():如果 event.isSet()==False将阻塞线程;

event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入

就绪状态, 等待操作系统 调度;

event.clear():恢复event的状态值为False。

Event对象的案例

例1:一个线程运行完,另外一个线程才开始运行

from threading import Event,Thread,current_thread

import time

e = Event() # 全局变量 = False

def f1():

print('%s 运行' %current_thread().name)

time.sleep(3)

e.set() # 全局变量 = True

def f2():

e.wait() # 等到全局变量 变为 True

print('%s 运行' % current_thread().name)

if name == 'main':

t1 = Thread(target=f1)

t2 = Thread(target=f2)

t1.start()
t2.start()

"""

运行结果:

Thread-1 运行

Thread-2 运行

运行过程:

线程t1运行起来之后打印‘Thread-1 运行’,接着睡了3秒,这个时间足够线程t2

运行起来,线程t2起来后运行f2,接着开始等,e.wait()等到全局变量变为 True,

但是默认全局变量是False,线程t2上来后就开始等了,等3秒多一点的时间,等的这

个时间线程t1将全局变量e.set()为True,这个时候线程t2立马感觉到了全局变量为

True接着继续运行起来了打印‘Thread-2 运行’

"""

例2:模仿行人过红绿灯场景

from threading import Event,Thread,current_thread

import time

import random

e = Event() # 全局变量 = False

def task1(): # 任务1负责控制红绿灯

while True:

e.clear() # 全局变量 = False

print("红灯亮--->禁止通行")

time.sleep(2) # 红灯亮的时间

    e.set()   # 全局变量 = True
print('绿灯亮--->行人通行')
time.sleep(3) # 绿灯亮的时间

def task2(): # 任务2负责控制行人

while True:

if e.is_set(): # 如果ste过了代表路灯亮了

print('%s 走你' %current_thread().name)

break

else: # 如果没有set代表红灯亮了

print("%s 等灯" %current_thread().name)

e.wait() # 原地等,只要等灯亮了就等过去了,这个时间刚刚好

if name == 'main':

Thread(target=task1).start() # 开启红绿灯

while True:
time.sleep(random.randint(1,5)) # 1-5秒产生一个行人
Thread(target=task2).start()

"""

运行结果:

红灯亮--->禁止通行

绿灯亮--->行人通行

Thread-2 走你

红灯亮--->禁止通行

绿灯亮--->行人通行

Thread-3 走你

红灯亮--->禁止通行

Thread-4 等灯

Thread-5 等灯

"""

定时器timer

定时器Timer类是Thread的派生类,用于在指定时间后调用一个方法。

构造方法:

Timer(interval, function, args=[], kwargs={})

interval: 指定的时间

function: 要执行的方法

args/kwargs: 方法的参数

实例方法:

指定n秒后执行某操作

from threading import Timer

def hello(n): # n秒后执行

print("hello, world",n)

用timer类传时间 任务 任务参数

t = Timer(3, hello,args=(1111,))

t.start() # 3秒后,将打印“hello, world 1111”

线程队列

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

当信息必须在多个线程之间安全交换时,队列在线程编程中特别有用。

线程queue基本方法:

put 往线程队列里防止,超过队列长度,直接阻塞

get 从队列中取值,如果获取不到,直接阻塞

put_nowait: 如果放入的值超过队列长度,直接报错(linux)

get_nowait: 如果获取的值已经没有了,直接报错

线程queue的用法

import queue

队列:先进先出

q = queue.Queue(3) # 指定队列的大小

q.put(111) # 整型

q.put("aaa") # 字符串

q.put((1,2,3)) # 元组

print(q.get())

print(q.get())

print(q.get())

'''

111

aaa

(1, 2, 3)

'''

堆栈:后进先出

q = queue.LifoQueue(3)

q.put(111)

q.put("aaa")

q.put((1,2,3))

print(q.get())

print(q.get())

print(q.get())

'''

(1, 2, 3)

aaa

111

'''

优先级队列:

1.默认按照数字大小排序,然后会按照ascii编码在从小到大排序

2.先写先排,后写后排

q = queue.PriorityQueue(3)

q.put((10,111)) # 第一个值是优先级,第二值才是要放的元素

q.put((11,"aaa"))

q.put((-1,(1,2,3)))

print(q.get())

print(q.get())

print(q.get())

'''

(-1, (1, 2, 3)) # 数越小优先级越高

(10, 111)

(11, 'aaa')

'''

GIL全局解释器锁

GIL的全称是:Global Interpreter Lock,意思就是全局解释器锁

在CPython中,全局解释器锁(GIL)是一个互斥锁,用于防止多个本机线程同时执行Python字节码。这个锁是必需的,主要是因为CPython的内存管理不是线程安全的。(然而,自从GIL存在以来,其他特性已经发展到依赖于它所执行的保证。)这个GIL并不是python的特性,他是只在Cpython解释器里引入的一个概念,而在其他的语言编写的解释器里就没有这个GIL例如:Jython,Pypy

为什么会有GIL?

随着电脑多核cpu的出现核cpu频率的提升,为了充分利用多核处理器,进行多线程的编程方式更为普及,随之而来的困难是线程之间数据的一致性和状态同步,而python也利用了多核,所以也逃不开这个困难,为了解决这个数据不能同步的问题,设计了GIL全局解释器锁。

说到GIL解释器锁,我们容易想到在多线程中共享全局变量的时候会有线程对全局变量进行的资源竞争,会对全局变量的修改产生不是我们想要的结果,而那个时候我们用到的是python中线程模块里面的互斥锁,哪样的话每次对全局变量进行操作的时候,只有一个线程能够拿到这个全局变量

GIL既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。

只要在一个进程里就一定有GIL锁的存在,GIL锁不能保证python数据的安全,它保证的是解释器级别(内存管理)的安全,也可以说是背后存在的一种机制。可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。

GIL保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理,如下图:

GIL与Lock代码演示:

from threading import Thread,Lock

import time

mutex = Lock()

n = 100

def task():

global n

mutex.acquire()
temp = n
time.sleep(0.1)
n = temp - 1
mutex.release()

if name == 'main':

l = []

for i in range(100):

t = Thread(target=task)

l.append(t)

t.start()

for obj in l:
obj.join() print(n) # 结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全

'''

分析:

  1. 100个线程去抢GIL锁,即抢执行权限
  2. 肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会mutex.acquire()
  3. 极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁 lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL
  4. 直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的

    线程再重复2 3 4的过程

'''

GIL与多线程

有了GIL的存在,同一时刻同一进程中只有一个线程被执行

产生质问:进程可以利用多核,但是开销大,而python的多线程开销小,但却无法利用多核优势,也就是说python没用了,php才是最牛逼的语言?

要解决这个问题,我们需要在几个点上达成一致:

  1. cpu到底是用来做计算的,还是用来做I/O的?

  2. 多cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能

  3. 每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处

    结论:

  对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用

  当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地

分析:

我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:

方案一:开启四个进程

方案二:一个进程下,开启四个线程

单核情况下,分析结果:

  如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜

  如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜

多核情况下,分析结果:

  如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线

  程执行用不上多核,方案一胜

  如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜

结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多

大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。

多线程性能测试

计算密集型:多进程效率高

from multiprocessing import Process

from threading import Thread

import os,time

def work():

res=0

for i in range(100000000):

res*=i

time.sleep(5)

if name == 'main':

l=[]

# print(os.cpu_count()) #本机为4核

start=time.time()

for i in range(4):

# p=Process(target=work) #多进程:耗时20s多

p=Thread(target=work) # 多线程:耗时31s多

l.append(p)

p.start()

for p in l:

p.join()

stop=time.time()

print('run time is %s' %(stop-start))

I/O密集型:多线程效率高

from multiprocessing import Process

from threading import Thread

import threading

import os,time

def work():

time.sleep(2)

if name == 'main':

l=[]

# print(os.cpu_count()) #本机为4核

start=time.time()

for i in range(100):

# p=Process(target=work) # 耗时14s多,大部分时间耗费在创建进程上

p=Thread(target=work) # 耗时2s多

l.append(p)

p.start()

for p in l:

p.join()

stop=time.time()

print('run time is %s' %(stop-start))

应用:

多线程用于IO密集型,如socket,爬虫,web

多进程用于计算密集型,如金融分析

[并发编程 - 多线程:信号量、死锁与递归锁、时间Event、定时器Timer、线程队列、GIL锁]的更多相关文章

  1. python 并发编程 多线程 信号量

    一 信号量 信号量也是一把锁,可以指定信号量为5,对比互斥锁同一时间只能有一个任务抢到锁去执行,信号量同一时间可以有5个任务拿到锁去执行 如果说互斥锁是合租房屋的人去抢一个厕所,那么信号量就相当于一群 ...

  2. python 并发编程 多线程 目录

    线程理论 python 并发编程 多线程 开启线程的两种方式 python 并发编程 多线程与多进程的区别 python 并发编程 多线程 Thread对象的其他属性或方法 python 并发编程 多 ...

  3. python 并发编程 多线程 GIL全局解释器锁基本概念

    首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念. 就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码. ...

  4. python并发编程&多线程(二)

    前导理论知识见:python并发编程&多线程(一) 一 threading模块介绍 multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性 官网链 ...

  5. 7.并发编程--多线程通信-wait-notify

    并发编程--多线程通信-wait-notify 多线程通信:线程通信的目的是为了能够让线程之间相互发送信号; 1. 多线程通信: 线程通信的目的是为了能够让线程之间相互发送信号.另外,线程通信还能够使 ...

  6. Python并发编程——多线程与协程

    Pythpn并发编程--多线程与协程 目录 Pythpn并发编程--多线程与协程 1. 进程与线程 1.1 概念上 1.2 多进程与多线程--同时执行多个任务 2. 并发和并行 3. Python多线 ...

  7. 并发编程——多线程计数的更优解:LongAdder原理分析

    前言 最近在学习ConcurrentHashMap的源码,发现它采用了一种比较独特的方式对map中的元素数量进行统计,自然是要好好研究一下其原理思想,同时也能更好地理解ConcurrentHashMa ...

  8. python并发编程&多线程(一)

    本篇理论居多,实际操作见:  python并发编程&多线程(二) 一 什么是线程 在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程 线程顾名思义,就是一条流水线工作的过程,一 ...

  9. 8.并发编程--多线程通信-wait-notify-模拟Queue

    并发编程--多线程通信-wait-notify-模拟Queue 1. BlockingQueue 顾名思义,首先是一个队列,其次支持阻塞的机制:阻塞放入和获取队列中的数据. 如何实现这样一个队列: 要 ...

随机推荐

  1. Android Studio 如何运行单个activity

    •写在前面 调试界面运行单个 Activity 可节省编译整个项目的时间提高效率: 本着提高效率的角度,特地上网百度相关知识: •解决方法 首先,在 AndroidManifest.xml 文件中,找 ...

  2. 图文详解Java对象内存布局

    作为一名Java程序员,我们在日常工作中使用这款面向对象的编程语言时,做的最频繁的操作大概就是去创建一个个的对象了.对象的创建方式虽然有很多,可以通过new.反射.clone.反序列化等不同方式来创建 ...

  3. [BFS]最优乘车

    最优乘车 题目描述 HH 城是一个旅游胜地,每年都有成千上万的人前来观光.为方便游客,巴士公司在各个旅游景点及宾馆,饭店等地都设置了巴士站并开通了一些单程巴上线路.每条单程巴士线路从某个巴士站出发,依 ...

  4. windows认证解读

    0x00 本地认证 本地认证基础知识 在本地登录Windows的情况下,操作系统会使用用户输入的密码作为凭证去与系统中的密码进行验证,但是操作系统中的密码存储在哪里呢? %SystemRoot%\sy ...

  5. Java 获取Word中的标题大纲(目录)

    概述 Word中的标题可通过"样式"中的选项来快速设置(如图1), 图1 在添加目录时,可将"有效样式"设置为"目录级别"显示(如图2),一 ...

  6. vite 动态 import 引入打包报错解决方案

    关注公众号: 微信搜索 前端工具人 ; 收货更多的干货 原文链接: 自己掘金文章 https://juejin.cn/post/6951557699079569422/ 关注公众号: 微信搜索 前端工 ...

  7. js--原型和原型链相关问题

    前言 阅读本文前先来思考一个问题,我们在 js 中创建一个变量,我们并没有给这个变量添加一些方法,比如 toString() 方法,为什么我们可以直接使用这个方法呢?如以下代码,带着这样的问题,我们来 ...

  8. 数据库MySQL一

    P252 1.MySQL 最为主要使用的数据库 my sequel 不容易查找数据 DB数据库 存储数据的仓库,它保存了一系列有组织的数据 DBMS数据库管理系统,数据库是通过DBMS创建和操作的容器 ...

  9. 【笔记】《Redis设计与实现》chapter18 发布与订阅

    chapter18 发布与订阅 客户端订阅频道. 客户端向频道发送消息, 消息被传递至各个订阅者. 匹配模式 客户端订阅模式. 客户端向频道发送消息, 消息被传递给正在订阅匹配模式的订阅者. 另一个模 ...

  10. ECDSA密钥对生成以及在Token中的应用

    1 概述 本文主要讲述了如何利用Openssl生成ECDSA密钥对,并利用Auth0库进行Token生成及验证的过程. 2 ECDSA 2.1 简介 ECC(Elliptic Curve Crypto ...