5_1_并发编程_多线程_多进程


文档链接

_thread ——低级线程API-文档

threading —基于线程的并行性 -文档

multiprocessing - 基于进程的并行性 - 文档

queue 同步队列 - 文档

python多进程任务拆分之apply_async()和map_async()

在使用 multiprocessing模块 时应遵守某些准则和习惯用法

取决于平台,multiprocessing支持三种启动流程的方法。


多线程_多进程

区别进程线程
根本区别作为资源分配的单位调度和执行的单位
开销每一个进程都有独立的代码和数据空间,进程间的切换会有较大的开销线程可以看出是轻量级的进程,多个线程共享内存,线程切换的开销小
所处环境在操作系统中,同时运行的多个任务在程序中多个顺序流同时执行
分配内存系统在运行的时候为每一个进程分配不同的内存区域线程所使用的资源是他所属进程的资源
包含关系一个进程内可以拥有多个线程线程是进程的一部分,所有线程有时候称为是轻量级的进程

理解并行和并发

  • 一个程序至少有一个进程,一个进程至少有一个线程

  • 并行:多个CPU核心,不同的程序就分配给不同的CPU来运行。可以让多个程序同时执行。

cpu1 -------------

cpu2 -------------

cpu3 -------------

cpu4 -------------

  • 并发:单个CPU核心,在一个时间切片里一次只能运行一个程序,如果需要运行多个程序,则串行执行。

cpu1  1----    3----

cpu1     2 ----   4----


1. 多线程

传统的方式

模拟同时写代码和画图


def coding():
     for x in range(3):
         print('正在写代码%s' % x)
         time.sleep(1)

 def drawing():
     for x in range(3):
         print('正在画图%s' % x)
         time.sleep(1)

 def main():
     coding()# 阻塞的  写完代码才可以画图  同步的
     drawing()

 if __name__ == "__main__":
     main()

模拟同时唱歌和跳舞


from time import sleep
def sing():
    for i in range(3):
        print('正在唱歌...%d'%i)
        dance() # 也是阻塞的 唱歌打印完 才会跳舞
        sleep(1)

def dance():
    for i in range(3):
        print('正在跳舞...%d'%i)
        sleep(1)

if __name__=='__main__':
    sing()
    # dance()

1.1 _thread 模块(了解即可用的少)

#导入模块
# _thread实现线程
import _thread
import time
def fun1():
    print('开始运行fun1')
    time.sleep(4)
    print('运行fun1结束')

def fun2():
    print('开始运行fun2')
    time.sleep(2)
    print('运行fun2结束')

if __name__ == '__main__':
    print('开始运行')
    #创建线程
    _thread.start_new_thread(fun1,())
    _thread.start_new_thread(fun2,())
    time.sleep(7)

1.1.1 _thread创建线程并传递参数



import _thread
import time
def fun1(thread_name,delay):
    print('开始运行fun1,线程的名:',thread_name)
    time.sleep(delay)
    print('运行fun1结束')

def fun2(thread_name,delay):
    print('开始运行fun2,线程的名:',thread_name)
    time.sleep(delay)
    print('运行fun2结束')

if __name__ == '__main__':
    print('开始运行')
    #创建线程
    _thread.start_new_thread(fun1,('thread-1',3))
    _thread.start_new_thread(fun2,('thread-2',3))
    time.sleep(7)

1.2 threading模块(常用需掌握)

例子 1


import threading
import time


#  多线程的方式
def coding():
    for x in range(3):
        print('正在写代码%s' % x)
        time.sleep(1)


def drawing():
    for x in range(3):
        print('正在画图%s' % x)
        time.sleep(1)


def main():
    t1 = threading.Thread(target=coding)
    t2 = threading.Thread(target=drawing)
    t1.start()
    t2.start()


if __name__ == "__main__":
    main()

运行结果
代码和 画图是同时出现的。
在这里插入图片描述

有兴趣的 可以用多线程 写一个 同时唱歌和跳舞的。

其实是差不多的对吧。 只是将函数 作为参数传给 Thread


例子 2

import threading
import time
def fun1(thread_name,delay):
    print('线程{}开始执行fun1'.format(thread_name))
    time.sleep(delay)
    print('线程{}运行fun1结束'.format(thread_name))
def fun2(thread_name,delay):
    print('线程{}开始执行fun2'.format(thread_name))
    time.sleep(delay)
    print('线程{}运行fun2结束'.format(thread_name))

if __name__ == '__main__':
    print('开始运行')
    #创建线程
    t1=threading.Thread(target=fun1,args=('thread-1',2))
    t2=threading.Thread(target=fun2,args=('thread-2',3))
    #启动线程
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('代码最后一行\n\n\n',threading.enumerate(),'\n\n\n')  # 打印多线程序列化
    # 等待线程终止。这会阻塞调用线程,
    # 调用join方法 :主进程等待调用join的子进程结束  才会结束
    # 直到 join() 方法被调用终止——正常或通过未处理的异常终止——或者直到出现可选的超时。
    # 说这么多,其实就一句话。 
    # 有join 阻塞。 最后一句 print 会在 最后打印。
    # 没有join 阻塞。
    # t1.start 之后 就会打印。 
    # 不会等到 t1 ,t2 两个线程执行完 再 打印

join 的结果
在这里插入图片描述
不加 join 的结果
在这里插入图片描述

1.2.1 以类(class)的方式构建多线程程序

class 类的方式

import threading
import time



#  多线程的方式  类  的方式

class CodingTread(threading.Thread):
    def run(self):
        for x in range(3):
            print('正在写代码%s' % threading.current_thread())  # 显示当前在运行的线程 的对象
            time.sleep(1)


class DrawingTread(threading.Thread):
    def run(self):
        for x in range(3):
            print('正在画图%s' % threading.current_thread())
            time.sleep(1)


def main():
    t1 = CodingTread()
    t2 = DrawingTread()
    t1.start()
    t2.start()
    # print(threading.enumerate())  # 打印多线程序列化


if __name__ == "__main__":
    main()

1.2.2 继承threading.Thread 实现创建线程


# 重写 run方法  继承
import threading
import time
def fun1(delay):
    print('线程{}执行fun1'.format(threading.current_thread().getName()))
    time.sleep(delay)
    print('线程{}执行fun1结束'.format(threading.current_thread().getName()))
    
def fun2(delay):
    print('线程{}执行fun2'.format(threading.current_thread().getName()))
    time.sleep(delay)
    print('线程{}执行fun2结束'.format(threading.current_thread().getName()))

#创建一个类MyThread 继承threading.Thread
class MyThread(threading.Thread):
    #重新构造方法
    def __init__(self,func,name,args):
        super().__init__(target=func,name=name,args=args)
    # 重写run方法
    def run(self):
        self._target(*self._args)

if __name__ == '__main__':
    print('开始运行')
    t1=MyThread(fun1,'thread-1',(2,))
    t2=MyThread(fun2,'thread-2',(4,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()

1.3 线程共享全局变量


# 在同一个线程内共享全局变量
import time
from threading import Thread
# 定义一个全局变量
num = 10


def test1():
    global num
    for i in range(3):
        num += 1
    print('执行test1函数num的值:%d\n' % num)


def test2():
    print('执行test2函数num的值:%d \n' % num)


if __name__ == '__main__':
    # 创建线程
    t1 = Thread(target=test1)
    t2 = Thread(target=test2)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

可以看到两个函数 打印 的num 是一样的

运行结果
在这里插入图片描述


1.3.1 线程共享全局变量存在的问题(加锁)

例子1

无锁


# 线程不安全
# 修改值
# 修改之后值应该是 100000和200000
import time
from threading import Thread
num = 0


def test1():
    global num
    time.sleep(2)
    for i in range(100000):
        num += 1

    print('执行test1函数num的值:', num)


def test2():
    global num
    time.sleep(2)
    for i in range(100000):
        num += 1

    print('执行test2函数num的值:', num)


if __name__ == '__main__':
    t1 = Thread(target=test1)
    t2 = Thread(target=test2)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

运行结果

并没有获得预期的 值。

因为交叉修改 num 导致数据混乱。

在这里插入图片描述

例子2

如何加锁

import threading
# 共享全局变量的问题
# 多线程都是在同一个进程中运行的,执行顺序是无序的,有可能会造成数据错误。
VALUE = 0
gLock = threading.Lock()  # 加锁 定义


def add_value():
    global VALUE  # 需要修改 全局变量,就要 关键字定义一下
    gLock.acquire()  # 锁定 VALUE   修改值的地方才加锁
    for x in range(10000000):
        VALUE += 1
    gLock.release()  # 释放一下  数据就不会 出错了
    print('value:%d' % VALUE)


def main():
    for x in range(2):
        t1 = threading.Thread(target=add_value())
        # t2 = threading.Thread(target=add_value())
        t1.start()
        # t2.start()


if __name__ == "__main__":
    main()

运行结果

得到预期的值

在这里插入图片描述

1.3.2 互斥锁的使用 1

在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

谁先抢先 锁上。 那么 其他的 锁 想上锁。 就只能等到这边 对该资源上的锁 释放之后。
那边才可以上锁。所以相当于就被阻塞了。


import time
from threading import Thread, Lock
num = 0
# 创建一个互斥锁
lock = Lock()


def test1():
    global num
    lock.acquire()  #上锁
    # 这里上锁之后 就要 等到循环 完之后
    # 才会 释放锁。
    # 意味着 test2 需要等到 循环完才能 进行上锁,这个时候 test2 的操作就被阻塞了。
    for i in range(100000):
        num += 1
    lock.release()  # 释放锁
    print('执行test1函数num的值:%d\n' % num)


def test2():
    global num
    lock.acquire()  # 上锁
    for i in range(100000):
        num += 1
    lock.release()  # 释放锁
    print('执行test2函数num的值:%d \n' % num)


if __name__ == '__main__':
    t1 = Thread(target=test1)
    t2 = Thread(target=test2)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

运行结果
在这里插入图片描述

1.3.3 互斥锁的使用 2

一般来说线程不安全是 怕 变量被修改

也就是说 其实我们只需要在 变量被修改的时候上锁

保证该变量 当前只有 一个线程在 修改他

谁先抢先 锁上。 那么 其他的 锁 想上锁。 就只能等到这边 对该资源上的锁 释放之后。
那边才可以上锁。所以相当于就被阻塞了。


# 锁优化后的使用
# 比(上一个代码) 的上锁效率高
import time
from threading import Thread,Lock
num=0
#创建一个互斥锁
lock=Lock()
def test1():
    global num
    for i in range(100000):
        lock.acquire()  # 上锁
        num+=1
        lock.release() #释放锁
    print('执行test1函数num的值:',num)

def test2():
    global num
    for i in range(100000):
        lock.acquire()  # 上锁
        num+=1
        lock.release()  # 释放锁
    print('执行test2函数num的值:',num)

if __name__ == '__main__':
    t1=Thread(target=test1)
    t2=Thread(target=test2)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

运行结果:
最终的结果 是 200000 是正确的

但是 test1 相当于一个中间 结果 。 没有价值了。

在这里插入图片描述

1.3.4 死锁

在多线程程序中,死锁问题很大一部分是由于线程同时获取多个锁造成的。

举个例子:一个线程获取了第一个锁,然后在获取第二个锁的 时候发生阻塞,那么这个线程就可能阻塞其他线程的执行,从而导致整个程序假死。

在线程共享多个资源的时候,
如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁

例子

下方代码执行之后 得到 如下 结果

Thread-6 执行-MyTread 1 ClassOut if
Thread-7 执行-MyTread 2 ClassOut if

后续的In 的内容没有打印。
因为被阻塞了。


from threading import Thread, Lock
import time

# 创建互斥锁
lock_a = Lock()
lock_b = Lock()


class MyThread1(Thread):
    def run(self):
        if lock_a.acquire():  # 如果 锁 a  锁定成功 则等待和打印
            print(self.name, '执行-MyTread 1 ClassOut if')
            time.sleep(1)
            if lock_b.acquire():  # 如果 锁 b 锁定成功 则等待和打印
                print(self.name, '执行-MyTread 1 ClassIn if')
                # in 里面的话就永远不会被打印。
                # 因为被阻塞了 class 2 在使用这把锁 lock_b
                # 这条 lock_b.acquire 就一直在阻塞中等待  lock_b 的释放。
                # 但是  class 2 中 又在等我们 我们 class 1 中的 lock_a 释放之后 它 好上锁。
                # 因为 class 2 中的  if lock_a.acquire() 也是被阻塞了的。
                # 需要class 1 释放 锁 a 。 之后 class 2 才可以使用 lock_a 上锁。
                # 这样好了。 你在等我 我在等你 释放。就死锁了。
                time.sleep(1)
                lock_b.release()  # 释放 锁 b
            print('lock_a  释放')  # print 语句并没有被执行  说明了 锁a 没有释放
            lock_a.release()  # 释放 锁 a


class MyThread2(Thread):
    def run(self):
        if lock_b.acquire():  # 如果 锁 b  锁定成功 则等待和打印
            print(self.name, '执行-MyTread 2 ClassOut if')
            time.sleep(1)
            if lock_a.acquire():  # 如果 锁 a 锁定成功 则等待和打印
                print(self.name, '执行-MyTread 2 ClassIn if')
                time.sleep(1)
                lock_a.release()  # 释放 锁 a
            print('lock_b  释放')  # print 语句并没有被执行  说明了 锁b 没有释放
            lock_b.release()  # 释放 锁 b




if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()



那么我们可以测试下 在执行完这段代码之后再 次执行 lock_a.acquire 或者 lock_b.acquire 就会看到程序挂起

在这里插入图片描述
非常直观的可以看到 是 * 说明程序一直在等待


在这里插入图片描述

然后我们手动在程序 中 释放一下 锁 ,执行 lock_a.release() 然后我们发现 程序 In 执行了。 因为们的lock_a 被我们 手动释放了。

所以会 报一个 释放 未锁定的锁的错误。

这就是整个死锁的流程。

所以我们应该避免 一个锁 锁定之后 还没有 释放。 就又执行下次 锁定。 导致 阻塞。
更应避免双方之间 互相 阻塞。 导致 死锁。


1.3.5 线程同步的应用


# 线程同步
# 协同步调
from threading import Thread,Lock
import time
# 创建3把互斥锁
lock1=Lock()
lock2=Lock()
lock3=Lock()
# 对lock2和lock3上锁
lock2.acquire()
lock3.acquire()

class Task1(Thread):
    def run(self):
        while True:
            if lock1.acquire():
                print('...task1...')
                time.sleep(1)
                #释放lock2的锁
                lock2.release()

class Task2(Thread):
    def run(self):
        while True:
            if lock2.acquire():
                print('...task2...')
                time.sleep(1)
                lock3.release()

class Task3(Thread):
    def run(self):
        while True:
            if lock3.acquire():
                print('...task3...')
                time.sleep(1)
                lock1.release()

if __name__ == '__main__':
    t1=Task1()
    t2=Task2()
    t3=Task3()
    t1.start()
    t2.start()
    t3.start()

轮流释放下个要执行的任务的锁。

在释放之前下一个任务是无法执行的
因为在等待上锁。
相等于你要上厕所,厕所有人。 你要等他出来。厕所里 只有这一个坑的情况。。


1.4 局部变量

所以我们现在有了三个方案来处理 线程不安全导致的数据共享问题。也就是脏数据的问题

  • 一个是加锁(互斥锁)
  • 一个是Local 局部变量
  • 一个是创造实例。将变量的值 挂在实例的属性上。

我们知道多线程环境下,每一个线程均可以使用所属进程全局变量

如果一个线程对全局变量进行了修改将会影响到其他所有的线程对全局变量的计算操作,从而出现数据混乱,即为脏数据。为了避免多个线程同时对变量进行修改,引入了线程同步机制,通过互斥锁来控制对全局变量的访问。所以有时候线程使用局部变量比全局变量好,因为局部变量只有线程自身可以访问,同一个进程下的其他线程不可访问

但是局部变量也是有问题,就是在函数调用的时候,传递起来很麻烦
每个函数一层一层调用都需要传递 参数

因此 Python 还提供了ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的。

1.4.1 局部变量测试(参数传递非常麻烦)


# 调用需要一层一层的去 调用 很麻烦
# 因为变量都在本地 所以无法 直接访问。
# 只能 通过参数传递的方式 访问。
# 你要访问 test 中的 参数 b
# 怎么做? 

# 其实就是 放到一个中转站上。 然后大家 都从中转站上去 拿数据就好了。 

def _split(name):
    return name.split('m')
    
def print_(b):
    print(b)

def foo1(b):
    print_(b)
    
def foo2(b):
    print_(b)
    
def test(name):
    b=_split(name)
    foo1(b)
    foo2(b)
    
    
test("onempis")

[‘one’, ‘pis’]
[‘one’, ‘pis’]


1.4.2 使用 ThreadLocal 对象

import threading
#创建ThreadLocal对象
local=threading.local()

def process_thread(name):
    #将传入的name值绑定到local的name上
    local.name=name
    process_student()

    
def process_student():
    student_name=local.name
    print('线程名:%s  学生姓名:%s'%(threading.current_thread().getName(),student_name))


t1=threading.Thread(target=process_thread,args=('张三',),name='Thread-A')
t2=threading.Thread(target=process_thread,args=('李四',),name='Thread-B')
# 全局变量但是 有自己的 私有属性,不共享相同的值,但是共享相同的变量
# 回顾下 之前写的  类的 类属性 和 实例属性。和这个场景 是否有点相似呢?
# 直接通过 实例访问 修改 类属性。 我们修改之后的值 只属于自己。
# 没修改的时候是 全局共享的。
# 当然这个属性名字 是 所有实例之间 都可访问的。 
# 你没有修改那么就是 类定义时候的值。

 

t1.start()
t2.start()
t1.join()
t2.join()

# ----------------------------

# 当然 这里 的例子 实际上就是  一个 嵌套函数
# 这样写也没啥毛病

import threading


class Local_:
    name=None


local = Local_()# 我们自己写的这个 Local_ 没有处理线程安全。之列的一些 问题
# 毕竟你只写了个 pass  
# 所以在下面student_name 那里 赋值的时候 我进行 休眠
# 就会导致 一些问题。 线程不安全。 导致 name 都是一个值。 这里只是为了 帮助大家 理解 概念
# 但是 threading 的 local 是 经过处理的 所以是 线程安全的
# 有兴趣的可以去看看  _threading_local.py  这是源码文件名字。

def process_thread(name):
    local.name = name

    def process_student():
        # 将传入的name值绑定到local的name上
        from time import sleep
        sleep(2)
        student_name = local.name

        print('线程名:%s  学生姓名:%s' %
              (threading.current_thread().getName(), student_name))

    process_student()


t1 = threading.Thread(target=process_thread, args=('张三', ), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('李四', ), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

# --------------
# 当然不加锁我们也可以实现 线程安全的
# 但是 这个例子中的处理方法。
# 我们每次使用的时候 都创建了 一个 Local_ 实例 然后再 销毁这个实例。
# 具体情况 还要看你的使用场景。 
# 这样也不失为一种处理方式。
# 相当于中转站中 每次 开辟一个 自己的空间
# 有点 超市里面去存包。每次那个密码 放东西。 
# 走的时候 取东西。然后这个空间释放出来给其他需要的人使用。

import threading


class Local_:
    name = None
def process_thread(name):
    local = Local_()
    local.name = name

    def process_student():
        # 将传入的name值绑定到local的name上
        from time import sleep
        sleep(2)
        student_name = local.name

        print('线程名:%s  学生姓名:%s' %
              (threading.current_thread().getName(), student_name))

    process_student()
    del local


t1 = threading.Thread(target=process_thread, args=('张三', ), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('李四', ), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

1.5 多线程生产者和消费者模式 Lock

本质上 就是

两个线程 对一个全局变量进行操作。
一个对 Money 进行添加。
一个对 Money 进行减去

相当于 一个赚钱 一个花钱。 而 Money 就是银行账户。

import threading
import random
import time
# Lock 版本 生产者 和消费者模式(多线程)
# 可以让代码可读性更高

gMoney = 1000  # 初始1000元
gLock = threading.Lock()  # 制造一把
gTotalTimes = 10  # 总的生产次数
gTimes = 0  # 已经生产的次数


class Producter(threading.Thread):  # 生产者类
    def run(self):
        global gMoney
        global gTimes
        while True:  # 死循环 保证他一直 生产金钱
            money = random.randint(100, 1000)
            gLock.acquire()  # 加锁操作
            gMoney += money
            print('%s生产了%d元钱,剩余%d' %
                  (threading.current_thread(), money, gMoney))
            if gTimes >= 10:
                gLock.release()
                break

            gTimes += 1
            gLock.release()  # 解锁释放
            time.sleep(0.5)  # 沉睡0.5秒


class Consumer(threading.Thread):  # 消费者类
    def run(self):
        global gMoney
        while True:
            money = random.randint(100, 1000)
            gLock.acquire()  # 加锁
            if gMoney >= money:
                gMoney -= money
                print('%s消费者消费了%d元钱,剩余%d元钱' %
                      (threading.current_thread(), money, gMoney))
            else:
                if gTimes >= gTotalTimes:  # 如果已经生产的次数 大于 我们 规定的 生产次数   执行以下代码
                    gLock.release()  # 解锁 释放
                    break  # 结束
                print('%s消费者准备消费%d元钱,剩余%d元钱,余额不足' %
                      (threading.current_thread(), money, gMoney))

            gLock.release()  # 解锁
            time.sleep(0.5)  # 沉睡0.5秒


def main():  # 主程序入口
    for x in range(5):
        t = Producter(name='生产者线程%d' % x)
        t.start()
    for x in range(3):
        t = Consumer(name='消费者线程%d' % x)
        t.start()


if __name__ == "__main__":
    main()

1.5.1 生产者消费者模式多线程 condition

信号量,发送通知。

condition 和一般的锁比较 其他都是一样的 。 这个类 可以发送通知。相当于你在外面等厕所,然后里面的人对你说,我快上好了。你准备一下。

此类(Condition)实现条件变量对象。条件变量允许一个或多个线程等待,直到另一个线程通知它们。

如果 lock 是否给出了参数 None ,必须是 Lock 或 RLock 对象,它用作基础锁。否则,一个新的 RLock 对象被创建并用作基础锁。

两个线程 对一个全局变量进行操作。
一个对 Money 进行添加。
一个对 Money 进行减去

相当于 一个赚钱 一个花钱。 而 Money 就是银行账户。 这里的代码 唯一不一样的就是
释放锁 之前 通知了 群发消息。 让其他等待的线程知道 我要释放了。

import threading
import random
import time
# 通知
# 还有阻塞 的方式 减少加锁

gMoney = 1000  # 初始1000元
gCondition = threading.Condition()  # 制造一把
gTotalTimes = 100  # 总的生产次数
gTimes = 0  # 已经生产的次数


class Producter(threading.Thread):  # 生产者类
    def run(self):
        global gMoney
        global gTimes
        while True:  # 死循环 保证他一直 生产金钱
            money = random.randint(100, 1000)
            gCondition.acquire()  # 加锁操作
            gMoney += money
            print('%s生产了%d元钱,剩余%d' %
                  (threading.current_thread(), money, gMoney))
            if gTimes >= gTotalTimes:
                gCondition.release()
                break

            gTimes += 1
            gCondition.notify_all()  # 通知所有等待的线程, 处于等待状态的被唤醒,获取锁
            gCondition.release()  # 解锁释放
            time.sleep(0.5)  # 沉睡0.5秒


class Consumer(threading.Thread):  # 消费者类
    def run(self):
        global gMoney
        while True:
            money = random.randint(100, 1000)
            gCondition.acquire()  # 加锁活动锁
            while gMoney < money:  # 这里 if 改为while 就可以循环 挂起等待 gMoney>money
                if gTimes >= gTotalTimes:  # 如果运行次数大于设计次数
                    gCondition.release()  # 解锁,释放
                    return  # 返回整个函数,终止掉此线程
                print('%s消费者准备消费%d元钱,剩余%d元钱,金额不足.' %
                      (threading.current_thread(), money, gMoney))
                gCondition.wait()  # 等待

            gMoney -= money
            print('%s消费者消费了%d元钱,剩余%d元钱' %
                  (threading.current_thread(), money, gMoney))
            gCondition.release()  # 解锁
            time.sleep(0.5)  # 沉睡0.5秒


def main():  # 主程序入口
    for x in range(1):
        t = Producter(name='生产者线程%d' % x)  # 设置线程名字
        t.start()
    for x in range(3):
        t = Consumer(name='消费者线程%d' % x)
        t.start()


if __name__ == "__main__":
    main()

1.5.2 queue 线程安全队列

线程不安全是因为,线程是无序的,会造成数据错误,和之前加锁才会数据正确,是一样的

queue的线程是 安全的

队列就是先进先出

栈 先进后出, 后进先出,和弹夹 ,以及我们打字一样。 后打的字删除的时候 就是先删除的。

queue 常用函数

  1. 初始化 Queue(maxsize)
  2. qsize() 返回队列的大小
  3. empty() 判断队列是否为空
  4. full() 判断队列是否满了
  5. get() 从队列中获取最先进去的数据,
    • block 参数:block 阻塞默认是True ,如果队列没有值,就会一直阻塞在这里
  6. put() 将一个数据放到队列当中
    • block参数: block 阻塞 默认是True ,如果队列满了,就会一直堵塞在这里,直到 队列为不满状态

简单测试

from queue import Queue
q = Queue(4)
for x in range(4):
    q.put(x, block=True)
    # 放入元素
    # block堵塞默认是True  ,如果队列满了,就会一直堵塞在这里,直到 不满
print(f"队列元素个数 qsize : {q.qsize()}")  # 获取队列总共有多少个元素

print(f"是否为空 empyt :{q.empty()}")  # 判断队列是否为空

print(f"是否装满了  full : {q.full()}")  # 判断队列是否满了
print(
    f"从队列中获取一个元素 : {q.get(block=True)}")  # block堵塞默认是True  ,如果队列没有值,就会一直堵塞在这里

q.put(10)  # 这个地方执行完
print(q.qsize()) # 对列长度为 4 满了
q.put(10, block=True, timeout=5)  # 这个地方 队列已经满了。 但是 阻塞 只阻塞 5 秒
# 超过 5 秒 就不阻塞了
# 不阻塞了。 队列还是满的 就报错了 。

运行结果
在这里插入图片描述
报错 Full 就是满了的意思。

在这里插入图片描述


下面代码 很简单

就是两个函数

一个函数 往队列中put 元素

一个函数从队列中 get 元素

使用多线程

from queue import Queue
import time
import threading


def set_value(q):
    index = 0
    while True:
        q.put(index)
        index += 1
        time.sleep(3)


def get_value(q):
    while True:
        print(q.get())  # block默认是True  没有值 会 堵塞等待有值再获取


def main():
    q = Queue(4)
    t1 = threading.Thread(target=set_value, args=[q])
    t2 = threading.Thread(target=get_value, args=[q])

    t1.start()
    t2.start()


if __name__ == "__main__":
    main()

1.5.3 生产者消费者模式(queue队列版)

本质还是一样的 只是变成了 对 队列的操作。

生产的东西放到 队列中了。


# 配合queue 安全队列的使用
from threading import Thread
from  queue import Queue
import time
class Producter(Thread):
    def run(self):
        global queue
        count=0
        while True:
            #判断队列的大小
            if queue.qsize()<1000:
                for i in range(100):
                    count+=1
                    msg='生产第'+str(count)+'个产品'
                    queue.put(msg)
                    print(msg)
                time.sleep(0.5)
                
class Consumer(Thread):
    def run(self):
        global queue
        while True:
            if queue.qsize()>100:
                for i in range(10):
                    msg=self.name+'消费'+queue.get()
                    print(msg)
                time.sleep(1)

if __name__ == '__main__':
    queue=Queue()
    p=Producter()
    c=Consumer()
    p.start()
    time.sleep(1)
    c.start()

用多线程写一个爬虫

多线程 + 队列 实现 生产者和消费者模式的 爬虫

import requests
from lxml import etree
import os
import re
from queue import Queue
import queue
import threading
import string
import time
# 多线程的方式
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64)',
    'Referer': 'http://www.doutula.com/'
}
PAT = string.punctuation
SAVEPATH = "".join([".", os.sep, "img"])
page_url_template = 'http://www.doutula.com/photo/list/?page={}'
get = requests.get


class Producter(threading.Thread):  # 生产者模式

    # 第一个*代表的是任意的未知参数, 后面第二个*的代表任意的关键字参数
    def __init__(self, page_queue, img_queue, *args, **kwargs):
        super(Producter, self).__init__(*args, **kwargs)
        self.page_queue = page_queue
        self.img_queue = img_queue

    def run(self):
        while True:
            try:
                url = self.page_queue.get(timeout=30)
                html = self.gen_html_obj(url)
                self.handle_html(html)
            except queue.Empty:
                print(f"page_queue->队列为空 page线程{threading.current_thread()}结束")
                break

    def gen_html_obj(self, url):
        try:
            r = get(url, headers=HEADERS, timeout=30)
            r.raise_for_status()  # 如果状态不是200 引发 异常
            r.encoding = "utf-8"  # 设置编码
            html = etree.HTML(r.text)
            return html
        except requests.exceptions.BaseHTTPError as exc:
            print(exc)

    def handle_img_node(self, img):
        """
        从图片节点提取属性
        构造文件名字,以及获取图片url
        """
        img_url = img.get('data-original')
        # 用get方法获取data-original属性 的值 ,也就是下载链接

        alt = img.get('alt')
        alt = re.sub(fr'[{PAT}]',  '', alt)  # 把特殊字符给替换掉
        suffix = os.path.splitext(img_url)[1]  # 提取后缀
        filename = alt + suffix  # 组合图片名字
        return img_url, filename

    def handle_html(self, html):
        # 获取 下面class 不等于 gif 的图片
        if html is not None:
            imgs = html.xpath(
                "//div[@class='page-content text-center']//img[@class!='gif']")
            for img in imgs:  # data-original
                result = self.handle_img_node(img)
                self.img_queue.put(result)  # 添加到队列里面
                # result -> img_url, filename


class Consumer(threading.Thread):
    def __init__(self, page_queue, img_queue, *args, **kwargs):
        super(Consumer, self).__init__(*args, **kwargs)
        self.page_queue = page_queue
        self.img_queue = img_queue

    def run(self):
        # run 方法是重写的用来覆盖 我们继承的 threding.Tread的fun
        # run 会自动运行
        while True:
            try:
                img_url, filename = self.img_queue.get(timeout=60)
                # 元组的话可以用这种方式解包
            except queue.Empty:
                print(f"img_queue -> 队列为空 img线程{threading.current_thread()}结束")
                break
            self.save_(img_url, filename)

    def save_(self, img_url, filename):
        res = get(img_url, headers=HEADERS).content
        save_name = "".join([SAVEPATH, os.sep, filename])
        try:
            with open(save_name, "wb") as f:
                f.write(res)
        except FileNotFoundError:
            os.mkdir(SAVEPATH)
            with open(save_name, "wb") as f:
                f.write(res)
        finally:
            print(filename, " —> 下载成功")
        # print(img_url)


def main():
    work_max = 20
    page = 3  # 下载页数
    img_work_max = 20  # 下载图片的线程数
    work_max = min(work_max, page)
    page_queue = Queue(work_max)  # 页面队列  个   创建队列大小
    img_queue = Queue(108*work_max)  # 图片队列  个  创建队列大小
    for x in range(1, page+1):
        url = 'http://www.doutula.com/photo/list/?page=%d' % x
        page_queue.put(url, timeout=30)
    for x in range(work_max):
        locals()[f"p{x}"] = Producter(page_queue, img_queue)
        locals()[f"p{x}"].start()
    for x in range(img_work_max):
        locals()[f"t{x}"] = Consumer(page_queue, img_queue)  # , daemon=True
        locals()[f"t{x}"].start()
    for x in range(img_work_max):
        locals()[f"t{x}"].join()


if __name__ == "__main__":
    start = time.time()
    main()
    print("线程销毁完毕 -> 程序结束")
    print("耗时 -> ", time.time()-start, " 秒")



2 多进程

Python 提供了非常好用的多进程包multiprocessing,只需要定义一个函数,Python会完成其他所有事情。借助这个包,可以轻松完成从单进程到并发执行的转换。multiprocessing支持子进程、通信和共享数据。语法格式如下:
语法

Process([group [, target [, name [, args [, kwargs]]]]])

# 创建子进程 
# 导入模块
from multiprocessing import Process
def run_test():
    print('...test.....')
if __name__=='__main__':
    print('主进程执行')
    # 创建子进程 target接收执行的任务
    p=Process(target=run_test)
    # 调用子进程
    p.start()

2.1 创建子进程

# 创建子进程 并且传入参数
# 导入模块
from multiprocessing import Process


# 定义任务的函数
def run_test(name, age, **kwargs):
    print('子进程正在运行 name的值:%s ,age的值:%d' % (name, age))
    print('字典kwargs:', kwargs)


if __name__ == '__main__':
    print('主进程开始执行')
    # 创建子进程
    # kwargs 可以传  也可以不传递
    # p=Process(target=run_test,args=('test',),kwargs={'key':12})
    p = Process(target=run_test, args=('test', 23), kwargs={'key': 12})
    # 调用子进程
    p.start()
    


运行结果
在这里插入图片描述

2.1 join 方法

和多线程的 效果是一样的。

阻塞。 等待子进程结束。

# join方法的使用
#导入模块
from multiprocessing import Process
from time import sleep
def worker(interval):
    print('work start')
    sleep(interval)
    print('work end')

if __name__=='__main__':
    print('主进程正在执行')
    #创建子进程
    p1 = Process(target=worker, args=(3, ))
    p2 = Process(target=worker, args=(3, ))
    #调用子进程
    p1.start()
    p2.start()
    #希望下面的输出语句,再子进程执行完才输出
    # sleep(4)
    #调用join方法 :主进程等待调用join的子进程结束  才会结束
    p1.join()
    p2.join()

    print("主进程执行完")
    print('主进程执行完')

如果没有 join 可能 最后两句 print 就会 提前打印出来。 因为没有阻塞。

运行结果
在这里插入图片描述

2.1.1join 方法的 timeout 参数

# join方法中timeout的使用
#导入模块
from multiprocessing import Process
from time import sleep
def worker(interval):
    print('work start')
    sleep(interval)
    print('work end')

if __name__=='__main__':
    print('主进程正在执行')
    #创建子进程
    p=Process(target=worker,args=(5,))
    #调用子进程
    p.start()
    #希望下面的输出语句,再子进程执行完才输出
    # sleep(4)
    #调用join方法 :主进程等待调用join的子进程结束
    p.join(3) # 相当于 阻塞 3秒
    print('主进程执行完')

因为只阻塞了 3秒。
而我们的任务中 睡眠了 5秒。

所以 主进程执行完 这句话还是 先打印了。

运行结果
在这里插入图片描述

2.2 process 属性

  • pid
  • name
  • is_alive
# process 属性的使用
# 导入模块
import multiprocessing
import time
# 定义执行任务的函数
def colck(interval):
    for i in range(3):
        print('当前时间:{}'.format(time.ctime()))
        time.sleep(interval)

if __name__=='__main__':
    # 创建子进程
    p=multiprocessing.Process(target=colck,args=(1,))
    p.start()
    p.join()
    print('p.pid:',p.pid)
    print('p.name:',p.name)
    print('p.is_alive:',p.is_alive())# 是否存活

is_alive 在最后才打印 所以那个时候 我们的子进程 p 以及 销毁。 返回 False

运行结果
在这里插入图片描述

2.3 创建多个任务

# 创建多个任务
# 导入模块
from multiprocessing import Process
from time import sleep
def work1(interval):
    print('执行work1')
    sleep(interval)
    print('end work1')

def work2(interval):
    print('执行work2')
    sleep(interval)
    print('end work2')

def work3(interval):
    print('执行work3')
    sleep(interval)
    print('end work3')

if __name__=='__main__':
    print('执行主进程')
    p1=Process(target=work1,args=(4,))
    p2=Process(target=work2,args=(2,))
    p3=Process(target=work3,args=(3,))
    # 调用子进程
    p1.start()
    p2.start()
    p3.start()
    p1.join()
    p2.join()
    p3.join()
    print('p1.name:',p1.name)
    print('p2.name:',p2.name)
    print('p3.name:',p3.name)
    print('主进程执行完')


运行结果:
在这里插入图片描述

2.4 继承的方式创建进程

# 使用继承方式创建进程
# 导入模块
# 什么叫实例化属性
# 有实例化属性的时候需要进行初始化
# 我们需要重写__init__ 初始化 构造函数
from multiprocessing import Process
from time import sleep
import time
# 定义类
class ClockProcess(Process):
    # 重写初始化方法 覆盖父类的同名方法
    def __init__(self,interval):
        Process.__init__(self)
        self.interval=interval

    # 重写run() 覆盖父类的同名方法
    def run(self):
        print('子进程开始执行的时间:{}'.format(time.ctime()))
        sleep(self.interval)
        print('子进程结束的时间:{}'.format(time.ctime()))

if __name__ == '__main__':
    # 创建子进程
    p=ClockProcess(3)
    # 调用子进程
    p.start()
    p.join()
    print('主进程执行完')



运行结果

在这里插入图片描述


2.5 进程池非阻塞状态的使用

# 进程池非阻塞状态的使用
# 导入模块
import multiprocessing
import time


# 进程执行的任务函数
def func(msg):
    print('start:', msg)
    time.sleep(3)
    print('end:', msg)


if __name__ == '__main__':
    # 创建初始化3的进程池
    pool = multiprocessing.Pool(3)
    # 添加任务
    for i in range(1, 6):
        msg = '任务%d' % i
        pool.apply(func, (msg, ))
        # pool.apply_async(func, (msg, ))
        # 此 apply_async 是异步的 多进程创建的  非阻塞
        # 还有一个apply 是单进程的
    # 如果进程池不再接收新的请求 调用close
    pool.close()
    # 等待子进程结束
    pool.join()
    print("程序结束")

apply()调用 func 带着参数 args 和关键字参数 kwds . 它会一直阻塞,直到结果准备就绪。考虑到这一块, apply_async() 更适合并行执行工作。此外, func 只在池中的一个工作人员中执行。
相当于 这样

# 伪代码
p1.start()
p1.join()
p2.start()
p2.join()

我们来对比一下

apply_async 运行结果
在这里插入图片描述
apply 运行结果
在这里插入图片描述
可以看见 apply是阻塞的。 相当于柜台排队,只有前面一个人 办完业务,然后你才可以上去。

但是 apply_async 有点像是 前面一个人在办。 然后 业务员 处理的时候, 有 其他时间,叫你上去,那个人在边上等结果,然后直接办你的业务。


2.6 阻塞 apply 使用

# 导入模块
# 进程池阻塞状态的使用
import multiprocessing
import time
# 进程执行的任务函数
def func(msg):
    print('start:',msg)
    time.sleep(3)
    print('end:',msg)

if __name__ == '__main__':
    # 创建初始化3的进程池
    pool=multiprocessing.Pool(3)
    # 添加任务
    for i in range(1,6):
        msg='任务%d'%i
        pool.apply(func,(msg,))
        # 单进程的就是阻塞的
    # 如果进程池不再接收新的请求 调用close
    pool.close()
    # 等待子进程结束
    pool.join()

运行结果
在这里插入图片描述


2.7 关于多个进程之间的数据共享。

  • 多个进程之间数据是否共享?
    • 结论 此方法不共享 num的值(即全局变量,每个进程单独维护自己的内存)
    • 可以看出 每个子进程之间单独维护 一份自己的数据。

全局变量仅仅只是被读取。

  • 用队列可以共享数据了
    • 目前常用的是 Queue

# 导入模块
from multiprocessing import Process
num=10
def work1():
    global num
    num+=5
    print('子进程1运行后:num的值',num)

def work2():
    global num
    num+=10
    print('子进程2运行后:num的值',num)

if __name__ == '__main__':
    print('主进程开始运行')
    # 创建子进程
    p1=Process(target=work1)
    p2=Process(target=work2)
    # 启动子进程
    p1.start()
    p2.start()
    # 主进程等待子进程结束
    p1.join()
    p2.join()
    print('全局变量num:',num)

运行结果
在这里插入图片描述

可以看出 每个子进程之间单独维护 一份自己的数据。

全局变量仅仅只是被读取。


实验二 发现 容器对象 也是 一样的并不是 引用, 而是 一个 新的 对象 , 看 id 打印的 内存地址就知道了。 (解决方案看 进程通信)




import multiprocessing as mp


def foo(pArg, res):
    for i in range(pArg):
        res.append(i)

    print(f"res{pArg-2}", id(res))
    return res


res1 = []
res2 = []
res3 = []

if __name__ == '__main__':
    p1 = mp.Process(target=foo, args=(3, res1))
    p2 = mp.Process(target=foo, args=(4, res2))
    p3 = mp.Process(target=foo, args=(5, res3))
    p1.start()
    p2.start()
    p3.start()

    p1.join()
    p2.join()
    p3.join()
    print("over")
    print("res1 main", res1, id(res1))
    print("res2 main", res2, id(res2))
    print("res3 main", res3, id(res3))

2.8 使用队列进行通信

关于队列 的用法

本文前面 多线程部分写过了。

可以拉上去看一下。

这个函数本质上也是一样的。

一个函数 put 元素

一个函数 get 元素

只是用上了多进程(前面队列那一节 用的多线程

# 导入模块
# 多进程之间通信
# 使用Queue
from multiprocessing import Process,Queue
from time import sleep
#定义写入的方法
def write(q):
    a=['a','b','c','d']
    for i in a:
        print('开始写入的值:%s'%i)
        q.put(i)
        sleep(1)

def reader(q):
    for i in range(q.qsize()):
        print('读取到的值:%s'%q.get())
        sleep(1)

if __name__ == '__main__':
    # 创建队列
    q=Queue()
    # 创建进程
    pw=Process(target=write,args=(q,))
    pr=Process(target=reader,args=(q,))
    pw.start()
    pw.join()
    pr.start()
    pr.join()

2.8.1 多进程通信(Manager)进程池通信

使用multiprocessing 自带的队列类 Manager().Queue() 来进行 进程池 Pool 之间的通信。

# 导入模块
# 多进程之间通信2
# 进程池之间的通信 不使用Queue
# 而是使用Manager
from multiprocessing import Process,Pool,Manager
from time import sleep
# 定义写入的方法
def write(q):
    a=['a','b','c','d']
    for i in a:
        print('开始写入的值:%s'%i)
        q.put(i)
        sleep(1)

def reader(q):
    for i in range(q.qsize()):
        print('读取到的值:%s'%q.get())
        sleep(1)

if __name__ == '__main__':
    # 创建队列
    q=Manager().Queue()
    # 创建进程
    pool=Pool(3)
    pool.apply(write,(q,))
    pool.apply(reader,(q,))
    pool.close()
    pool.join()



import multiprocessing as mp


def foo(pArg, res):
    for i in range(pArg):
        res.append(i)

    print(f"res{pArg-2}", id(res))


if __name__ == '__main__':
    Manager = mp.Manager
    with Manager() as m:
        res1 = m.list([])
        res2 = m.list([])
        res3 = m.list([])
        p1 = mp.Process(target=foo, args=(3, res1))
        p2 = mp.Process(target=foo, args=(4, res2))
        p3 = mp.Process(target=foo, args=(5, res3))
        p1.start()
        p2.start()
        p3.start()

        p1.join()
        p2.join()
        p3.join()
        print("over")
        print("res1 main", res1[:], id(res1))
        print("res2 main", res2[:], id(res2))
        print("res3 main", res3[:], id(res3))

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值