38.4-barrier、semaphore和GIL

没有人会为你的贫穷负责,却有人为你的富有而喝彩,所以不要活在别人的嘴巴里,请做好自己。有路就大胆的去走,有梦就大胆的飞翔,若要成功,就要大胆去闯,大胆尝试才是信仰。不敢做,不去闯,梦想就会变成幻想,前行的路不怕万人阻挡,只怕自己投降,人生的帆,不怕狂风巨浪,只怕自己没胆量!!

有目标的人睡不着,沒目标的人睡不醒,容易走的都是下坡路。埋怨是懦弱的表现,努力才是人生的应有态度,睁开眼就是新的开始,时常鼓励一下自己,对自己说该奋斗了!

总结:

  1. 和Lock很像,信号量和锁都是解决资源有限的问题的;
  2. 最常见的池:连接池、线程池;
  3. GIL全局解释器锁(面试常问):假并行的本质、解决方案;*GIL是进程级别的锁,你可以绕开它(使用多进程);
  4. 线程安全的类型目前只有 queue ; 内置数据类型 其实不是线程安全的;
  5. 欠的债都是要还的;
  6. 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的存在,尤其是像上面的计算密度型程序;

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容