目录

threading.Semaphore类:... 1

数据结构Queue... 5

GIL. 5

 

 

 

threading.Semaphore类:

线程同步技术;

Lock很像,信号量内部维护一个倒计数器,每一次acquire都会减1release1),当acquire方法发现计数为0就阻塞,直至其它线程对信号量release后,计数大于0,恢复阻塞的线程;

 

倒计数器永远不会<0,是非负数,因为acquire时发现是0都会被阻塞;

linux OS的信号完全不同;

 

s=threading.Semaphore(value=1),构造方法,value<0ValueError异常;

s.acquire(blocking=True,timeout=None),获取信号量,计数器减1,获取成功返回True

s.release(),释放信号量,计数器加1

 

应用场景:

连接池;

因为资源有限,每开启一个,连接成本很高(TCP三次握手四次挥手等),所以使用连接池;

 

threading.BoundedSemaphore类:

有界的信号量,作上界控制;

不允许使用release超出初始值的范围,否则抛ValueError异常;

 

信号量和锁:

锁,只允许同一个时间一个线程独占资源,它是特殊的信号量,即信号量计数器初值为1

信号量,允许多个线程访问共享资源,但这个共享资源数量有限;

锁,可看作是特殊的信号量;

 

例:

def work(s:threading.Semaphore):

    logging.info('in sub')

    s.acquire()

    logging.info('end sub')

 

s = threading.Semaphore(3)

 

logging.info(s.acquire())

logging.info(s.acquire())

logging.info(s.acquire())

 

threading.Thread(target=work, args=(s,)).start()

 

print('~'*20)

time.sleep(2)

logging.info(s.acquire(False))

logging.info(s.acquire(timeout=3))

s.release()

print('end main')

输出:

2018-08-07-10:07:23       Thread info: 9360 MainThread True

2018-08-07-10:07:23       Thread info: 9360 MainThread True

2018-08-07-10:07:23       Thread info: 9360 MainThread True

2018-08-07-10:07:23       Thread info: 13180 Thread-1 in sub

~~~~~~~~~~~~~~~~~~~~

2018-08-07-10:07:25       Thread info: 9360 MainThread False

2018-08-07-10:07:28       Thread info: 9360 MainThread False

2018-08-07-10:07:28       Thread info: 13180 Thread-1 end sub

end main

 

例:

s = threading.Semaphore(3)

s.release()   #直接使用release改变了初始预设值

s.release()

s.release()

print(s.__dict__)

输出:

{'_value': 6, '_cond': , 0)>}

 

例,解决超出预设值:

s = threading.BoundedSemaphore(3)

s.release()

输出:

Traceback (most recent call last):

  File "E:/git_practice/cmdb/example_threading2.py", line 359, in

    s.release()

  File "D:\Python\Python35\lib\threading.py", line 480, in release

    raise ValueError("Semaphore released too many times")

ValueError: Semaphore released too many times

 

例:

连接池应有容量(总数),有一个工厂方法可获取连接,能够把不用的连接返回,供其它调用者使用;

 

class Conn:

    def __init__(self, name):

        self.name = name

 

class Pool:

    def __init__(self, count):

        self.count = count

        # self.pool = []

        self.pool = [self._connect('conn-{}'.format(i)) for i in range(count)]

 

    def _connect(self, conn_name):

        return Conn(conn_name)

 

    def get_conn(self):   #此方法在多线程时有安全问题;如果池中正好有一个连接,有可能多个线程判断池的长度是>0的,当一个线程拿走了连接对象,其它线程再来pop就抛异常,解决:用LockSemaphore

        # return self.pool.pop()

        if len(self.pool) > 0:

            return self.pool.pop()

 

    def return_conn(self, conn:Conn):

        self.pool.append(conn)

 

例,连接池改进:

class Conn:

    def __init__(self, name):

        self.name = name

 

class Pool:

    def __init__(self, count):

        self.count = count

        self.pool = [self._connect('conn-{}'.format(i)) for i in range(count)]

        # self.sema = threading.Semaphore(count)

        self.sema = threading.BoundedSemaphore(count)   #信号量比计算列表长度要好

 

    def _connect(self, conn_name):

        return Conn(conn_name)

 

    def get_conn(self):

        self.sema.acquire()

        data = self.pool.pop()

        return data

 

    def return_conn(self, conn:Conn):

        self.pool.append(conn)

        self.sema.release()

 

pool = Pool(3)

def work(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=work, args=(pool,), name='worker-{}'.format(i)).start()

输出:

2018-08-07-14:09:11       Thread info: 3264 worker-0 <__main__.Conn object at 0x00000000014E6780>

2018-08-07-14:09:11       Thread info: 12452 worker-1 <__main__.Conn object at 0x00000000014E6630>

2018-08-07-14:09:11       Thread info: 11468 worker-2 <__main__.Conn object at 0x00000000014E6748>

2018-08-07-14:09:13       Thread info: 13228 worker-3 <__main__.Conn object at 0x00000000014E6748>

2018-08-07-14:09:14       Thread info: 12692 worker-4 <__main__.Conn object at 0x00000000014E6780>

2018-08-07-14:09:15       Thread info: 6748 worker-5 <__main__.Conn object at 0x00000000014E6630>

 

注:

使用信号量解决资源有限问题;

如果池中有资源,请求者获取资源时信号量减1,拿走资源;

当请求超过资源数,请求者只能等待;

当使用者用完归还资源后信号量加1,等到线程拿到就可唤醒拿走资源;

 

以上,从程序逻辑上分析:

1、如果还没有使用信号量就release,会怎么样?

超出预设值,解决用threading.BoundedSemaphore

2、如果使用了信号量,但还没用完?

        self.pool.append(conn)

        self.sema.release()

如果一种极端情况,计数器还差1个就满了,有三个线程ABC都执行了第一句,都没来得及release,这时轮到线程A release,然后轮到C release,这时一定出问题,超界了ValueError,因此有界信号量能保证,一定不能多归还;

3、很多线程用完了信号量?

没有获得信号量的线程都阻塞,没有线程和归还的线程争抢,当append后才release,这时才能等待的线程被唤醒,才能pop,即没有获取信号量就不能pop,这是安全的;

 

 

 

数据结构Queue

标准库;

提供FIFOLIFOQueue,优先队列;

Queue类是线程安全的,适用于多线程间安全的交换数据,内部使用了LockCondition

 

魔术方法中,说实现容器的大小不准确?

如果不加锁,是不可能获得准确的大小的,因为当前线程刚读取了一个大小,还没取走,就有可能被其它线程改掉了;

 

Queue类的size虽加了锁,但依然不能保证立即getput就能成功,因为读取大小和getput方法是分开的;

 

 

 

GIL

gobal intepreter lock,全局解释器锁;

GIL保证cpyton进程中,只有一个线程执行字节码,甚至在多核cpu情况下也是如此;ruby中也有GIL;另,其它解释器没有这种情况,如pypyjython等;

 

cpython中没有真正的多线程;

GIL的本质:理解cpython多线程;

 

cpython中:

IO密集型,由于线程阻塞,就会调度其它线程;

cpu密集型,当前线程可能会连续的获得GIL,导致其它线程几乎无法使用cpu

 

IO密集型,使用多线程;

cpu密集型,使用多进程,绕开GIL

 

新版cpython正努力优化GIL问题,但不是移除;

如果非要使用多线程且要有效率,请绕行,选择其它语言goerlang等;

 

py中绝大多数内置数据结构都是原子操作,如lst.append()lst.pop()

由于GIL的存在,py的内置数据类型在多线程编程时就变为安全的了,但实际上它们本身不是线程安全的;

 

项目一开始就要固定使用某一解释器的XX版本;

 

保留GIL的原因:

guido坚持的简单哲学,对于初学者门槛低,不需要高深的系统知识也能安全、简单的使用py

若移除GIL,会降低cpython单线程的执行效率;

 

1

def calc():

    sum = 0

    for _ in range(100000000):

        sum += 1

 

start = datetime.datetime.now()

 

calc()

calc()

calc()

calc()

calc()

 

delta = (datetime.datetime.now() - start).total_seconds()

print(delta)

输出:

32.942885

 

2

def calc():

    sum = 0

    for _ in range(100000000):

        sum += 1

 

start = datetime.datetime.now()

 

lst = []

for _ in range(5):   #CPU密集型,用多进程解决

    t = threading.Thread(target=calc)

    t.start()

    # t.join()   #t.join()不能放在此处,若放在此处是串行(第1个线程执行完才能循环执行到下一个线程),不是并行

    lst.append(t)

for t in lst:

    t.join()

 

delta = (datetime.datetime.now() - start).total_seconds()

print(delta)

输出:

32.487859

 

对比例1和例2

从结果上看,单线程和多线程一样,cpython的多线程没有优势和一个线程执行时间相当,因为GIL的存在;