python--多线程之互斥锁Lock、信号量Semaphore、线程同步Condition 及 Event、队列Queue、进程池Pool

  • 一、了解Lock互斥锁
    • 1.不加锁示例
    • 2.加锁示例
  • 二、多线程同步之Semaphore信号量
    • 1.应用示例
  • 三、多线程同步之Condition
    • 1、应用示例
  • 四、线程同步之Event
    • 1.应用示例
  • 五、队列Queue
    • 1.应用示例
  • 六、多线程之线程池Pool
    • 1.应用示例(1)
    • 2.应用示例(2)
    • 2.异步调用+回调函数
      • 2.1、先来了解下爬虫
      • 2.2 回调函数

一、了解Lock互斥锁

  • 使用多线程对某个数据进行修改时,可能会出现同一个数据同一时刻被多个线程修改,造成数据不一致
  • python提供的release() 和 acquire()方法,每次只允许一个线程进行数据操作

1.不加锁示例

import threading
import time

num = 0

def task_thread(n):
    global num
    for i in range(1000000):
        num = num + n
        num -= n

t1 = threading.Thread(target=task_thread, args=(6,))
t2 = threading.Thread(target=task_thread, args=(17,))
t3 = threading.Thread(target=task_thread, args=(11,))

t1.start()
t2.start()
t3.start()

t1.join()
t2.join()
t3.join()
print(num)

执行结果:多次执行,发现出现结果为 -11 ,正确结果应该是0

-11

2.加锁示例

import threading
import time

num = 0

def task_thread(n, lock):
    global num
    lock.acquire()
    for i in range(1000000):
        num = num + n
        num -= n
    lock.release()

lock = threading.Lock()
t1 = threading.Thread(target=task_thread, args=(6, lock))
t2 = threading.Thread(target=task_thread, args=(17, lock))
t3 = threading.Thread(target=task_thread, args=(11, lock))

t1.start()
t2.start()
t3.start()

t1.join()
t2.join()
t3.join()
print(num)

执行结果:多试了几次,结果都是正确的

0

二、多线程同步之Semaphore信号量

  • 示例:银行有5个窗口,同时有5个人在银行办理业务,后面的人只能排队等待,直到有窗口空闲,等待的人才能去那个窗口办理业务

1.应用示例

import threading
import time
import random

# 模拟最多5人同时办理业务
semaphore = threading.BoundedSemaphore(5)

# 模拟银行办理业务
def yewubanli(name):
    # 因为在同一个进程中
    semaphore.acquire()
    time.sleep(random.randint(1, 3))
    print(f'{time.strftime("%Y-%m-%d %H:%M:%S")} {name} 正在办理业务')
    semaphore.release()

thread_list = []

for i in range(10):
    t = threading.Thread(target=yewubanli, args=(f'客户_{i}',))
    thread_list.append(t)
    t.start()
for i in thread_list:
    i.join()

执行结果:同一时刻,最多5个线程在执行

2021-05-20 11:10:17 客户_4 正在办理业务
2021-05-20 11:10:17 客户_3 正在办理业务

2021-05-20 11:10:18 客户_1 正在办理业务
2021-05-20 11:10:18 客户_2 正在办理业务
2021-05-20 11:10:18 客户_5 正在办理业务

2021-05-20 11:10:19 客户_0 正在办理业务
2021-05-20 11:10:19 客户_7 正在办理业务
2021-05-20 11:10:19 客户_8 正在办理业务
2021-05-20 11:10:19 客户_9 正在办理业务

2021-05-20 11:10:20 客户_6 正在办理业务

Process finished with exit code 0

三、多线程同步之Condition

  • 条件对象Condition能让一个线程A停下来,等待其他线程B,线程B满足某个条件后通知(notify)线程A继续执行。
  • 线程首先获取一个条件变量锁,如果条件不满足,则该线程等待(wait)并释放条件变量锁;如果条件满足,就继续执行线程,执行完成后通知(notify)其他状态为wait的线程执行。
  • 其他处于wait状态的线程接收到通知后会重新判断条件,以确定是否继续执行

1、应用示例

import threading

class Boy(threading.Thread):
    def __init__(self, cond ,name):
        super().__init__()
        self.cond = cond
        self.name = name

    def run(self):
        self.cond.acquire()
        print(self.name + ': 嫁给我吧!')
        
        # 唤醒一个挂起的线程,让韩梅梅表态(唤醒的是Girl类中wait()的线程)
        self.cond.notify()
        
        # 释放内部占用的锁,同时线程被挂起,直至接收到通知被唤醒或者超时,等待韩梅梅回答
        self.cond.wait()
        print(self.name + ': 单膝跪下,送上戒指!!')
        self.cond.notify()
        self.cond.wait()
        print(self.name + ': 太太,你的选择太明智了')
        self.cond.release()

class Girl(threading.Thread):
    def __init__(self, cond, name):
        super().__init__()
        self.name = name
        self.cond = cond

    def run(self):
        self.cond.acquire()
        # 等待李磊的求婚
        self.cond.wait()
        print(self.name +': 没有情调,不够浪漫,不答应')
        self.cond.notify()
        self.cond.wait()
        print(self.name + ': 好的,答应你了')
        self.cond.notify()
        self.cond.release()

cond = threading.Condition()
boy = Boy(cond, "李磊")
girl = Girl(cond, "韩梅梅")
girl.start()
boy.start()

执行结果:

李磊: 嫁给我吧!
韩梅梅: 没有情调,不够浪漫,不答应
李磊: 单膝跪下,送上戒指!!
韩梅梅: 好的,答应你了
李磊: 太太,你的选择太明智了

在上述示例中:

1.程序实例化一个Conditon对象cond,一个Boy对象boy,一个Girl对象girl。
2.程序先启动girl线程,girl 线程获取了条件变量锁 cond,但是又执行了 wait()并释放了条件变量锁,自身进入阻塞状态;
3. boy线程启动后,就获得了条件变量锁 cond 并发出了消息,之后通过notify 唤醒一个挂起的线程,并释放条件变量锁,等待 girl 的回答。
4. 后面的过程都是重复这些步骤,最后通过release程序释放资源。

四、线程同步之Event

1.应用示例

这个例子不怎么好,执行结果可能会不一致

import threading, time

class Boy(threading.Thread):
    def __init__(self, cond, name):
        super().__init__()
        self.cond = cond
        self.name = name

    def run(self):
        print(self.name + ': 嫁给我把')
        # 唤醒一个挂起的线程,等待韩梅梅表态
        self.cond.set()
        time.sleep(0.5)
        self.cond.wait()
        print(self.name + ': 单膝跪下,送上戒指')
        self.cond.set()
        time.sleep(0.5)
        self.cond.wait()
        self.cond.clear()
        print(self.name + ": 太太,你太明智了")

class Girl(threading.Thread):
    def __init__(self, cond, name):
        super().__init__()
        self.name = name
        self.cond = cond
    def run(self):
        # 等待李磊的求婚
        self.cond.wait()
        print(self.name +': 没有情调,不够浪漫,不答应')
        self.cond.set()
        time.sleep(0.5)
        self.cond.wait()
        print(self.name + ': 好的,答应你了')
        self.cond.set()


cond = threading.Event()
boy = Boy(cond, "李磊")
girl = Girl(cond, "韩梅梅")

girl.start()
boy.start()

执行结果:

李磊: 嫁给我把
韩梅梅: 没有情调,不够浪漫,不答应
李磊: 单膝跪下,送上戒指
韩梅梅: 好的,答应你了
李磊: 太太,你太明智了

Event 内部默认设置了一个标志,初始值为False。
上述代码中对象girl通过 wait() 方法进入等待状态,直到对象boy调用该 Event 的 set() 方法将内置的标志设置为True时,对象girl再继续运行。
对象boy最后调用 Event 的 clear() 方法 再将内置标志设置为 False,恢复初始状态。

五、队列Queue

  • 同进程的队列相似。例如有个包子店,店员A在生产包子。客户B在不断的吃店员A生产的包子。A生产的速度和B消费的速度并不是一致的, 因此需要用到队列来保持一致

1.应用示例

import threading
import time
import queue

# 先进先出
q = queue.Queue(maxsize=5)
# q = queue.LifoQueue(maxsize=3)        # 后入先出
# q = queue.PriorityQueue(maxsize=3)    # 优先级

def Producer_A():
    count = 1
    while True:
        q.put(f"包子_{count}")
        print(f"{time.strftime('%H:%M:%S')} A 生产了: [包子_{count}]")
        count += 1
        time.sleep(1)

def Consumer_B():
    while 1:
        print(f"{time.strftime('%H:%M:%S')} B 吃了: [{q.get()}]")
        time.sleep(3)

def Consumer_C():
    while 1:
        print(f"{time.strftime('%H:%M:%S')} C 吃了: [{q.get()}]")
        time.sleep(5)


a = threading.Thread(target=Producer_A)
b = threading.Thread(target=Consumer_B)
c = threading.Thread(target=Consumer_C)
a.start()
b.start()
c.start()

执行结果

16:30:04 A 生产了: [包子_1]
16:30:04 B 吃了: [包子_1]
16:30:05 A 生产了: [包子_2]
16:30:04 C 吃了: [包子_2]
16:30:06 A 生产了: [包子_3]
16:30:07 A 生产了: [包子_4]
16:30:08 C 吃了: [包子_3]
16:30:08 A 生产了: [包子_5]
16:30:09 B 吃了: [包子_4]
16:30:09 A 生产了: [包子_6]
……

六、多线程之线程池Pool

  • 在面向对象的编程中,创建和销毁对象是非常消耗时间的,因为创建一个对象需要获取内存资源或者其他更多资源。因此,在多任务下,每新生成一个新线程,执行任务后再被回收就显得非常低效。
  • 线程池就解决了频繁创建回收的问题。将任务添加到线程池中,线程池会自动指定一个空闲的线程去执行任务,当任务超过线程池的最大线程时,任务需要等待有新的空闲进程后才会被执行
  • 我们可以使用 threading 模块 以及 queue模块 来定制线程池;
  • 或者 from concurrent.futures import ThreadPoolExecutor导入线程池
  • 也可以使用multiprocess。from multiprocess.dummy import Pool 导入线程池。

1.应用示例(1)

from multiprocessing.dummy import Pool as ThreadPool
import time

def fun(n):
    time.sleep(2)

start = time.time()

for i in range(5):
    fun(i)
print(f'单线程执行耗时: {time.time() - start}')

start2 = time.time()
# 开5个worker线程,没有参数时表示默认cou的核心数
pool = ThreadPool(5)
# pool = ThreadPool(processes=5)

# # 在线程中执行urllib2.urlopen(url),并且返回执行结果
# result2 = pool.map(func, range(5))  # 相当于执行了下面的for循环

for i in range(5):
    pool.apply_async(func=fun, args=(i,))

pool.close()
pool.join()

print(f"线程池(5)并发执行耗时:{time.time() - start2}")

执行结果

单线程执行耗时: 10.002147674560547
线程池(5)并发执行耗时:2.0186564922332764

Process finished with exit code 0

2.应用示例(2)

from concurrent.futures import ThreadPoolExecutor
import threading
import time

def func(max):
    num = 0
    for i in range(max):
        print(threading.current_thread().name + '  ' + str(i))
        num += i
    return my_sum
# 创建一个包含2条线程的线程池
with ThreadPoolExecutor(max_workers=2) as pool:
    # 向线程池提交一个task, 50会作为action()函数的参数
    future1 = pool.submit(action, 50)
    # 向线程池再提交一个task, 100会作为action()函数的参数
    future2 = pool.submit(func, 100)
    def get_result(future):
        print(future.result())
    # 为future1添加线程完成的回调函数
    future1.add_done_callback(get_result)
    # 为future2添加线程完成的回调函数
    future2.add_done_callback(get_result)

2.异步调用+回调函数

2.1、先来了解下爬虫

浏览器的工作原理:

  • 向服务端发送一个请求,服务端验证你的请求,如果正确,给你的浏览器返回一个文件,浏览器收到文件,将其中到吗渲染成网页

爬虫原理:

  • 1、利用代码模拟一个浏览器,进行浏览器的工作流程,得到一堆源代码(requests模块就是模拟浏览器的)
  • 2、对源代码进行数据清洗,得到想要的内容
'''
requests模块是第三方模块,需要下载,
    方法1:cmd命令下载:pip install requests
    方法2:pycharm中安装:File --> Settings --> Project:project01 --> Project Interpreter --> pip --> 搜索需要下载的模块 -->点击 install
'''
import requests
ret = requests.get('http://www.baidu.com')

if ret.status_code == 200:
    print(ret.text)

输出结果:获取到源码

 <!DOCTYPE html>=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>ç™¾åº¦ä¸€ä¸‹ï¼Œä½ å°±çŸ¥é“</title></head> <body link=#0000cc> 

2.2 回调函数

  • 按顺序接收每个任务的结果,进行下一步处理
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import time
import random
import os
import requests

def task(url):
    '''模拟爬取多个源代码'''
    # 这个一定会有IO,因为会出现网络延迟等因素
    ret = requests.get(url)
    if ret.status_code == 200:
        return ret.text

# 定义回调函数
def parse(obj):
    ''' 模拟对数据分析'''
    # 非IO类型
    print(len(obj.result()))

url_list = ['https://www.cnblogs.com/operationhome',
            'https://www.cnblogs.com/poloyy/p/14593841.html',
            'https://www.cnblogs.com',
            ]

if __name__ == '__main__':
    # 异步调用结果:两种方式
    # 方式二:异步回调函数,并发执行任务并且能实时返回数据
    pool = ThreadPoolExecutor(4)
    start_time = time.time()
    for url in url_list:
        obj = pool.submit(task, url)
       
        # add_done_callback(),引用回调函数(注意,传入的回调函数是没有返回值的,结果可以直接用print输出)
        obj.add_done_callback(parse)


    # shutdown(wait=True)的所用 :1、类似 join 的作用,让主进程等待进程池内所有的子进程都结束后再执行
    #                            2、在上一个进程池没有完成任务之前,不允许添加新的任务
    pool.shutdown(wait=True)



    # 补充:一个任务是通过一个函数实现的,任务完成了,它的返回值就是函数的返回值
    print(f'==== 主 :{time.time() - start_time}====')

输出结果

69247
20355
27822
====:0.1579575538635254====

上面的函数回调示例:线程池设置4个线程,异步发起5个任务,每个任务是通过网页获取源码,并发执行,当一个任务完成之后,将parse这个分析代码的任务交由
剩余空闲的线程去执行,你的这个线程继续去处理其他任务。

异步 + 回调:
异步:处理IO
回调:处理非IO

              如果是 进程池 + 回调:回调函数由主进程去执行
              如果是 线程池 + 回调:回调函数由空闲的线程去执行

你可能感兴趣的:(Python)