Fork me on GitHub

线程并发

本文是Python通用编程系列教程,已全部更新完成,实现的目标是从零基础开始到精通Python编程语言。本教程不是对Python的内容进行泛泛而谈,而是精细化,深入化的讲解,共5个阶段,25章内容。所以,需要有耐心的学习,才能真正有所收获。虽不涉及任何框架的使用,但是会对操作系统和网络通信进行全局的讲解,甚至会对一些开源模块和服务器进行重写。学完之后,你所收获的不仅仅是精通一门Python编程语言,而且具备快速学习其他编程语言的能力,无障碍阅读所有Python源码的能力和对计算机与网络的全面认识。对于零基础的小白来说,是入门计算机领域并精通一门编程语言的绝佳教材。对于有一定Python基础的童鞋,相信这套教程会让你的水平更上一层楼。

一 线程理论

进程指的是一个程序正在进行,线程可以理解为是一条流水线的工作过程,我们会把一个进程比如成一个车间,而线程就比喻成车间里面的流水线,很明显一个车间里面至少应该有一条流水线,确实,一个进程内就自带一个线程。在这里先把我们之前学过的关于进程的概念忘记,因为准确的说一个进程是不能执行的,进程本质上不是一个执行单位,而是一个资源单位(你想开一个工厂,并不是有了这个车间就能生产了,而是应该车间里面至少有一条流水线设备),一个进程内自带一个线程,这个线程才是执行单位,就像是一个车间内只有流水线上的机器才是真正用于生产的。

其实真正运行进程里面的代码的其实就是这个进程内自带的这个线程,这个也就是这个进程内的主线程。同一进程内的多个线程共享这个进程内的资源,不同进程内的线程资源必然是隔离的。 在一个进程内再次创建一个线程肯定是不需要再次申请内存空间的,那么创建线程的开销自然会比创建进程的开销要小很多,具体小多少,100倍以上。
线程应用场景
比如现在我们需要写一个文本处理工具,像word,notepad++,现在很多文本处理工具都很先进了,他会自动的帮你保存到硬盘,你要考虑的功能至少有4个:

  1. 获取用户输入的信息
  2. 把用户输入的信息打印到屏幕上
  3. 把用户输入的信息保存到硬盘

如果我们让程序串行执行,我都不敢想象这个程序写出来是有多难用,所以,他一定是需要并发执行的,并发的方案无非就是两种,多进程和多线程,我们要处理的信息就是用户输入的内容,如果我们使用多进程,这个通信的过程一定很漫长而且程序很复杂,但是如果我们使用同一个进程内的多线程,那么他们就能共享这个内存空间的数据,我们的处理会变得很容易。其实很多情况下,我们写的程序各部分组件之间都要共享一些数据,所以我们会推荐使用多线程。

二 开启线程的两种方式

我们在前面把进程相关的知识打好了基础,那么线程会变得非常简单,开始线程的方式也非常类似,区别就是在于线程的开销比进程小的多。

1. 用函数的方式创建线程

from threading import Thread
def task(name):
    print("%s is running" % name)
if __name__ == '__main__':
    t = Thread(target=task, args=('Albert',))
    t.start()  # 同样是发信号,但是线程的开销小的多
    print('主线程')  # 你会看到这一行后打印,这就是与进程的区别

线程本来没有父子关系,也就是没有子线程的说法,但是为了区分,我们暂时先把有主线程开启的线程称为“子线程”,接下来请看下面的示例

from threading import Thread
import time
def task(name):
    print("%s is running" % name)
    time.sleep(3)
if __name__ == '__main__':
    t = Thread(target=task, args=('Albert',))
    t.start()  # 同样是发信号,但是线程的开销小的多
    print('主线程')  # 你会看到这一行后打印,这就是与进程的区别

执行代码,你会看到这样的结果,当主线程代码运行结束后,它并不会结束,而是会等待“子线程”代码结束。因为主线程其实就是代表的这个进程,一旦这个进程结束了,那么进程的内存空间就会清理掉,如果还存在没有运行完的“子线程”,那也就废了。这里的主线程等待“子线程”的等待和主进程等待子进程的等待不是一样的,主线程的等待是为了确保“子线程”都运行完了,主进程的等待是为了给子进程收尸。
在一个车间内有一个自带的线程,这个线程就是进程内的主线程,我们可以比如一下,如果主线程结束就相当于是把这个车间的电闸给停了,那他肯定是要等待别的线程都干完了活再停电。主进程就相当于是管理所有进程车间的主任,他要等所有的车间都不工作了,然后安排人清理一下车间里面的垃圾。

2. 用类的方式创建线程

from threading import Thread
import time
class MyThread(Thread):
    def run(self):
        print("%s is running" % self.name)
        time.sleep(3)
if __name__ == '__main__':
    t = MyThread()
    t.start()
    print('主线程')

接下来用一下代码来证明一句废话:同一进程内的多个线程同属于一个进程

from threading import Thread
import time
import os
def task():
    print("%s is running" % os.getpid())
    time.sleep(3)
if __name__ == '__main__':
    t = Thread(target=task, )
    t.start()
    print('主线程', os.getpid())

证明:同一进程内的多个线程共享该进程内的资源

from threading import Thread
import time
x = 999
def task():
    global x
    x = 1
    time.sleep(3)
if __name__ == '__main__':
    t = Thread(target=task, )
    t.start()
    t.join()  # 让主线程等待"子线程"结束,与进程接口相同,我们就不让他睡了
    print('主线程', x)

三 线程对象的其他方法

# current_thread是一个类,这个类实例化得到的对象就是当前的线程
from threading import Thread, current_thread, active_count, enumerate
import time
def task():
    print('%s is running' % current_thread().name)
    time.sleep(3)
if __name__ == '__main__':
    t1 = Thread(target=task, name='这是一个牛逼的线程')
    t2 = Thread(target=task, )
    t3 = Thread(target=task, )
    t1.start()
    t2.start()
    t3.start()
    print(t1.is_alive())  # 判断这个线程是否存活
    print(active_count())  # 查看活跃线程数数量=主线程数量+"子线程"数量
    print(enumerate())  # 把当前活跃的线程对象全部放到列表
    print('主线程', current_thread().name)

四 守护线程

正常情况下主线程需要等待“子线程”结束才会结束,代码如下:

from threading import Thread, current_thread
import time
def task():
    print('%s is running' % current_thread().name)
    time.sleep(3)
if __name__ == '__main__':
    t1 = Thread(target=task, name='这是一个牛逼的线程')
    t1.start()
    print('主线程', current_thread().name)

当我们把“子线程”变成守护线程之后,只要主线程运行完毕,程序就会结束

from threading import Thread, current_thread
import time
def task():
    print('%s is running' % current_thread().name)
    time.sleep(3)
if __name__ == '__main__':
    t1 = Thread(target=task, name='这是一个牛逼的线程')
    t1.daemon = True
    t1.start()
    print('主线程', current_thread().name)

注意:主线程运行完毕指的是非守护的子线程运行完毕,因为主线程就是代表了这个进程,它是一个整体,对比一下:主进程的运行完毕和子进程是没有关系的,直到代码运行结束就是主进程的运行结束。
守护进程与守护线程对比

from multiprocessing import Process
import time
def foo():
    print(123)
    time.sleep(1)
    print("end123")
def bar():
    print(456)
    time.sleep(3)
    print("end456")
if __name__ == '__main__':
    p1 = Process(target=foo)
    p2 = Process(target=bar)
    p1.daemon = True  # 主进程很快运行完,守护进程不会运行
    p1.start()
    p2.start()
    print("main-------")  # 主进程代码运行完毕,守护进程就会结束

上面的代码你还会发现主进程的运行结束与守护进程片p1的运行没有关系

from threading import Thread
import time
def foo():
    print(123)
    time.sleep(1)
    print("end123")
def bar():
    print(456)
    time.sleep(3)
    print("end456")
if __name__ == '__main__':
    p1 = Thread(target=foo)
    p2 = Thread(target=bar)
    p1.daemon = True  # 线程很快运行,不会死
    p1.start()
    p2.start()
    print("main-------")  # main的出现标志着主线程的代码运行完了,但是非守护的"子线程"还没有结束

区别主要在于两点:

  1. 线程的创建开销小,所以它会先于主线程运行
  2. 主线程必须要等待非守护“子线程”运行结束才算是结束


如果我们把守护线程的执行时间改成5秒,那么只要非守护“子线程”执行完了,主线程也就运行结束了。

五 线程互斥锁

以下代码我们希望看到的结果是,100个线程把结果变为0,可结果并不如意

from threading import Thread
import time
x = 100
def task():
    global x
    temp = x
    time.sleep(0.1)  # 模拟线程处理任务过程中的延迟
    x = temp - 1
if __name__ == '__main__':
    start = time.time()
    thread_list = []
    for i in range(100):
        t = Thread(target=task)
        thread_list.append(t)
        t.start()
    for t in thread_list:
        t.join()
    print('主', x)
    print(time.time() - start)

关键的点就在延迟的0.1秒,这个时间足够100个线程起来,他们所执行的操作都是一样的,所以你才会看到屏幕上的结果。
这就是并发执行所带来的数据不安全的问题,为了解决这个问题,我们需要对多个线程进行加锁处理。

from threading import Thread, Lock
import time
mutex = Lock()
x = 100
def task():
    global x
    mutex.acquire()
    temp = x
    time.sleep(0.1)  # 100个线程需要等待10秒多的时间
    x = temp - 1
    mutex.release()
if __name__ == '__main__':
    start = time.time()
    thread_list = []
    for i in range(100):
        t = Thread(target=task)
        thread_list.append(t)
        t.start()
    for t in thread_list:
        t.join()
    print('主', x)
    print(time.time() - start)

六 死锁与递归锁

死锁现象在程序中一旦出现,是很严重的问题,所以我们要尽可能去避免,现在给大家演示一下,人为写bug是怎么写的。

from threading import Thread, Lock
import time
mutex1 = Lock()
mutex2 = Lock()
class MyThread(Thread):
    def run(self):
        self.f1()
        self.f2()
    def f1(self):
        mutex1.acquire()
        print('%s 拿到了1锁===f1' % self.name)
        mutex2.acquire()
        print('%s 拿到了2锁===f1' % self.name)
        mutex2.release()
        mutex1.release()
    def f2(self):
        mutex2.acquire()
        print('%s 拿到了2锁===f2' % self.name)
        time.sleep(0.1)
        mutex1.acquire()
        print('%s 拿到了1锁===f2' % self.name)
        mutex1.release()
        mutex2.release()
if __name__ == '__main__':
    for i in range(10):
        t = MyThread()
        t.start()
    print('主')

程序执行会进入死锁状态,原因就在于第一个线程和第二个线程,线程创建的快,肯定是第一个创建的线程先起来,他很容易就可以抢到A锁和B锁,然后依次释放并执行f2函数,开始抢B锁,这时其他的线程都在抢A锁,当第一个线程拿到了B锁,肯定也会有一个线程拿到了A锁,这是他们分别需要抢对方手里面的锁,谁都不松手,互相把对方锁死了,那么程序也就卡在原地了。
当有些时候为了保证数据的安全,我们必须要加锁,但是这个处理锁的过程又非常让人头疼,所以,我们应该尽量避免,但是如果躲不过去了,那么我们也能有解决方案。
我们现在用的锁Lock(多进程也是一样)叫做互斥锁,它的一个小缺点就是不能连续acquire,接下来给大家介绍一个递归锁RLock(Recursion Lock),递归锁的特点就是他可以连续acquire,每次acquire都会给锁的计数+1,这时其他的线程都不能抢,直到锁的计数变为0才可以抢锁,所以问题自然就解决了。

from threading import Thread, RLock
import time
mutexA = mutexB = RLock() # 其他代码都不用改 
class MyThread(Thread):
    def run(self):
        self.f1()
        self.f2()
    def f1(self):
        mutexA.acquire()  # 锁+1
        print('%s 拿到了A锁===f1' % self.name)
        mutexB.acquire()  # 锁再+1
        print('%s 拿到了B锁===f1' % self.name)
        mutexB.release()  # 锁-1
        mutexA.release()  # 锁变为0
    def f2(self):
        mutexB.acquire()
        print('%s 拿到了B锁===f2' % self.name)
        time.sleep(0.1)
        mutexA.acquire()
        print('%s 拿到了A锁===f2' % self.name)
        mutexA.release()
        mutexB.release()
if __name__ == '__main__':
    for i in range(10):
        t = MyThread()
        t.start()
    print('主')

七 信号量

我们最开始接触互斥锁的时候使用的大学寝室公用同一个卫生间来说明的,但是除了独用的卫生间,你一定还见过公用的卫生间,它里面会有很多的仓位,假如有10个仓位,如果里面都有人了,那么肯定是不能再进去的,但是只要有一个人出来,那么就能再进去一个人,信号量的作用就是做这样的处理。

# from multiprocessing import Semaphore  # 进程也有信号量
from threading import Thread, Semaphore, current_thread
import time, random
sm = Semaphore(5)  # 卫生间有5个仓位
def go_wc():
    sm.acquire()
    print('%s 上卫生间ing' % current_thread().getName())
    time.sleep(random.randint(1, 3))  # 模拟上卫生间时间
    sm.release()
if __name__ == '__main__':
    for i in range(23):
        t = Thread(target=go_wc)
        t.start()

八 GIL全局解释器锁

先来看一段关于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.
'''

以下是我的翻译
在CPython中,全局解释器锁,或者说是GIL,是一把阻止多个本机的线程在同一时间运行Python字节码的互斥锁。CPython解释器需要这把锁主要是因为它的内存管理不是线程安全的。
首先需要明确的一点是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。
每运行一个Python程序就会在内存中就开辟一块进程的内存空间,创建一个进程,在这个内存空间中会先把Python解释器的代码放进去,然后再把你写的Python代码放进去,如下图所示。

解释器代码先运行起来,然后它会把test.py的代码全部都当作字符串来解释执行,其实解释器就像是一堆接口,当你写的代码里面出现了一些Python的关键字,解释就会调用他自己的代码来帮你完成,最后再返回给你结果。
先明确运行一个Python程序这是一个进程,如果我们在test.py这个代码中开启3个线程,包括test.py他默认的这个线程在内,它的每一个“子线程”的运行都要依赖解释器来运行,这样的话就会有4个线程去争抢Python解释器来执行自己的代码。为了使争抢变得有序,CPython解释器做了加锁的处理,这个锁本质就是互斥锁,也就是全局解释器锁。所以,现在我们再来理解一下上面那段话的第一句,GIL就是用来控制同一进程内的多个线程同一时间的只能有一个运行。
接下来我们再来理解上面那段话的第二句,你肯定有一个疑问:什么叫线程安全?
我们知道Python解释器中有一个垃圾回收的功能,这个垃圾回收的功能是在后台运行的,和你写的程序是并发执行的,所以你运行一个Python程序的时候起的一个进程内还会有一个垃圾回收的线程,这个线程不会一直存在,但是它会由解释器定期的启动,所以你会发现:主线程,“子线程”和垃圾回收线程都是要运行Python解释器的功能,如下图所示

假如没有GIL的存在,在多核CPU的情况下,就会出现并行,你写的代码里面一个线程刚好的一个数据还没来得及做绑定关系,垃圾回收线程看到这个没用的数据就要准备把它删掉,这时Python解释器应该怎么做,他很为难呀,如果让这两个线程同时执行成功,那一定会出错的,垃圾回收机制毋庸置疑有它存在的必要,既然不能同时运行,为了保证数据安全,那就只好一个一个来。要想保证一个进程内同一时间只有一个线程运行,那就是加锁处理,这把锁加在解释器身上,这就是全局解释器锁。
注意:GIL是保证解释器级别的数据安全,我们自己写的程序中的数据如果开多线程要保证数据安全那么还是需要自己加锁处理的,这两把锁不能混淆。
这里有一篇文章深入讲解了GIL对多线程的影响,有兴趣的同学可以看一下 http://www.dabeaz.com/python/UnderstandingGIL.pdf 
GIL的优缺点:

优点:保证了CPython解释器内存管理的线程安全
缺点:同一进程内的多个线程同一时刻只能有一个执行,也就是CPython解释器多线程无法实现并行
     也就是意味着无法利用CPU多核优势,但是并发没有任何问题,因为遇到当某个线程遇到IO,
     还是要把锁交出来的

至此,很多人不了解Python的人到了这里基本上就明白了,Python原来就是这么个吊玩意,还是PHP好啊,全世界最牛逼的语言。
如果真的是这么下结论,那还是没有真正理解并发编程。
CPU的作用是运算而不是IO,如果程序中大部分都是IO,即使你的电脑是一万个核的CPU那你也得给我等着。
假如你是一个老板,雇用了100个工人来干活,这100个工人就是相当于是100个CPU,但是你的原材料分别要从南非,美国,巴西,意大利,缅甸,月球和火星运过来,每到一批原材料10个工人很快就能做完了,大部分时间大部分工人都在打牌。。。。。。
所以,如果是你的程序的任务是IO密集型,开多线程使用单核CPU和多核CPU执行效率的差距微乎其微,但是如果你的程序是计算密集型,肯定是多核计算的更快,这时使用多线程是不合理的,可以使用多进程来完成。

# 分析:
我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程
# 单核情况下,分析结果: 
  如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
  如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜
# 多核情况下,分析结果:
  如果四个任务是计算密集型,多核意味着并行计算,在Python中一个进程中同一时刻只有一个线
   程执行用不上多核,方案一胜
  如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜
 
# 结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大
# 性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。

九 进程池与线程池

之前我们写的套接字通信不能实现并发,现在我们可以利用多线程来对他进行修改一下。
服务端代码

from socket import *
from threading import Thread
def communicate(conn, client_address):
    while True:  
        try:
            data = conn.recv(1024)
            if not data: break
            conn.send(data.upper())
        except ConnectionResetError:
            break
    conn.close()
def server():
    server = socket(AF_INET, SOCK_STREAM)
    server.bind(('127.0.0.1', 8080))
    server.listen(5)
    while True:  # 链接循环
        conn, client_address = server.accept()
        print(client_address)
        t = Thread(target=communicate, args=(conn, client_address))
        t.start()
    server.close()
if __name__ == '__main__':
    server()

客户端代码

from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
    msg = input('>>>: ').strip()
    if not msg: continue
    client.send(msg.encode('utf-8'))
    data = client.recv(1024)
    print(data.decode('utf-8'))
client.close()

我们现在自己测试可能也就会有几个客户端,但是如果是上亿个客户端,虽然开线程的开销小,但是线程也不能无限的开启,随着客户端不断的增多,服务器开启的线程数量会不断的攀升,直到你的服务端机器卡死,瘫痪甚至爆炸(吓唬你们一下啊,不会炸的,但是后果很严重,老板很生气,老板炸了)。
我们不能无限的开进程,也不能无限的开线程,核心的问题就是要控制这个数量,进程或者线程的数量都要在当前环境下可承受的范围之内,这个工作一般是由测试人员和运维人员来负责完成的,这时候我们引入了“池”的概念,使用它就是为了限制并发任务数目,限制我们的机器在一个自己可承受的范围内去并发的执行任务,接下来要说的就是进程池和线程池。
当任务是计算密集型,这是时候应该用多进程,那么也就应用用进程池。当任务是IO密集型,这时候应该用多线程,那么也就应该用线程池。“池”只是一个思路,并不影响多进程或者多线程的使用区别。
进程池与线程池的用法一样,我们先来看一下进程池。

from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time, os, random
print(os.cpu_count())  # 打印CPU核数
 
def task(x):
    print('%s is running' % os.getpid())
    time.sleep(random.randint(2, 5)) 
    return x ** 2
if __name__ == '__main__':
    p = ProcessPoolExecutor(max_workers=5)  # 不传参数默认开启的进程数是cpu的核数
    for i in range(20):
        p.submit(task, i)  # 分配任务

再来看一下线程池

from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time, random
def task(x):
    print('%s is running' % x)
    time.sleep(random.randint(2, 5))
    return x ** 2
if __name__ == '__main__':
    p = ThreadPoolExecutor(50)  # 默认开启的线程数是cpu的核数*5
    for i in range(200):
        p.submit(task, i)

现在我们再来用线程池改写套接字通信程序
服务端代码(客户端不变)

from socket import *
from concurrent.futures import ThreadPoolExecutor
thread_pool = ThreadPoolExecutor(50)
def communicate(conn, client_address):
    while True:
        try:
            data = conn.recv(1024)
            if not data: break
            conn.send(data.upper())
        except ConnectionResetError:
            break
    conn.close()
def server():
    server = socket(AF_INET, SOCK_STREAM)
    server.bind(('127.0.0.1', 8080))
    server.listen(5)
    while True:  
        conn, client_address = server.accept()
        print(client_address)
        # t = Thread(target=communicate, args=(conn, client_address))
        # t.start()
        thread_pool.submit(communicate, conn, client_address)
    server.close()
if __name__ == '__main__':
    server()

十 同步与异步

同步与异步指的是提交任务的两种方式,这与阻塞或者非阻塞是没有直接关系的,可能有些人会有一种山炮逻辑对这两类概念混淆。

同步调用:提交完任务之后,就在原地等待,直到任务运行完毕后,拿到任务的返回值,
        才继续执行下一行代码。
异步调用:提交完任务之后,不在原地等待,直接执行下一行代码。


很明显上图我们写的代码提交任务的方式是异步的,那么我们怎么拿到任务的结果呢?
站在线程池的角度考虑,我们要等这个线程池任务结束之后,然后统一的再拿结果,等待一个任务结束就是使用p.join(),但是线程池没办法结束,因为不断的有任务进进出出,所以我们要先关闭线程池的入口p.close(),然后在执行p.join(),这样他才有可能结束。
以前的老池就是使用这两个方法,现在我们用的新池把这两个方法合并成了一个方法。

from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time, random
"""
from multiprocessing import pool
这是以前老的进程池
"""
def task(x):
    print('%s is running' % x)
    time.sleep(random.randint(2, 5))
    return x ** 2
if __name__ == '__main__':
    p = ThreadPoolExecutor(30)
    obj_list = []
    for i in range(100):
        obj = p.submit(task, i)  # submit的结果是一个对象
        obj_list.append(obj)
    p.shutdown()  # 先关闭入口,再等待结束
    print(obj_list[0].result())  # 取第1个对象并返回结果
    print(obj_list[1].result())  # 取第2个对象并返回结果
    print(obj_list[2].result())  # 取第3个对象并返回结果
    print(obj_list[3].result())  # 取第4个对象并返回结果
    print('主')

上面这种就是一个异步调用,如果我们想把它改成同步调用呢?

from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time, random
"""
from multiprocessing import pool
这是以前老的进程池
"""
def task(x):
    print('%s is running' % x)
    time.sleep(random.randint(2, 5))
    return x ** 2
if __name__ == '__main__':
    p = ThreadPoolExecutor(30)
    for i in range(10):
        res = p.submit(task, i).result()
        print(res)
    print('主')

十一 线程queue

queue就是队列,线程queue就是线程队列,它的用法很简单,这里我们说一下队列的三种形式。

import queue
# 队列:先进先出
q = queue.Queue(3)
q.put(1)
q.put(2)
q.put(3)
# q.put(4)  # 阻塞
print(q.get())
print(q.get())
print(q.get())
# 堆栈:后进先出(last in first out queue)
q = queue.LifoQueue(3)
q.put('a')
q.put('b')
q.put('c')
print(q.get())
print(q.get())
print(q.get())
# 优先级队列:
"""
可以以元组或者列表的形式往队列里存值,不能混用,
第一个元素代表优先级,数字越小优先级越高
用元组的时候可以用小数,不可以用0,用列表的时候可以用0,不可以用小数(这个不重要,自己用一个就好)
"""
q = queue.PriorityQueue(3)
q.put((10, 'user1'))
q.put((-3, 'user2'))
q.put((-1.1, 'user3'))
# q.put((0, 'user4'))  # 不能用0
"""
q.put([2, 'user1'])
q.put([-5, 'user2'])
q.put([0, 'user3'])
# q.put([1.2, 'user4'])  # 不能放小数
"""
print(q.get())
print(q.get())
print(q.get())

十二 线程Event

线程event是用来实现在同一个进程内的多个线程协同工作的,比如说有有两个线程,其中一个线程把任务做完了,他必须要发一个通知信号,另外一个线程才能进行工作,这也就是一个A线程工作到了某一个点A线程会通知另外一个B线程开始工作。这种场景就是一个任务要向套接字服务端发连接请求, 但是如果服务端瘫痪,他就会直接报错了,所以我们可以先让发送连接请求的任务等一下,等上面会先有一个【线程1】去检测服务端是否存活, 如果存活的话,【线程1】会发送一个信号告诉【线程2】现在可以去连接服务端了。
如果是多进程,那么只能使用队列了,但是对于多线程,处理起来比较简单,修改全局变量就可以了。
现在有了Event就可以取代这个工作,让他变得更简单。

from threading import Event, current_thread, Thread
import time
# success = False  # 被取代
event = Event()
def check():
    print('%s 正在检测服务是否正常....' % current_thread().name)
    time.sleep(5)
    # global success  # 被取代
    # success = True  # 被取代
    event.set()  # success由false改成了True
def connect():
    print('%s 等待连接...' % current_thread().name)
    """
        while True:
        if not success:
            time.sleep(1)
            continue
        else:
            break
    """
    event.wait()  # 取代了上面注释掉的代码
    print('%s 开始连接...' % current_thread().name)
if __name__ == '__main__':
    t1 = Thread(target=connect)
    t2 = Thread(target=connect)
    t3 = Thread(target=connect)
    c1 = Thread(target=check)
    t1.start()
    t2.start()
    t3.start()
    c1.start()

event.wait()可以接一个参数,就是等待时间,默认是无限等待的,我们肯定不能让用户无限的等待,如果服务端机器繁忙或者用户的网速不好,需要做的是让用户重复等待3次之后,如果还不能开始连接,就返回给用户一个提示信息“你的网速太差了,请稍后重试”,这个时候我们应该有一种方式监测event.set()是否设置成功,可以使用event.is_set()来检测,他的返回值就是True或者False。

posted @ 2019-04-09 18:28  马一特  阅读(96)  评论(0编辑  收藏  举报