没有人会为你的贫穷负责,却有人为你的富有而喝彩,所以不要活在别人的嘴巴里,请做好自己。有路就大胆的去走,有梦就大胆的飞翔,若要成功,就要大胆去闯,大胆尝试才是信仰。不敢做,不去闯,梦想就会变成幻想,前行的路不怕万人阻挡,只怕自己投降,人生的帆,不怕狂风巨浪,只怕自己没胆量!!
有目标的人睡不着,沒目标的人睡不醒,容易走的都是下坡路。埋怨是懦弱的表现,努力才是人生的应有态度,睁开眼就是新的开始,时常鼓励一下自己,对自己说该奋斗了!
总结:
- 和Lock很像,信号量和锁都是解决资源有限的问题的;
- 最常见的池:连接池、线程池;
- GIL全局解释器锁(面试常问):假并行的本质、解决方案;*GIL是进程级别的锁,你可以绕开它(使用多进程);
- 线程安全的类型目前只有 queue ; 内置数据类型 其实不是线程安全的;
- 欠的债都是要还的;
- Python大力发展协程;
1. semaphore 信号量
和Lock很像,信号量对象内部维护一个倒计数器,每一次acquire都会减1,当acquire方法发现计数为0就阻塞请求的线程,直到其它线程对信号量release后,计数大于0,恢复阻塞的线程。
名称 | 含义 |
---|---|
Semaphore(value=1) | 构造方法,value小于0,抛 valueError异常 |
acquire(blocking=True,timeout=None) | 获取信号量,计数器减 1,获取成功返回True |
release() | 释放信号量,计数器 加 1 |
import threading
import logging
import time
# 输出格式定义
FORMAT = '%(asctime)-15s\t [%(threadName)s, %(thread)8d] %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)
def worker(s: threading.Semaphore):
logging.info('in sub thread')
logging.info(s.acquire()) # 阻塞
logging.info('sub thread over')
# 信号量
s = threading.Semaphore(3)
logging.info(s.acquire())
print(s._value)
logging.info(s.acquire())
print(s._value)
logging.info(s.acquire())
print(s._value)
threading.Thread(target=worker, args=(s,)).start()
time.sleep(2)
logging.info(s.acquire(False))
logging.info(s.acquire(timeout=3)) # 阻塞3秒
# 释放
logging.info('released')
s.release()
#---------------------------------------------------------------------
2020-01-14 20:28:43,564 [MainThread, 17620] True
2020-01-14 20:28:43,564 [MainThread, 17620] True
2020-01-14 20:28:43,564 [MainThread, 17620] True
2020-01-14 20:28:43,564 [Thread-1, 19392] in sub thread
2
1
0
2020-01-14 20:28:45,564 [MainThread, 17620] False
2020-01-14 20:28:48,565 [MainThread, 17620] False
2020-01-14 20:28:48,565 [MainThread, 17620] released
2020-01-14 20:28:48,565 [Thread-1, 19392] True
2020-01-14 20:28:48,565 [Thread-1, 19392] sub thread over
elease方法超界问题
假设如果还没有acquire信号量,就release,会怎么样?
2. BoundedSemaphore类
有界的信号量,不允许使用release超出初始值的范围,否则,抛出ValueError异常。
将上例的信号量改成有界的信号量试一试。
应用举例
连接池
因为资源有限,且开启一个连接成本高,所以,使用连接池。
一个简单的连接池
连接池应该有容量(总数),有一个工厂方法可以获取连接,能够把不用的连接返回,供其他调用者使用。
场景:要频繁的创建资源;
class Conn:
def __init__(self, name):
self.name = name
class Pool:
def __init__(self, count:int):
self.count = count
# 池中是连接对象的列表
self.pool = [self._connect("conn-{}".format(x)) for x in range(self.count)]
def _connect(self, conn_name):
# 创建连接的方法,返回一个名称
return Conn(conn_name)
def get_conn(self):
# 从池中拿走一个连接
if len(self.pool) > 0:
return self.pool.pop()
def return_conn(self,conn:Conn):
# 向池中添一个链接
self.pool.append(conn)
真正的连接池的实现比上面的例子要复杂的多,这里只是简单的一个功能的实现;
本例中,get_conn()方法在多线程的时候有线程安全问题
假设池中正好有一个连接,有可能多个线程池的长度是大于0,当一个线程拿走了连接对象,其他线程再pop就会抛异常的。
解决方案
1、加锁,在读写的地方加锁;
2、使用信号量Semaphore;
import threading
import logging
import random
FORMAT = "%(asctime)s %(thread)d %(threadName)s %(message)s"
logging.basicConfig(level=logging.INFO, format=FORMAT)
class Conn:
def __init__(self, name):
self.name = name
def __repr__(self):
return self.name
class Pool:
def __init__(self, count: int):
self.count = count
# 池中是连接对象的列表
self.pool = [self._connect("conn-{}".format(x)) for x in range(count)]
self.semaphore = threading.Semaphore(count) # threading.Semaphore()
def _connect(self, conn_name):
# 返回一个名称
return Conn(conn_name)
def get_conn(self):
# 从池中拿走一个连接
print('-------------')
self.semaphore.acquire()
print('===============')
conn = self.pool.pop()
return conn
def return_conn(self, conn: Conn):
# 向池中添加一个连接
self.pool.append(conn)
self.semaphore.release()
# 连接池初始化
pool = Pool(3)
def worker(pool:Pool):
conn = pool.get_conn()
logging.info(conn)
# 模拟使用了一段
threading.Event().wait(random.randint(1,4))
pool.return_conn(conn)
for i in range(6):
threading.Thread(target=worker, name="worker-{}".format(i),args=(pool,)).start()
#------------------------------------------------------------------------------------
上例中,使用信号量解决资源有限的问题。
如果池中有资源,请求者获取资源时信号量减1,拿走资源。当请求超过资源数,请求者只能等待。当使用者用完归还资源后信号量加1,等待线程就可以被唤醒拿走资源。
注意:这个连接池的例子不能用到生成环境,只是为了说明信号量使用的例子,连接池还有很多未完成功能。
3. 问题
self.conns.append(conn) 这一句有哪些问题考虑?
1、边界问题分析
return_conn方法可以单独执行,有可能多归还连接,也就是会多release,所以,要用有界信号量BoundedSemaphore类。
这样用有界信号量修改源代码,保证如果多return_conn就会抛异常。
self.pool.append(conn)
self.semaphore.release()
假设一种极端情况,计数器还差1就归还满了,有三个线程A、B、C都执行了第一句,都没有来得及release,这时候轮到线程A release,正常的release,然后轮到线程C先release,一定出问题,超界了,直接抛异常。
因此信号量,可以保证,一定不能多归还。
如果归还了同一个连接多次怎么办,重复很容易判断。
这个程序还不能判断这些连接是不是原来自己创建的,这不是生成环境用的代码,只是简单演示。
2、正常使用分析
正常使用信号量,都会先获取信号量,然后用完归还。
创建很多线程,都去获取信号量,没有获得信号量的线程都阻塞。能归还的线程都是前面获取到信号量的线程,其他没有获得线程都阻塞着。非阻塞的线程append后才release,这时候等待的线程被唤醒,才能pop,也就是没有获取信号量就不能pop,这是安全的。
经过上面的分析,信号量比计算列表长度好,线程安全。
信号量和锁
锁,只允许同一个时间一个线程独占资源,他是特殊的信号量;
信号量,可以多个线程访问共享资源,但这个共享资源数量有限;
锁,可以看做特殊的信号量;
4. 数据结构和GIL
Queue
标准库queue模块,提供FIFO的Queue\LIFO的队列、优先队列;
Queue类是线程安全的,适用于多线程间安全的交换数据。内部使用了Lock和Condition。
为什么讲魔术方法时,说实现容器的大小,不准确?
如果不加锁,是不可能获得准确的大小的,因为你刚读取到了一个大小,还没有取走,就有可能被其他线程改了。
Queue类的size虽然加了锁,但是,依然不能保证立即get、put就能成功,因为读取大小和get、put方法是分开的。
import queue
q = queue.Queue(8)
if q.qsize() == 7:
q.put() # 上下两句可能被打断
if q.qsize() == 1:
q.get() # 未必会成功
5. GIL全局解释器锁***(面试常问)
CPython 在解释器进程级别有一把锁,叫做GIL 全局解释器进程锁。
(假并行运行的本质:)GIL 保证CPython进程中,只有一个线程执行字节码。甚至是在多核CPU的情况下,也是只能允许一个CPU上的一个线程在运行。(时间片非常短)
CPython中
IO密集型,由于线程阻塞,就会调度其他线程;
CPU密集型,当前线程可能会连续的获得GIL,导致其它线程几乎无法使用CPU。
假并行运行解决方案:在CPython中由于有GIL存在,IO密集型,使用多线程较为合算;CPU密集型,使用多进程,要绕开GIL。
新版CPython正在努力优化GIL的问题,但不是移除。
如果在意多线程的效率问题,请绕行,选择其它语言erlang、Go等。
Python中绝大多数内置数据结构的读、写操作都是原子操作。
由于GIL的存在,Python的内置数据类型在多线程编程的时候就变成了安全的了,但是实际上它们本身 **不是线程安全类型**。
保留GIL的原因:
Guido坚持的简单哲学,对于初学者门槛低,不需要高深的系统知识也能安全、简单的使用Python。
而且移除GIL,会降低CPython单线程的执行效率。
测试下面2个程序,请问下面的程序是计算密集型还是IO密集型?
import logging,threading
import datetime
logging.basicConfig(level=logging.INFO, format="%(thread)s %(message)s")
start = datetime.datetime.now()
# 计算
def calc():
sum = 0
for _ in range(10000):
sum += 1
calc()
calc()
calc()
calc()
calc()
delta = (datetime.datetime.now() - start).total_seconds()
logging.info(delta)
#-------------------------------------------------------------
9328 0.00197
import threading
import logging
import datetime
logging.basicConfig(level=logging.INFO, format="%(thread)s %(message)s")
start = datetime.datetime.now()
# 计算
def calc():
sum = 0
for _ in range(1000000000):
sum += 1
t1 = threading.Thread(target=calc)
t2 = threading.Thread(target=calc)
t3 = threading.Thread(target=calc)
t4 = threading.Thread(target=calc)
t5 = threading.Thread(target=calc)
t1.start()
t2.start()
t3.start()
t4.start()
t5.start()
t1.join()
t2.join()
t3.join()
t4.join()
t5.join()
delta = (datetime.datetime.now() - start).total_seconds()
logging.info(delta)
#------------------------------------------------
18056 194.114758
注意,不要在代码中出现print等访问IO的语句;
从两段程序测试的结果来看,CPython 中多先后才能根本没有任何优势,和一个线程执行时间相当,因为GIL的存在,尤其是像上面的计算密度型程序;