11-04 多线程

一. 什么是线程

进程: 资源单位. 每个进程自带一个原生线程. 起一个进程仅仅只是在内存空间中开辟一块独立的空间.

线程: 执行单位. 真正被CPU执行的其实是进程里面的线程,线程指的就是代码的执行过程, 执行代码中所需要使用到的数据或者叫资源,都是去找其所在的进程索要.

举例: 将操作系统比喻成一个大的工厂,进程就像当于工厂里面的车间,而线程就是车间里面的流水线.

提示: 进程和线程都是虚拟单位,只是为了我们更加方便的描述问题.

二. 为何要有线程

开设进程: 
    1. 申请内存空间 耗资源
    2. '拷贝代码'  耗资源

开设线程: 一个进程内可以开设多个线程. 在进程内开设多个线,无需重新申请内存空间.

总结:
    1. 开设线程的开销要远远小于线程的开销.
    2. 同一个进程下的多个线程数据是共享的.

三. 如何使用线程?

举例: 我们要开发一款文本编辑器.
    功能1: 获取用户输入的功能.
    功能2: 实时展示到屏幕的功能
    功能3: 自动保存到硬盘的功能
    针对上面这三个功能,开设进程还是线程合适???

如果开设多个进程, 那么进程之间内存空间互相隔离, 如果想要执行以上3种功能, 不仅需要消耗申请内存空间开设进程的资源, 还需要建立管道, 如果多个进程之间通信, 更需要使用消费者生产者模型的支持.
如果开设多个线程, 我们知道线程之间数据的获取都是在自己进程内, 且对于多个线程来说都是共享的, 因此访问数据,修改数据,保存数据都很方便.

四. 开启线程的两种方式

1. 第一种方式:

import time
from threading import Thread

def task(name):
    print(f'{name} is running...')
    time.sleep(1)
    print(f'{name} is over...')

# 提示: 开启线程不需要在main下面执行代码直接书写就可以,但是我们还是习惯性的将启动命令写在main下面.
if __name__ == '__main__':
    t = Thread(target=task, args=('egon', ))
    t.start()
    print('主')

    # 执行结果: 
    '''
    egon is running...   # 注意: 线程的开启远远快与进程, 因此start完毕就直接会执行子线程中你的代码了.
    主 
    egon is over...
    '''

2. 第二种方式: 类的继承

import time
from threading import Thread

class MyThread(Thread):
    def __init__(self, name):
        '''针对双个下划线开头双下划线结尾的(__init__)方法 统一读成`双下init`'''
        super().__init__()
        self.name = name

    def run(self):
        print(f'{self.name} is running...')
        time.sleep(1)
        print(f'{self.name} is over...')

if __name__ == '__main__':
    t = MyThread('egon')
    t.start()
    print('主')

    # 执行结果:
    '''
    egon is running...   # 注意: 线程的开启远远快与进程, 因此start完毕就直接会执行子线程中你的代码了.
    主 
    egon is over...
    '''

五. 在一个进程下开启多个线程与在一个进程下开启多个子进程的区别

1. 谁的开启速度快

答案: 在一个进程下开启多个线程速度快

from threading import Thread
from multiprocessing import Process


def work():
    print('hello')


if __name__ == '__main__':
    # 在主进程下开启线程
    t = Thread(target=work)
    t.start()
    print('主线程/主进程')

    # 打印结果:
    '''
    hello
    主线程/主进程
    '''

    # 在主进程下开启子进程
    t = Process(target=work)
    t.start()
    print('主线程/主进程')
    # 打印结果:
    '''
    主线程/主进程
    hello
    '''

2. 瞅一瞅pid

from threading import Thread
from multiprocessing import Process
import os


def work():
    print('hello', os.getpid())


if __name__ == '__main__':
    # part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
    t1 = Thread(target=work)
    t2 = Thread(target=work)
    t1.start()
    t2.start()
    print('主线程/主进程pid', os.getpid())
    # 执行结果:
    '''
    hello 7136
    hello 主线程/主进程pid 7136
    7136
    '''

    # part2:开多个进程,每个进程都有不同的pid
    p1 = Process(target=work)
    p2 = Process(target=work)
    p1.start()
    p2.start()
    print('主线程/主进程pid', os.getpid())
    # 执行结果:
    '''
    主线程/主进程pid 7136
    hello 12752
    hello 12748
    '''

3. 同一个进程下的数据被多个线程共享

from threading import Thread
from multiprocessing import Process


def work():
    global n
    n = 0


if __name__ == '__main__':
    n = 100
    p = Process(target=work)
    p.start()
    p.join()
    print('主', n)  # 毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100. 执行结果: 主 100

    n = 1
    t = Thread(target=work)
    t.start()
    t.join()
    print('主', n)  # 查看结果为0,因为同一进程内的线程之间共享进程内的数据. 执行结果: 主 0

六. TCP服务端实现并发的效果

1. TCP服务端

'''
服务端必备3要素:
    1. 要有固定的Ip和端, 为让客户端能够通过这个固定的IP和端口访问到我们服务端,
    2. 24小时不间断提供服务。
    3. 能够支持并发,要提供给多个人服务.
'''
import socket
from threading import Thread
from multiprocessing import Process

server = socket.socket()  # 括号内不加参数: 第一个参数默认地址家族就是AF_INET, 第二个参数默认协议就是SOCK_STREAM
server.bind(('127.0.0.1', 8080))
server.listen(5)

def communication(conn):
    while True:  # 通讯循环
        try:
            data_bytes = conn.recv(1024)
            if not data_bytes:
                break
            conn.send(data_bytes.upper())
        except ConnectionResetError as e:
            print(e)
            break
    conn.close()

while True:  # 链接循环
    conn, client_address = server.accept()
    print(client_address)
    # p = Process(target=communication, args=(conn, ))
    # p.start()
    t = Thread(target=communication, args=(conn, ))
    t.start()

2. TCP客户端

import socket

client = socket.socket()
client.connect(('127.0.0.1', 8080))

while True:
    msg = input(">>:").strip()
    if not msg:
        continue

    client.send(msg.encode('utf-8'))
    data_bytes = client.recv(1024)
    print(data_bytes.decode('utf-8'))

七. 线程对象的join方法

import time
from threading import Thread


def task(name):
    print(f'{name} is running...')
    time.sleep(1)
    print(f'{name} is over...')

if __name__ == '__main__':
    t = Thread(target=task, args=('egon', ))
    t.start()
    t.join()  # 主线程等待子线程运行完毕后再执行。
    print('主')
    # 执行结果:
    '''
    egon is running...
    egon is over...
    主
    ''' 

八. 线程对象属性及其它方法

'''
current_thread().name: 统计当前线程名. 如果是主线程显示为MinThrad. 如果是子线程,第一个子线程为Thread-1,第二个子线程为Thread-2依次类推.
active_count(): 统计当前正在活跃的线程数.
'''
import os
import time
from threading import Thread
from threading import current_thread
from threading import active_count

def task(name, n):
    print(f'子线程{name}的PID号:', os.getpid())
    print(f'主线程{name}的线程名:', current_thread().name, end='\n\n')
    time.sleep(n)

if __name__ == '__main__':
    t = Thread(target=task, args=('egon', 1, ))
    t1 = Thread(target=task, args=('tank', 2, ))
    t.start()
    t1.start()
    t.join()
    print('统计当前正在活跃的线程数:', active_count())
    print('主进程的PID号:', os.getpid())
    print('主进程的线程名:', current_thread().name)

    # 执行结果
    '''
    子线程egon的PID号: 9300   # 子线程的pid号用的就是进程pid号,都是同一个.
    主线程egon的线程名: Thread-1
    
    子线程tank的PID号: 9300
    主线程tank的线程名: Thread-2
    
    统计当前正在活跃的线程数: 2
    主进程的PID号: 9300
    主进程的线程名: MainThread
    '''

九. 守护线程

'''
# 注意注意注意!!!: 代码执行完毕并不代表结束.

线程进程对比:
    进程: 父进程代码执行完毕, 守护进程会立即被回收(回收守护进程的PID等等资源), 但是父进程不会立即结束, 父进程会等待所有的非守护的子进程结束, 父进程会回收结束的子进程的PID号等资源, 回收完毕以后父进程才会结束. 
        
    线程: 主线程结束, 守护线程立即结束. 主线程要等待所有非守护线程结束才结束, 且主线程一旦结束主线程所在的进程也会结束. 进程的整体资源都会被回收, 而进程必须保证所有的非守护的子进程结束.

    问题:为什么主线程要等待子线程结束以后主线程才结束?
        因为主线程的结束就意味着所在的进程的结束. 
'''
import time
from threading import Thread

def task(name):
    print(f'{name} is running...')
    time.sleep(1)
    print(f'{name} is over...')

if __name__ == '__main__':
    t = Thread(target=task, args=('egon', ))
    t.daemon = True
    t.start()
    print('主')

    # 执行结果:
    '''
    egon is running...
    主
    '''

稍微有一点迷惑性的例子:

from threading import Thread
import time

def foo():
    print(123)
    time.sleep(1)
    print('end123')

def func():
    print(456)
    time.sleep(3)
    print('end456')

if __name__ == '__main__':
    t1 = Thread(target=foo)
    t2 = Thread(target=func)
    t1.daemon = True
    t1.start()
    t2.start()
    print('主')

    # 执行结果:
    '''
    123
    456
    主
    end123
    end456
    '''

十. 线程互斥锁

问题: 在涉及到多个人操作同一份数据的情况下出现的数据混乱的问题

 
import time
from threading import Thread

money = 100

def task():
    global money
    tmp = money      # 模拟从数据库中拿到money
    time.sleep(1)    # 模拟用户网络延迟
    money = tmp - 1  # 默认数据money被修改

if __name__ == '__main__':
    t_list = []
    for i in range(100):
        t = Thread(target=task)
        t.start()
        t_list.append(t)

    for t in t_list:
        t.join()

    print(f'主:', money)  # 99

解决: 因此我们因该加锁进程处理, 将程序的并发变成串行, 虽然牺牲了程序的运行效率, 但是保证了数据的安全性.

'''
注意1: 并不是所有的情况下都需要加锁问题, 只涉及到多个人操作同一份数据的情况下使用.
注意2: 锁不要轻易使用, 容易造成死锁现象
'''
import time
from threading import Thread
from threading import Lock
# from multiprocessing import Lock  # 这里的Lock与上面的Lock都是一样的概念.

mutex = Lock()
money = 100

def task():
    global money
    # 加锁写法一: 推荐, 寓意更明确
    mutex.acquire()
    tmp = money        # 模拟从数据库中拿到money
    time.sleep(0.1)    # 模拟用户网络延迟
    money = tmp - 1    # 默认数据money被修改
    mutex.release()
    
    # 加锁写法二:
    '''
    with mutex:
        tmp = money        # 模拟从数据库中拿到money
        time.sleep(0.1)    # 模拟用户网络延迟
        money = tmp - 1    # 默认数据money被修改
    '''
        
if __name__ == '__main__':
    t_list = []
    for i in range(100):
        t = Thread(target=task)
        t.start()
        t_list.append(t)

    for t in t_list:
        t.join()

    print(f'主:', money)  # 0        

十一. GIL全局解释器锁(难点)

1. GIL介绍(Global Interpreter Lock)



'''
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. (However, since the GIL 
exists, other features have grown to depend on the guarantees that it enforces.)
'''
上文分析:
    1. python解释器有多个版本: Cpython, Jpython, Pypython. 最普遍使用的就是Cpython解释器。
    2. 在Cpython解释器才有GIL这种全局解释器锁(互斥锁), 它是用来阻止同一个进程下的多个线程同时运行的, 是因为在每个开启的进程当中都包含一个垃圾回收机制(GC)线程的存在, 垃圾回收机制中的标记/清除算法, 会遍历堆区的所有对象, 
    将没有标记为0的对象清楚. 因为垃圾回收机制的存在, 很可能某个线程刚开启并执行的过程中某个即将被执行的值还没有被标记, 就被垃圾回收机制线程拿到了python解释器的交给CPU执行, 执行完毕以后刚刚的值就被清除了.
    3. Cpython中的内存管理不是线程安全的, 因此我们才需要使用GIL.
    4. 同一个进程下的多个线程无法利用多核优势!!!
    
问题:  既然同一个进程下的多个线程无法利用多核优势, 是不是就没有一点用没有???


重点总结:
    1. GIL是Cpython解释器的特点 不是python的特点!!!
    2. GIL本质也是一把互斥锁, 但它是解释器级别的锁. 争对不同级别的数据需要用不同的锁处理. 比如: 争对解释器级别的数据, 使用了GIL. 争对用户级别的数据, 使用了Lock. 
    3. GIL的存在是因为Cpyhotn解释器的内存管理不是线程安全的.
    2. GIL的存在虽然让同一个进程下的多个线程无法同时执行,但保证了解释器级别的数据的安全. ==> 垃圾回收机制
        1) 什么是垃圾回收机制?
            垃圾回收机制简称GC, 是python解释器自带的一种机制, 专门用来回收不可用的变量值所占用的内存空间.

        2) 为什么要用垃圾回收机制?
            程序的运行过程中会申请大量的内存空间, 而对于一些无用的内存空间如果不及时清理的话会导致内存空间是用殆尽, 进而造成内存溢出, 最终导致程序奔溃. 但是管理内存是一件非常繁琐且复杂的事情, python则提供了垃圾回收机制来帮我们程序从复杂的内存管理中解放出来.

        3) 垃圾回收机制的三大算法工作模式:
            1) 引用计数: 跟踪回收垃圾
                引用计数又分直接应用, 间接引用. 如果'值'引用计数为0, '值'占用的内存地址将会被垃圾回收机制回收.

            2) 标记/清除: 解决容器类型的循环引用问题
                执行前提: 当应用程序可用内存空间快被耗尽.
                标记: 有根之人当活, 无根之人当死. 根指的栈区, 也就是说可以通过栈区间接或者直接可以访问到堆区的对象的数据会被保留. 否则执行清除. 
                清除: 遍历堆区中所有对象, 将没有标记的对象全部清除.

            3) 分代回收: 解决引用计数每次回收内存都需要遍历所有对象的效率问题.
                根据存活时间划分扫描频率. 刚来的数据权重最低, 扫描频率最高. 数据存活时间越长权重越高, 扫描频率越低.
            
    4. GIL间接导致了在同一个进程下多个线程无法同时执行, 从而无法利用多核优势.
    5. 解释器语言的通病: 同一个进程下的多个线程无法利用多核优势.

img

图解:

1. python的代码的执行必须要先拿到解释器,让解释器和CPU打交道,才能去执行你的代码. 每个进程下都会有一个独立的解释器在进程内, 还有一个垃圾回收线程。
2. 如果多个线程同时运行,当我的代码刚刚申请,还没有与变量名进行绑定. 同时运行的情况下, 垃圾回收线程也拿到了CPU的执行权限, 那垃圾回收机制就会检测,将你这些没有被绑定的变量的这个数据回收。 所以问题就是,为了保证解释器级别的对一份数据的安全性的问题, 这个时候就需要用到了锁的概念,让所有线程不能同时去运行,且这个锁要加在解释器上面,
3. 一但由某个线程要运行,必须要抢GIL(全局解释器锁), 这样才能让CPU执行. 一旦你的代码中遇到了IO问题, CPU就会切换运行态变成堵塞态, 解释器收到CPU阻塞的命令就会释放GIL, 让下一个排队的线程去获取解释器让CPU执行.

2. GIL与普通互斥锁的区别

import time
from threading import Thread
from threading import Lock

mutex = Lock()
money = 100

def task():
    global money
    
    '''
    1. 100个线程起来以后, 都要先去抢GIL, 当线程A抢到了GIL, 接下来就开始执行以下代码.在执行以下代码的过程中线程A执行到mutex拿到了一把锁, 并接着执行, 当执行到了sleep这种IO操作时, GIL将自动释放, 线程A没有执行以下代码的权限了.
    2. 于此同时线程B,C,D...过来抢GIL锁获取执行以下代码的权限. 如果GIL被线程B抢到了, 线程B将开始执行以下代码, 线程B突然发现有一把新的锁mutex挡在它的前面, 且这把锁已经被之间的线程A拿着, 所以线程B陷入了等待线程A释放mutex锁的IO中, 线程B陷入了IO了, GIL将又会释放, 接着线程B也没有执行以下代码的权限了.
    3. 第1步中线程A没有释放mutex锁的过程中, 一直会执行第2步. 线程抢GIL, 遇见mutex的IO, 释放GIL的循环.
    4. 这时线程A的sleep的IO结束, 在轮流获取GIL中, 某一次线程A再次拿到了GIL, 且线程A还有mutex, 由CPU保存的状态接着之前的状态继续运行, 线程A执行完毕以后, release释放了锁, 线程A因此执行完毕了, 释放了GIL锁.
    5. 接着又接着步骤2, 线程B先拿到GIL获取执行权限, 接着线程B获取到了mutex, 可以执行以下代码了.结果又遇见了sleep这种IO问题, 毫无疑问也会释放GIL. 
    6. 线程B释放了GIL, 又开始循环步骤2. 直到线程B释放mutex, 将又开始执行步骤3,4. 接下来的所有只要是没拿到mutex则一直抢GIL, 释放GIL. 直到有个线程释放mutex, 在抢到了GIL基础之上再拿到mutex锁.依此类推....
    '''
    mutex.acquire()
    tmp = money
    time.sleep(0.5)
    money = tmp - 1
    mutex.release()

if __name__ == '__main__':
    t_list = []
    for i in range(100):
        t = Thread(target=task)
        t.start()
        t_list.append(t)

    for t in t_list:
        t.join()

    print('主:', money)

十二. 同一个进程下的多个线程无法利用多核优势是不是就没有用了?

1. 分析: 多线程的作用要看具体情况, 不能一概而论.

# 之前疑问: 同一个进程下的多线程无法利用多核优势,是不是就没有用了???

举例: (提示: 多核可以开启多个进程并行运行, 且以下不包括进程间切换, 线程间切换, 申请资源等时间) 
1. 当执行计算密集型的任务每个任务都需要10s. 一共4个任务.
    四核: 
        1) 四个核只有一个核会处理开启的单个进程下的多线程并发运行, 但是计算密集型没有IO问题, 应为GIL本质还是串行. 耗时10s + 10s + 10s + 10s. 
        2) 四个核每个核处理开启的四个进程并行运行. 耗时10s
    
2. 当执行IO密集型的任务时每个任务都会堵塞40s. 一共4个任务
    四核: 
        1) 四个核只有一个核会处理开启的单个进程下的四线程并发运行, 因为IO问题, 会来回切换. 耗时40s. 优点: 不需要额外的开销, 更加节省资源.
        2) 四个核每个核处理开启的四个进程并行运行. 耗时40s. 缺点: 需要额外的开销, 浪费资源.

2. 多进程与多线程的实际应用场景

计算密集型:

import time
import os
from multiprocessing import Process
from threading import Thread

def task():
    res = 1
    for i in range(100000000):
        res += i

if __name__ == '__main__':
    # print(os.cpu_count())  # 先查看CPU的核数用来判断应该开设几个进程.  结果: 4

    # 开设4进程
    start_time = time.time()
    p_list = []
    for i in range(4):
        p = Process(target=task)
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()
    stop_time = time.time()
    print('开设4进程执行时间:', stop_time-start_time)  # 开设4进程执行时间: 7.227627515792847

    # 开设4线程
    start_time = time.time()
    t_list = []
    for i in range(4):
        t = Thread(target=task)
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    stop_time = time.time()
    print('开设4线程执行时间:', stop_time-start_time)  # 开设4线程执行时间: 25.81493353843689

IO密集型:


import time
import os
from multiprocessing import Process
from threading import Thread

def task():
    time.sleep(3)

if __name__ == '__main__':
    # print(os.cpu_count())  # 先查看CPU的核数用来判断应该开设几个进程.  结果: 4

    # 开设4进程
    start_time = time.time()
    p_list = []
    for i in range(4):
        p = Process(target=task)
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()
    stop_time = time.time()
    print('开设4进程执行时间:', stop_time-start_time)  # 开设4进程执行时间: 3.1007001399993896

    # 开设4线程
    start_time = time.time()
    t_list = []
    for i in range(4):
        t = Thread(target=task)
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    stop_time = time.time()
    print('开设4线程执行时间:', stop_time-start_time)  # 开设4线程执行时间: 3.0021445751190186

3. 总结

"""
1. 多线程是否有用就要看具体情况, 任务分两种方式:一种是计算密集型,另一种是Io密集型的任务,遇到计算密集型任务就是用多进程, 利用了多核优势. 遇到Io密集型的任务就是用多线程, 可以节省资源的消耗.
2. 多进程和多线程都有各自的优势,并且我们后面在写项目的时候,都是通过多进程下面开设多线程,这样的话,既可以在计算密集状态下利用多核优势,也可以在IO密集状态下使用多线程进而节省资源的消耗.
"""

十三. 死锁与递归锁(了解)

1. 死锁Lock

当你知道使用锁时抢锁必须要释放锁,其实你在操作锁的时候也极其容易产生死锁现象, 死锁现象会造成整个程序处于阻塞, 进而程序无法正常运行.

import time
from threading import Thread
from threading import Lock

'''
类只要加括号调用多次, 在没有特殊出理的情况下, 产生的肯定是不同的对象.
如果你想要实现多次加括号调用得到的是相同的对象那么请使用单例模式. 在本网址第七节中会讲到:

注意!!!:  Lock锁不支持多次的acquire和release, 这种连续是争对同一个互斥锁对象.
'''
mutexA = Lock()
mutexB = Lock()
# print(mutexA is mutexB)  # False


class MyThead(Thread):
    def run(self):
        self.func1()
        self.func2()

    def func1(self):
        mutexA.acquire()
        print('%s 抢到A锁' % self.name)  # self.name 获取的是当前线程对象的线程名
        mutexB.acquire()  # 引发问题(先): Thread一2抢不到B锁. 此时B锁在func2中正在执行的Thread一1手上, Thread一1抢到了B锁还没有释放
        print('%s 抢到B锁' % self.name)
        mutexB.release()
        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('%s 抢到B锁' % self.name)
        time.sleep(2)
        mutexA.acquire()  # 引发问题(后): Thread一1抢不到A锁. 此时A锁在Thread一2手上, Thread一2抢到了A锁还没有释放
        print('%s 抢到A锁' % self.name)  # self.name获取的是当前线程对象的线程名
        mutexA.release()
        mutexB.release()


if __name__ == '__main__':
    for i in range(10):
        t = MyThead()
        t.start()

    # 执行结果:
    '''
    Thread一1 抢到A锁
    Thread一1 抢到B锁
    Thread一1 抢到B锁
    Thread一2 抢到A锁
    # 这里引发了死锁现象, 整个程序阻塞住了.
    '''
    
# 补充: 
'''
self.name: 获取的是当前线程对象的线程名. self是自己派生Thread类实例化出来的线程对象, 通过改对象也能访问到继承类Thread中的方法. 
current_thread().name: 获取的是当前线程的线程名
'''

2. 递归锁RLock

'''
RLock中的R --> recursion   /rɪˈkɜːʃn/ 递归 递归 递回

递归锁(RLock)的特点:  
    1. 可以被连续的acquire和release., 但是只能被第一个抢到这把锁执行锁内的操作
    2. 它的内部有个计数器 每acquire一次计数加一, 每release一次计数减一, 只要计数不为0那么其他人都无法抢到该锁.
    
注意!!!: RLock可以连续的acquire和release, 这种连续是也争对同一个互斥锁对象.    
'''

import time
from threading import Thread
from threading import RLock 

mutexB = mutexA = RLock()
# print(mutexA is mutexB)  # True


class MyThead(Thread):
    def run(self):
        self.func1()
        self.func2()

    def func1(self):
        mutexA.acquire()
        print('%s 抢到A锁' % self.name)  # self.name获取的是当前线程对象的线程名
        mutexB.acquire()
        print('%s 抢到B锁' % self.name)
        mutexB.release()
        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('%s 抢到B锁' % self.name)
        time.sleep(2)
        mutexA.acquire()
        print('%s 抢到A锁' % self.name)  # self.name获取的是当前线程对象的线程名
        mutexA.release()
        mutexB.release()


if __name__ == '__main__':
    for i in range(10):
        t = MyThead()
        t.start()

十四. 信号量Semaphore(了解) ==> 锁

信号量在不同的阶段可能对应不同的技术点,但在并发编程中信号量指的就是锁!!!

'''
Semaphore  /ˈseməfɔː(r)/  信号量 信号灯 信号量

举例: 之前我们将互斥锁比喻成一个厕所, 多个人过来争抢厕所(acquire), 只有上完厕所的那个人出来了(release), 接着才能下一个人继续(acquire). 那么我们的信号量在这里就相当于多个厕所.
'''

import time
import random
from threading import Thread
from threading import Semaphore


'''
利用random模块实现打印随机验证码(搜狗的一道笔试题)
'''
sm = Semaphore(5)  # 括号内写数字 写几就表示开设几个坑位. 默认不指定value=1


def task(name):
    sm.acquire()
    print('%s 正在蹲坑' % name)
    time.sleep(random.randint(1, 5))
    sm.release()


if __name__ == '__main__':
    for i in range(10):
        t = Thread(target=task, args=('伞兵%s号' % i,))
        t.start()

十五. Event事件(了解)

Event事件作用: 控制一些进程或者线程需要等待另外一些进程或者线程运行完毕之后才能运行.

'''
举例: 如果是在一个车间(进程)里面,有一个流水线(线程)是提供原材料的,另一个流水线(线程)是制作原材料的, 那么进行包装的那个流水线(线程)就需要等待,准备原材料的流水线(线程)准备完毕.

应用举例: 汽车只有绿灯才能通行(event.set()), 如果遇到红灯就得等待(even.wait()). 类似于发射信号一样.
    - 红灯时: 这种等待的信号就会发送给司机, 让司机等待
    - 绿灯时: 司机收到绿灯的信号才能过马路. 
'''

from threading import Thread
from threading import Event
import time

event = Event()  # 造了一个红绿灯


def light():
    print('红灯亮着的')
    time.sleep(3)
    print('绿灯亮了')
    # 告诉等待红灯的人可以走了
    event.set()


def car(name):
    print('%s 车正在灯红灯' % name)
    event.wait()  # 等待别人给你发信号
    print('%s 车加油门飙车走了' % name)


if __name__ == '__main__':
    t = Thread(target=light)
    t.start()

    for i in range(5):
        t = Thread(target=car, args=('%s' % i,))
        t.start()

    # 执行结果:
    '''
    红灯亮着的
    0 车正在灯红灯
    1 车正在灯红灯
    2 车正在灯红灯
    3 车正在灯红灯
    4 车正在灯红灯
    绿灯亮了
    3 车加油门飙车走了
    2 车加油门飙车走了
    4 车加油门飙车走了
    1 车加油门飙车走了
    0 车加油门飙车走了
    '''

十六. 线程queue(了解)

# 前言: 之前我们说进程之间内存空间互相隔离,数据不是共享的,我们才需要需要用到队列.

同一个进程下的多个线程数据是共享的, 为什么在一个进程下还要去使用列队呢?
    解答: 因为队列是管道+锁,使用用队列是为了保证数据的安全.
    
提示: 我们现在使用的队列都是只能在本地测试使用,以后我们要使用的队列是基于别人封装好的,例如: radius, kafka, RQ...

提示: 以下三种方式都是通过import导入queue模块下的类实现, 返回的都是队列对象.

1. 先进先出队列Queue

import queue

q = queue.Queue(3)
q.put(1)  # 队列中添加值大于上面Queue的指定数处于阻塞状态  
q.get()   # 队列中没有值可取处于阻塞状态        
q.get_nowait()    # 队列中没有值可取抛出异常: queue.empty
q.get(timeout=3)  # 等待3秒钟以后队列中没有值可取抛出异常: queue.empty
q.full()   # 判断队列是否还有值
q.empty()  # 判断队列是否没有值

2. 后进先出队列LifoQueue

import queue

# 队列LifoQueue: 后进先出队列 LIFO. 类似于堆栈的先进后出.
q = queue.LifoQueue()
q.put(1)
q.put(2)
q.put(3)
print(q.get())  # 3

3. 优先级队列PriorityQueue

'''
# priority /praɪˈɒrəti/  优先 优先权 优先级
使用方法: 添加值以元组的形式(priority number, data). 
    - 第一个参数为指定的优先级, 
    - 第二个参数为往队列中添加的数据. 其中优先级越低的越先被get取出. 
    - 提示: 优先级可以指定负数,0,正整数
'''
q = queue.PriorityQueue()
q.put((10, '111'))
q.put((100, '222'))
q.put((0, '333'))
q.put((-5, '444'))
print(q.get())  # (-5, '444')
'''

4. 总结: 区分进程队列与线程队列

强调!!!: 线程队列的方法只能使用线程间, 不能使用在进程间. 进程也同理

# 进程间获取队列的3种模式:  
    注意: 进程间通信队列必须放到main下实例化出队列对象, 实例化出的队列对象当作参数传给每个需要开启的进程所指定的函数中, 目的: 达到进程间通信
    1) from multiprocessing import Queue
        q = Queue(): 提供内存空间相互隔离的进程间通讯(管道+锁)
    2) from multiprocessing import JoinableQueue
        q = JoinableQueue(): 在队列Queue的基础之上添加了计数器的功能. 执行put操作计数器+1, 执行get操作以后使用task_done让计数器-1, 最后可以通过join判断该计数器队列中的数值是否未0(就是判断队列是否为空)才执行下一行代码.


# 线程间获取队列的4种模式:
    注意: 在同一个进程下的多个线程的数据共享, 所以开启线程也无需发到main下, 但是我们一般都同开启进程般放在mian下. 目的: 保证同一进程下的多个线程间对数据的修改的安全性
    以下导入方式都是: import queue
    1) q = queue.Quque(): 先进先出队列
    2) q = queue.LifoQueue(): 后进先出队列
    3) q = queue.PriorityQueue(): 优先级队列

十七. 进程池与线程池(掌握)

1. 进程池与线程池介绍

# 引入: 为什么要有进程池或者线程池? 
    无论是开启进程或者开启线程也好, 都需要消耗系统资源. 只是开启线程的消耗比开启进程的消耗小一点而已, 我们不能无限制的开启进程和线程, 因为计算机的硬件资源很可能跟不上!!! 因此就有了进程池的概念.

#  为什么要用进程池和线程池?
    进程池和线程池是用来保证计算机硬件安全的情况下最大限度的利用计算机的资源. 但是有了池的存在会在一定程度上降低了程序的运行效率, 因为池限制了可以利用资源的范围. 为了保证的硬件的安全, 从而让你的程序能够正常运行, 池就必须存在.

2. 进程池与线程池基本使用

'''
# concurrent  /kənˈkʌrənt 并发 同时的 并行
# future  /ˈfjuːtʃə(r)/ 未来 未来 日后
# Executor /ɪɡˈzekjətə(r)/ 遗嘱执行人 执行者 执行员
'''
'''
# 步骤: 
    1) 导入concurrent.futures模块开启进程池或者线程池: 
        使用方法: 
            pool = ProcessPoolExecutor()
            pool = ThreadPoolExecutor()
        注意!!!: 这个进程池或者线程池造出来意后不会出现重复创建和销毁的过程.
        使用详解:
            进程池的开启默认不指定参数的情况下最大开启进程池的大小是: CPU的核数
            线程池的开启默认不指定参数的情况下最大开启线程池的大小是: 32 
    
    2) 往pool池中提交任务: submit 提交任务的方式是异步提交
        使用方法: 
            future = pool.submit(task, i)  
        使用详解:
            第一个参数指定提交任务需要执行的的函数
            第二个参数对需要执行的函数所传的参数
    
    3) 回调机制: 为submit提交的任务添加将来执行完毕以后可以回调的可调用项  ==> 异步提交任务的返回结果应该通过回调机制来获取. 
        使用方法:
            pool.submit(task, i).add_done_callback(指定调用项名) 
        举例: 
            回调机制就相当于给每个异步任务绑定了一个定时炸弹, 一旦该任务有结果立刻触发爆炸.
    
    4) 拿到异步提交的任务执行完毕以后的返回值: result 提交任务的方式是同步提交
        使用方法: res = future.result()
    
    补充(了解): pool.shutdown()  关闭线程池或进程池等待池中所有的任务运行完毕. 类始于join方法.
    
# 知识储备: 任务的2种提交方式
    1) 同步提交: 提交任务之后原地等待任务的返回结果, 在此期间不做任何事情.
    2) 异步提交: 提交任务之后不在原地等待任务的返回结果, 继续往下执行. 最后通过回调机制拿到任务执行完毕的返回值.
'''

import os
import random
import time
from concurrent.futures import ProcessPoolExecutor
from concurrent.futures import ThreadPoolExecutor


def task(n):
    print(f'任务[{n}]PID号是:', os.getpid())
    time.sleep(random.randint(1, 3))
    return f'任务[{n}]执行完毕!!!'


def call_back(future):
    print('call_back>>>:', future.result())


if __name__ == '__main__':
    # 进程池的开启默认不指定参数的情况下会和CPU的核数相同. 可以通过os.cpu_count()查看cpu的核数.
    # print(os.cpu_count())  # 4
    # pool = ProcessPoolExecutor()
    pool = ThreadPoolExecutor()

    # 方式一: 如果submit提交完任务以后直接通过result拿到返回值. 因为result提交的方式是同步提交, 此时程序就变成串行了. 不合理!!
    '''
    for i in range(20):
        future = pool.submit(task, f'任务{i}')  
        print(future.result())
    '''

    # 方式二: 任务提交的方式解决了, 但是任务拿到返回结果的方式任然存在问题.
    # 只有future_list中的第一个任务的返回值拿到了, 才能拿到future_list中第二个任务的返回值. 而每个任务的执行时间是不一定的.
    # 也许任务2或者其他任务执行完毕的时间少于future_list中的第一个任务, 其他任务因该先拿到返回值, 而不是等待其它的任务执行完毕. 因该所有任务中只要有谁的任务执行完毕, 那么谁就因该第一时间拿到返回值. 所以本次也不太合理!!!
    '''
    future_list = []
    for i in range(20):
        future = pool.submit(task, i)
        future_list.append(future)
    
    # pool.shutdown()
    for future in future_list:
        print(future.result())
    '''

    # 方式三: 异步提交任务的返回结果应该通过回调机制来获取.
    for i in range(20):
        pool.submit(task, i).add_done_callback(call_back)

3. 使用map取代 submit+for

# map内部就帮我们做了submit+for的事情
'''
def map(self, fn, *iterables, timeout=None, chunksize=1):
    fs = [self.submit(fn, *args) for args in zip(*iterables)]
'''

import os
import time
import random
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ProcessPoolExecutor


def task(n):
    print('%s is runing' % os.getpid())
    time.sleep(random.randint(1, 3))
    return n ** 2


if __name__ == '__main__':
    executor = ThreadPoolExecutor(max_workers=3)

    # for i in range(11):
    #     future=executor.submit(task,i)

    executor.map(task, range(1, 12))  # map取代了for+submit

4. 总结

from concurrent.futures import ProcessPoolExecutor
from concurrent.futures import ThreadPoolExcecutor

if __name__ == '__main__':
    # 1. 开启进程池或者线程池
    # 注意!!!: 这个进程池或者线程池造出来意后不会出现重复创建和销毁的过程.
    # 提示!!!: 下面2句严谨性不因该放到main外面, 无论是进程的开始还是线程的开始已经调用都是在主进程或者线程中执行. 没有必要再开启多进程任务时, 多个进程名称空间都重新导入一份.
	pool = ProcessPoolExecutor()
	pool = ThreadPoolExecutor()

    # 2. 异步提交任务并绑定回调机制
    pool.submit(task, task参数).add_done_callback(回调函数)

    # 3. 在回调函数中同步拿到异步提交任务的返回结果
    future.result()
posted @ 2020-04-23 21:45  给你加马桶唱疏通  阅读(126)  评论(0编辑  收藏  举报