python并发编程

python并发编程(上)

1. 线程和进程的描述

线程是真正工作的单位,进程是为线程提供资源的单位。

类比:

  • 一个工厂,至少有一个车间,一个车间中至少有一个工人,最终是工人在工作。
  • 一个程序,至少有一个进程,一个进程中至少有一个线程,最终是线程在工作。

1.1 多线程

基于多线程对上述串行示例进行优化:

  • 一个工厂,至少有一个车间,一个车间中至少有3个工人,最终是工人在工作。
  • 一个程序,至少有一个进程,一个进程中至少有3个线程,最终是线程在工作。

使用多线程:

  1. 导入threading模块;

  2. 定义线程干的事函数:

    def func(a1, a2, a3):
        pass
    
  3. 创建了一个线程,线程创建了干一件事(target=函数名这件事),函数需要参数,通过args来进行传递。

    t = threading.Thread(target=函数名, args=(11, 22, 33))
    
  4. 线程开始工作:

    t.start()
    

完整代码如下:

import time
import requests
import threading
"""
def func(a1, a2, a3):
	pass
	
t = threaing.Thread(target=func, args=(11, 22, 33))
t.start()
"""

注意:

​ 如果统计多线程的运行时间不能在多线程的循环后直接添加time.time(),因为一旦创建多线程程序会瞬间执行完全部循环内容创建多线程,之后就继续运行循环后的主程序内容了。此时多线程内容和主线程同步运行彼此不冲突。

结果是:多线程比单线程运行速度快很多。

1.2 多进程

基于多进程对上述串行示例进行优化:

  • 一个工厂,创建三个车间,每个车间一个工人**(共3人)**,并行处理任务。
  • 一个程序,创建三个进程,每个进程一个线程**(共3人)**,并行处理任务。

使用多进程:

  1. 导入multiprocessing模块;

  2. 定义进程干的事函数:

    def func(a1, a2, a3):
        pass
    
  3. 创建了一个进程,进程创建了干一件事(target=函数名这件事),函数需要参数,通过args来进行传递。

    **进程创建之后,在进程中还会创建一个线程。**因为线程才是实际工作的人。

    另外在window系统中,采用多进程一定要写在主程序里。因为windows中创建进程,内部机制是基于spawn做的。Linux系统基于fork,因此无需注意。

    # windows系统中多进程一定要写在主程序中。
    if __name__ == '__main__':
        # 进程创建之后,在进程中还会创建一个线程。
        t = multiprocessing.Process(target=函数名, args=(11, 22, 33))
    
  4. 进程开始工作:

    t.start()
    

总结:多进程比多线程开销大。

1.3 GIL锁

GIL,全局解释器锁(Global Interpreter Lock),是CPython解释器特有一个玩意,让一个进程中同一个时刻只能有一个线程可以被CPU调用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FD50np74-1639737881853)(C:\Users\lirenjie\AppData\Roaming\Typora\typora-user-images\image-20211117221224079.png)]

如果程序想要利用计算机的多核优势,让CPU同时处理一些任务,适合用多进程开发(即使资源开销大)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O0IZADvW-1639737881855)(C:\Users\lirenjie\AppData\Roaming\Typora\typora-user-images\image-20211117221400059.png)]

如果程序不需要利用计算机的多核优势,适合用多线程开发。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jjgtwkaI-1639737881856)(C:\Users\lirenjie\AppData\Roaming\Typora\typora-user-images\image-20211117221505516.png)]

总结

常见的程序开发中,计算操作需要使用CPU多核优势,IO操作不需要利用CPU的多核优势,因此:

  • 计算密集型,用多进程,例如:大量的数据计算【累加计算示例】;
  • IO密集型,用多线程,例如:文件读写、网络数据传输【下载抖音视频示例】。

累加计算实例(计算密集型):

  • 串行处理

    import time
    
    start = time.time()
    
    result = 0
    for i in range(100000000):
        result += i
    print(result)
    
    end = time.time()
    
    print("耗时:", end - start)
    
  • 多进程处理

    import time
    import multiprocessing
    
    def task(start, end, queue):
        result = 0
        for i in range(start, end):
            result += i
        queue.put(result)
    
    if __name__ == '__main__':
        queue = multiprocessing.Queue()
        
        start_time = time.time()
        
        p1 = multiprocessing.Process(target=task, args=(0, 50000000, queue))
        p1.start()
        
        p2 = multiprocessing.Process(target=task, args=(50000000, 100000000, queue))
        p2.start()
        
        v1 = queue.get(block=True)
        v2 = queue.get(block=True)
        print(v1 + v2)
        
        end_time = time.time()
        
        print("耗时:", end_time - start_time)
        
    

当然,在程序开发中 多线程 和 多进程 是可以结合使用的,例如:创建两个进程(建议与CPU个数相同),每个进程中创建3个线程。

import multiprocessing
import threading


def thread_task():
    pass

def task(start, end):
    t1 = threading.Thread(target=thread_task)
    t1.start()
    
    t2 = threading.Thread(target=thread_task)
    t2.start()
    
    t3 = threading.Thread(target=thread_task)
    t3.start()


if __name__ == '__main__':
    p1 = multiprocessing.Process(target=task, args=(0, 50000000))
    p1.start()
    
    p2 = multiprocessing.Process(target=task, args=(50000000, 100000000))
    p2.start()

2. 多线程开发

整个程序是一个进程,进程里面是一个主线程,当发现要求创建线程时,python就创建了一个子线程,此时主线程继续向下执行

import threading

def task(arg):
    pass


# 创建一个Thread对象(线程),并封装线程被CPU调度时应该执行的任务和相关参数
t = threading.Thread(target=task, args=('xxx',))
# 线程准备就绪(等待CPU调度),代码继续向下执行
t.start()

print("继续执行...")    # 主线程执行完所有代码,不结束(等待子线程)

线程的常见方法:

  • t.start(),当前线程准备就绪(等待CPU调度,具体时间是由CPU决定)。

    举例说明:下列代码中执行t.start()后,主线程继续执行print(number),但是子线程执行到什么状况是由CPU决定的,是无法确定的,因此打印出来的number可能是0到10000000中的任何一个数。

    import threading
    
    loop = 10000000
    number = 0
    
    def _add(count):
    	global number
    	for i in range(count):
    		number += 1
    
    t = threading.Thread(target=_add, args=(loop,))
    t.start()  # 准备完毕,可以被CPU调度了
    
    print(number)
    
  • t.join(),等待当前线程的任务执行完毕后再向下继续执行。

    下列代码print出来的值一定是10000000。

    import threading
    
    loop = 10000000
    number = 0
    
    def _add(count):
    	global number
    	for i in range(count):
    		number += 1
    
    t = threading.Thread(target=_add, args=(loop,))
    t.start()
    
    t.join()  # 主线程等待中...
    
    print(number)
    
    import threading
    
    number = 0
    
    
    def _add():
    	global number
    	for i in range(10000000):
    		number += 1
    
    
    def _sub():
    	global number
    	for i in range(10000000):
    		number -= 1
    
    
    t1 = threading.Thread(target=_add)
    t2 = threading.Thread(target=_sub)
    t1.start()
    t1.join()  # t1线程执行完毕,才继续往前走
    t2.start()
    t2.join()  # t2线程执行完毕,主线程才继续往下执行
    
    print(number)
    
    import threading
    
    loop = 10000000
    number = 0
    
    
    def _add(count):
    	global number
    	for i in range(count):
    		number += 1
    	
    
    def _sub(count):
    	global number
    	for i in range(count):
    		number -= 1
    
    
    t1 = threading.Thread(target=_add, args=(loop,))
    t2 = threading.Thread(target=_sub, args=(loop,))
    # 由于GIL锁,两个线程是相互切换执行的
    t1.start()
    t2.start()
    
    t1.join()  # t1线程执行完毕,主线程才继续往下执行,此时t2也在执行但不知道执行到哪一步了,两个线程都对一个值进行操作时会发生混乱。
    t2.join()  # t2线程执行完毕,主线程才继续往下执行
    
    print(number)
    
  • t.setDaemon(布尔值),守护线程(必须放在start之前

    • t.setDaemon(True),设置为守护线程,主线程执行完毕后,子线程也自动关闭。
    • t.setDaemon(False),设置为非守护线程,主线程等待子线程,子线程执行完毕后,主线程才结束。(默认)
    import threading
    import time
    
    def task(arg):
        time.sleep(5)
        print('任务')
        
    t = threading.Thread(target=task, args=(11,))
    t.setDaemon(True)  # setDaemon应当在start前执行
    t.start()
    
    print('end')  # 如果setDaemon中设置为False,那么可能会先打印end然后打印任务
    
  • 线程名称设置和获取

    import threading
    
    
    def task(arg):
        # 获取当前线程的名字
        name = threading.current_thread().getName()
        print(name)
        
    
    for i in range(10):
        t = threading.Thread(target=task, args=(11,))
        t.setName('Jinx-{}'.format(i))  # 为当前线程设置名字,必须在start前
        t.start()
    
  • 自定义线程类,直接将线程需要做的事写到run方法中。

    import threading
    
    
    class MyThread(threading.Thread):
        def run(self):
            print('执行此线程', self._args)
    
            
    t = MyThread(args=(100,))
    t.start()
    

    原来是通过设定函数来实现多线程,这里通过类里的run方法来实现。

    import requests
    import threading
    
    
    class DouYinThread(threading.Thread):
        def run(self):
        	file_name, video_url = self._args
            res = requests.get(video_url)
            with open(file_name, mode='wb') as f:
                f.write(res.content)
              
          
    url_list = [
        ("东北F4模仿秀.mp4 ""https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"),
        ("卡特扣篮.mp4 " ,"https://aweme.snssdk.com/aweme/v1/playwm/ ?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"),
        ("罗斯mvp.mp4""https: //aweme.snssdk.com/ aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajg")
    ]
    for item in url_list:
        t = DouYinThread(args=(item[0], item[1]))
        t.start()
    

3. 线程安全

一个进程中可以有多个线程,且线程共享所有进程中的资源。

多个线程同时去操作一个“东西”,可能会存在数据混乱的情况。例如:

  • 示例1

    import threading
    
    loop = 10000000
    number = 0
    
    
    def _add(count):
    	global number
    	for i in range(count):
    		number += 1
    	
    
    def _sub(count):
    	global number
    	for i in range(count):
    		number -= 1
    
    
    t1 = threading.Thread(target=_add, args=(loop,))
    t2 = threading.Thread(target=_sub, args=(loop,))
    # 由于GIL锁,两个线程是相互切换执行的
    t1.start()
    t2.start()
    
    t1.join()  # t1线程执行完毕,主线程才继续往下执行,此时t2也在执行但不知道执行到哪一步了,两个线程都对一个值进行操作时会发生混乱。
    t2.join()  # t2线程执行完毕,主线程才继续往下执行
    
    print(number)
    

    通过加锁来解决数据混乱问题:

    import threading
    
    lock_object = threading.RLock()  # 创建锁
    
    loop = 10000000
    number = 0
    
    
    def _add(count):
        lock_object.acquire()  # 加锁
    	global number
    	for i in range(count):
    		number += 1
        lock_object.release()  # 释放锁
    	
    
    def _sub(count):
        lock_object.acquire()  # 申请锁(等待)
    	global number
    	for i in range(count):
    		number -= 1
        lock_object.release()  # 释放锁
    
    
    t1 = threading.Thread(target=_add, args=(loop,))
    t2 = threading.Thread(target=_sub, args=(loop,))
    t1.start()
    t2.start()
    
    t1.join()  # t1线程执行完毕,主线程才继续往下执行,此时t2也在执行但不知道执行到哪一步了,两个线程都对一个值进行操作时会发生混乱。
    t2.join()  # t2线程执行完毕,主线程才继续往下执行
    
    print(number)
    
  • 示例2

    # 两个线程同时处理num,同样会发生混乱
    import threading
    
    num = 0
    
    def task():
        global num
        for i in range(1000000):
            num += 1
        print(num)
    
        
    for i in range(2):
        t = threading.Thread(target=task)
        t.start()
    

    通过加锁解决:

    方法一:

    import threading
    
    lock_object = threading.RLock()
    
    num = 0
    
    def task():
        print("开始")
        lock_object.acquire()  # 申请锁
        global num
        for i in range(1000000):
            num += 1
        print(num)
    	lock_object.release()  # 释放锁
        
        
    for i in range(2):
        t = threading.Thread(target=task)
        t.start()
    

    方法二:

    import threading
    
    lock_object = threading.RLock()
    
    num = 0
    
    def task():
        print("开始")
        with lock_object:  # 基于上下文管理,内部自动执行 acquire 和 release
            global num
            for i in range(1000000):
                num += 1
        print(num)
        
        
    for i in range(2):
        t = threading.Thread(target=task)
        t.start()
    

在开发过程中,有些操作默认都是 线程安全的 (内部自己带锁的),使用时无需再通过锁再处理。例如:

# 列表 自带锁
import threading

data_list = []

def task():
    print("开始")
    for i in range(1000000):
        data_list.append(i)
    print(len(data_list))  # 第二次输出的值为2000000,说明列表里的自带锁
    
    
for i in range(2):
    t = threading.Thread(target=task)
    t.start()

下图展示了一些默认线程安全 和 线程不安全的操作:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OGVAyGlj-1639737881858)(C:\Users\lirenjie\AppData\Roaming\Typora\typora-user-images\image-20211118170611186.png)]

4. 线程锁

在程序中如果想要自己动手加锁,一般有两种:LockRLock

  • Lock,同步锁。

    import threading
    
    lock_object = threading.Lock()
    
    num = 0
    
    def task():
        print("开始")
        lock_object.acquire()  # 申请锁
        global num
        for i in range(1000000):
            num += 1
        print(num)
    	lock_object.release()  # 释放锁
        
        
    for i in range(2):
        t = threading.Thread(target=task)
        t.start()
    
  • RLock,递归锁。

    import threading
    
    lock_object = threading.RLock()
    
    num = 0
    
    def task():
        print("开始")
        lock_object.acquire()  # 申请锁
        global num
        for i in range(1000000):
            num += 1
        print(num)
    	lock_object.release()  # 释放锁
        
        
    for i in range(2):
        t = threading.Thread(target=task)
        t.start()
    

区别:

Lock不支持嵌套操作(如果使用,会死锁),RLock支持嵌套操作。但是Lock锁一次解一次效率比RLock高。例如:

# RLock可以用于嵌套锁
import threading
import time

lock_object = threading.RLock()


def task():
    print("开始")
    lock_object.acquire()  # 申请锁
    lock_object.acquire()  # 再次申请锁
    print(123)
    lock_object.release()  # 释放锁
    lock_object.release()  # 释放锁
    
    
for i in range(3):
    t = threading.Thread(target=task)
    t.start()

什么时候会用到这种嵌套呢?

import threading

lock_object = threading.RLock()

# 程序员A开发了一个函数,函数可以被其他开发者调用,内部需要基于锁保证数据安全。
def func():
    with lock_object:
        pass

# 程序员B开发了一个函数,可以直接调用这个函数。
def run():
    print("其他功能")
    func()
    print("其他功能")

# 程序员C开发了一个函数,自己需要加锁,同时也需要调用func函数。
def process():
    with lock_object:
        print("其他功能")
        func()  # -------------------> 此时就会出现多次锁的情况,只有RLock支持(Lock不支持)。
        print("其他功能")

推荐使用RLock进行开发。

5. 死锁

死锁,由于竞争资源或者由于彼此通信而造成的一种堵塞现象。

  • 情况1:

    # Lock嵌套使用会发生死锁
    import threading
    
    lock_object = threading.Lock()
    
    
    def task():
        print("开始")
        lock_object.acquire()  # 第1个抵达的线程进入并上锁,其他线程就需要在此等待
        lock_object.acquire()  # 再次申请锁
        print(123)
        lock_object.release()  # 线程出去,并解开锁,其他线程就可以进入并执行了
        lock_object.release()  # 释放锁
        
        
    for i in range(3):
        t = threading.Thread(target=task)
        t.start()
    
  • 情况2:

    互相

    # 两个线程相互需要对方的锁
    import threading
    import time
    
    lock_1 = threading.Lock()
    lock_2 = threading.Lock()
    
    
    def task1():
        lock_1.acquire()
        time.sleep(1)
        lock_2.acquire()
        print(111)
        lock_2.release()
        lock_1.release()
    
        
    def task2():
        lock_2.acquire()
        time.sleep(1)
        lock_1.acquire()
        print(222)
        lock_1.release()
        lock_2.release()
    
    
    t1 = threading.Thread(target=task1)
    t1.start()
    
    t2 = threading.Thread(target=task2)
    t2.start()
    

6. 线程池

python3中官方才正式提供线程池

线程不是开越多越好,开的多了可能导致系统性能降低,例如:如下的代码就不推荐在项目开发中编写。

不建议:无限制的创建线程。

# 这种每次都创建一个线程去操作,创建的任务太多,线程就会特别多,可能效率反而降低了。
import threading

def task(video_url):
    pass

url_list = ["www.xxxx-{}.com".format(i) for i in range(30000)]

for url in url_list:
    t = threading.Threade(target=task, args=(url,))
    t.start()

建议:使用线程池

pool = ThreadPoolExecutor(100):表示创建了一个线程池,最多维护100个线程

pool.submit(函数名, 参数1, 参数2, 参数...):表示把一个任务交给线程池,让线程池安排一个线程帮助执行这个任务

pool.shutdown(True):等待线程中的任务执行完毕后,再继续执行.有点类似线程中的join()

  • 示例1:

    import time
    from concurrent.futures import ThreadPoolExecutor
    
    # pool = ThreadPoolExecutor(100)
    # pool.submit(函数名, 参数1, 参数2, 参数...)
    
    
    def task(video_url, num):
        print("开始执行任务", video_url)
        time.sleep(5)
    
    # 创建线程池,最多维护10个线程
    pool = ThreadPoolExecutor(10)
    
    url_list = ["www.xxxx-{}.com".format(i) for i in range(300)]
    
    for url in url_list:
        # 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待
        pool.submit(task, url, 2)
    
    print("END")
    
  • 示例2:等待线程池

    import time
    from concurrent.futures import ThreadPoolExecutor
    
    
    def task(video_url):
        print("开始执行任务", video_url)
        time.sleep(5)
        
    # 创建线程池,最多维护10个线程
    pool = ThreadPoolExecutor(10)
    
    url_list = ["www.xxxx-{}.com".format(i) for i in range(300)]
    for url in url_list:
        # 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待
        pool.submit(task, url)
    
    print("执行中...")
    pool.shutdown(True)  # 等待线程中的任务执行完毕后,再继续执行
    print('继续往下走')
    
  • 示例3:任务执行完,再干点其他事

    import time
    import random
    from concurrent.futures import ThreadPoolExecutor
    
    
    def task(video_url):
        print("开始执行任务", video_url)
        time.sleep(2)
        return random.randint(0, 10)
    
    
    def done(response):
        '''
        返回值封装在response中,通过response.result()可以得到这个返回值结果
        '''
        print("任务执行后的返回值", response.result())
    
    
    # 创建线程池,最多维护10个线程。
    pool = ThreadPoolExecutor(10)
    
    url_list = ["www.xxxx-{}.com".format(i) for i in range(300)]
    for url in url_list:
        # 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待
        future = pool.submit(task, url)
        future.add_done_callback(done)  # 是子主线程执行,就是线程的任务task执行完毕后,再执行一个done
        
    # 可以做分工,例如:task专门下载,done专门将下载的数据写入本地文件
    
  • 示例4:最终统一结果

    import time
    import random
    from concurrent.futures import ThreadPoolExecutor
    
    
    def task(video_url):
        print("开始执行任务", video_url)
        time.sleep(2)
        return random.randint(0, 10)
    
    
    # 创建线程池,最多维护10个线程。
    pool = ThreadPoolExecutor(10)
    
    future_list = []
    
    url_list = ["www.xxxx-{}.com".format(i) for i in range(15)]
    for url in url_list:
        # 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待
        future = pool.submit(task, url)  # 可以视为线程代号
        future_list.append(future)  # 将线程的结果放在列表中,方便统一操作
    
    pool.shutdown(True)  # 等待子线程都执行完毕
    
    # 可以通过列表统一操作咯:比如进行累加等等。。
    for fu in future_list:
        print(fu.result())
        
    
  • 示例5:线程池实现网络图片下载

    # 运用线程池实现网络图片下载
    import os
    import requests
    from concurrent.futures import ThreadPoolExecutor
    
    
    kv = {'user_agent':'chrome/10', 'Cookie':'thw=cn; cna=BSfBFuOu/ncCAQFG9HCx3cIv; t=57181df8aba7996dfccf20347d248903; cookie2=1bc3e5c3ddeec0257ada72226303718e; v=0; _tb_token_=eeea3bee53e3e; _samesite_flag_=true; uc3=nk2=BdGua%2FU%2BB%2FuPwTox&lg2=URm48syIIVrSKA%3D%3D&vt3=F8dBxdsXEDuz9AfJC6E%3D&id2=UNQ1wrx3OkKsZQ%3D%3D; csg=ef14e122; lgc=fabulous%5Cu4E36%5Cu4E36; dnk=fabulous%5Cu4E36%5Cu4E36; skt=318c349261fc9441; existShop=MTU4MDk5NDgxNA%3D%3D; uc4=id4=0%40UgP%2BhFvxiBHOJe4l%2BXUq16lXWZNz&nk4=0%40B12ytR%2Frhs4rbXrdSSKaKGmeET8lxIw%3D; tracknick=fabulous%5Cu4E36%5Cu4E36; _cc_=WqG3DMC9EA%3D%3D; tg=0; enc=J9u7iMAehylgUZz%2FFhgpegvjszP%2FZEQQYRCJmern28ywruunLXqnys2uEXhqXG9fXH6aXFLz74%2BNKOUKz4Ompg%3D%3D; hng=CN%7Czh-CN%7CCNY%7C156; mt=ci=14_1; alitrackid=www.taobao.com; lastalitrackid=www.taobao.com; _uab_collina=158100275537958739743704; JSESSIONID=1F531BD2B1424649756E02EFFE31B46E; x5sec=7b227365617263686170703b32223a223537313237343136333737613335363833383863303434356533616533386561434c6e6838504546454e66327174714f6b61795a7777456144444d304d6a63314f4463334e7a45374d673d3d227d; uc1=cookie16=Vq8l%2BKCLySLZMFWHxqs8fwqnEw%3D%3D&cookie21=UIHiLt3xThH8t7YQoFNq&cookie15=VT5L2FSpMGV7TQ%3D%3D&existShop=false&pas=0&cookie14=UoTUO8D7ZytjSQ%3D%3D&tag=8&lng=zh_CN; l=cBOVCUzVQIx64fGvKOfZhurza77O1IRb8sPzaNbMiICPO7fM7tidWZ0K97YHCnGVK68Jz353T9A8B7Th5yCq0-Y3LAaZk_f..; isg=BDAwfPBQGg_Wh8YLynNEQMAXAf6CeRTDxJwUdCqAsgtS5dKP3onkU4aXOe2F9cyb'}
    
    def download(file_name, image_url):
        res = requests.get(
            url = image_url,
            headers = kv
        )
        # 检查images目录是否存在?不存在,则创建images目录
        if not os.path.exists("images"):
            # 创建images目录
            os.makedirs("images")
        file_path = os.path.join("images", file_name)
        # 2.将图片的内容写入到文件
        with open(file_path, mode='wb') as img_object:
            img_object.write(res.content)
    
    
    
    # 创建线程池,最多维护10个线程
    pool = ThreadPoolExecutor(10)
    
    with open("mv.csv", mode='r', encoding='utf-8') as file_object:
        for line in file_object:
            nid, name, url = line.split(',')
            file_name = "{}.png".format(name)
            pool.submit(download, file_name, url)
    
    pool.shutdown(True)
    
  • 示例6:示例5的改进,分步实现下载和写入。

    # thread_pool_t5.py的改进,分步实现下载和写入:运用线程池返回值future+闭包实现
    import os
    import requests
    from concurrent.futures import ThreadPoolExecutor
    
    
    kv = {'user_agent':'chrome/10', 'Cookie':'thw=cn; cna=BSfBFuOu/ncCAQFG9HCx3cIv; t=57181df8aba7996dfccf20347d248903; cookie2=1bc3e5c3ddeec0257ada72226303718e; v=0; _tb_token_=eeea3bee53e3e; _samesite_flag_=true; uc3=nk2=BdGua%2FU%2BB%2FuPwTox&lg2=URm48syIIVrSKA%3D%3D&vt3=F8dBxdsXEDuz9AfJC6E%3D&id2=UNQ1wrx3OkKsZQ%3D%3D; csg=ef14e122; lgc=fabulous%5Cu4E36%5Cu4E36; dnk=fabulous%5Cu4E36%5Cu4E36; skt=318c349261fc9441; existShop=MTU4MDk5NDgxNA%3D%3D; uc4=id4=0%40UgP%2BhFvxiBHOJe4l%2BXUq16lXWZNz&nk4=0%40B12ytR%2Frhs4rbXrdSSKaKGmeET8lxIw%3D; tracknick=fabulous%5Cu4E36%5Cu4E36; _cc_=WqG3DMC9EA%3D%3D; tg=0; enc=J9u7iMAehylgUZz%2FFhgpegvjszP%2FZEQQYRCJmern28ywruunLXqnys2uEXhqXG9fXH6aXFLz74%2BNKOUKz4Ompg%3D%3D; hng=CN%7Czh-CN%7CCNY%7C156; mt=ci=14_1; alitrackid=www.taobao.com; lastalitrackid=www.taobao.com; _uab_collina=158100275537958739743704; JSESSIONID=1F531BD2B1424649756E02EFFE31B46E; x5sec=7b227365617263686170703b32223a223537313237343136333737613335363833383863303434356533616533386561434c6e6838504546454e66327174714f6b61795a7777456144444d304d6a63314f4463334e7a45374d673d3d227d; uc1=cookie16=Vq8l%2BKCLySLZMFWHxqs8fwqnEw%3D%3D&cookie21=UIHiLt3xThH8t7YQoFNq&cookie15=VT5L2FSpMGV7TQ%3D%3D&existShop=false&pas=0&cookie14=UoTUO8D7ZytjSQ%3D%3D&tag=8&lng=zh_CN; l=cBOVCUzVQIx64fGvKOfZhurza77O1IRb8sPzaNbMiICPO7fM7tidWZ0K97YHCnGVK68Jz353T9A8B7Th5yCq0-Y3LAaZk_f..; isg=BDAwfPBQGg_Wh8YLynNEQMAXAf6CeRTDxJwUdCqAsgtS5dKP3onkU4aXOe2F9cyb'}
    def download(image_url):
        res = requests.get(
            url = image_url,
            headers = kv,
            timeout=30,
        )
        return res
    
    
    # 闭包,因为额外的参数file_name要传
    def outer(file_name):
        def save(response):
            res = response.result()
            # 写入本地
            # 检查images目录是否存在?不存在,则创建images目录
            if not os.path.exists("images"):
                # 创建images目录
                os.makedirs("images")
    
            file_path = os.path.join("images", file_name)
    
            # 2.将图片的内容写入到文件
            with open(file_path, mode='wb') as img_object:
                img_object.write(res.content)
        
        return save
    
    
    
    # 创建线程池,最多维护10个线程
    pool = ThreadPoolExecutor(10)
    
    with open("mv.csv", mode='r', encoding='utf-8') as file_object:
        for line in file_object:
            nid, name, url = line.split(',')
            file_name = "{}.png".format(name)
            future = pool.submit(download, url)
            future.add_done_callback(outer(file_name))
    
    pool.shutdown(True)
    

7. 单例模式(扩展)

面向对象+多线程相关的一个面试题(以后项目和源码中会用到)。

之前写一个类,每次执行类()都会实例化一个类对象。

class Foo:
    pass

obj1 = Foo()
obj2 = Foo()
print(obj1, obj2)

以后开发会遇到单例模式,每次实例化类的对象时,都是最开始创建的那个对象,不在重复创建新的对象。节省空间。

  • 简单的实现单例模式

    class Singleton:
        instance = None
        
        def __init__(self, name):
            self.name = name
    
        def __new__(cls, *args, **kwargs):
            # 返回空对象
            if cls.instance:
                return cls.instance
            cls.instance = object.__new__(cls)
            return cls.instance  # 返回之后就会执行 __init__ 进行初始化 
        
    obj1 = Singleton('alex')  # 得到了obj1,此时instance存的也是obj1
    
    obj2 = Singleton('alex2')  # 在创建一个对象,此时cls.instance已经有(obj1)
    # 返回obj1,继续做初始化。但是内存地址是一样的。
    
  • 多线程中的单例模式

    上述实现无法保证多线程单例,将其修改:

    # singleton.py的改进, 多线程中的单例模式
    import threading
    import time
    
    
    class Singleton:
        instance = None
        lock = threading.RLock()  # 定义锁
        
        def __init__(self, name):
            self.name = name
    
        def __new__(cls, *args, **kwargs):
            # 返回空对象
            if cls.instance:  # 优化,提高性能
                return cls.instance
            with cls.lock:  # 加锁,加锁就可以使多线程实现单例模式了
                if cls.instance:
                    return cls.instance
                # time.sleep(0.1)  # 地址都一样,是因为代码执行太快了,添加这条代码(模拟实际项目)地址就会不一样,通过加锁解决
                cls.instance = object.__new__(cls)
                return cls.instance  # 返回之后就会执行 __init__ 进行初始化 
    
    
    def task():
        obj = Singleton('alex')
        print(obj)
    
    
    for i in range(10):
        t = threading.Thread(target=task)
        t.start()
    
    # 又写了1000行代码,要生成一个obj对象
    # obj = Singleton('x')  # 15、16行代码就可以免去加锁释放锁,从而提高性能
    # print(obj)
    
    

总结

  1. 进程和线程的区别和应用场景。
  2. 什么是GIL锁。
  3. 多线程和线程池的使用。
  4. 线程安全 & 线程锁 & 死锁。
  5. 单例模式。

作业

  1. 简述进程和线程的区别以及应用场景:

    进程和线程类似于工厂中的车间和工人。

    线程,是计算机中可以被CPU调度的最小单元(真正在工作)。
    进程,是计算机资源分配的最小单元(进程为线程提供资源)。
    一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。
    由于GIL锁的存在,控制一个进程中同一时刻只有一个线程可以被CPU调度。所以:

    应用场景:计算密集型,用多进程,例如:大量的数据计算【累加计算示例】;IO密集型,用多线程,例如:文件读写、网络数据传输【下载抖音视频示例】。

  2. 什么是GIL锁:

    GIL,全局解释器锁(Global Interpreter Lock),是CPython解释器特有的一个玩意,让一个进程中同一个时刻只能有一个线程可以被CPU调用。

    同时,像列表、字典等常见对象的线程数据安全,也得益于GIL。

  3. 手写单例模式:

import threading

class Singleton:
    lock = threading.RLock()
    instance = None

    def __new__(cls, *args, **kwargs):
        if cls.instance:
            return cls.instance
        with cls.lock:
            if cls.instance:
                return cls.instance
            cls.instance = object.__new__(cls)
        return cls.instance
  1. 程序从flag a执行到flag b的时间大致是多少秒?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8IBeUnyR-1639737881860)(C:\Users\lirenjie\AppData\Roaming\Typora\typora-user-images\image-20211119201438882.png)]

    大概60s?这里说的是flag b执行完

  2. 程序从flag a执行到flag b的时间大致是多少秒?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QSh7nhMQ-1639737881862)(C:\Users\lirenjie\AppData\Roaming\Typora\typora-user-images\image-20211119201806329.png)]

    大概0秒?

    setDeamon()守护线程:True表示主线程执行完毕后,子线程自动结束;False表示主线程等待子线程执行完毕才会结束。

  3. 程序从flag a执行到flag b的时间大致是多少秒?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tDDPQVVj-1639737881864)(C:\Users\lirenjie\AppData\Roaming\Typora\typora-user-images\image-20211119202145807.png)]

    大概60s吧。

  4. 读程序,请确认执行到最后number是否一定为0?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rGhSg0xH-1639737881866)(C:\Users\lirenjie\AppData\Roaming\Typora\typora-user-images\image-20211119202316861.png)]

    一定是0吧。 1E7是科学计数法 代表1乘10^7

  5. 读程序,请确认执行到最后number是否一定为0?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kBv9MZwS-1639737881868)(C:\Users\lirenjie\AppData\Roaming\Typora\typora-user-images\image-20211119202554215.png)]

    不一定是0

  6. data.txt文件中共有10000条数据,请为每100行数据创建一个线程,并在线程中把当前100条数据的num列相加并打印

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wCRSM3NN-1639737881870)(C:\Users\lirenjie\AppData\Roaming\Typora\typora-user-images\image-20211119202859330.png)]

import threading


def task(row_list):
    num_list = [int(i.split(',')[-1]) for i in row_list]
    result = sum(num_list)
    print(result)


def run():
    file_object = open('data.txt', mode='r', encoding='utf-8')
    file_object.readline()
    row_list = []
    for line in file_object:
        row_list.append(line.strip())
        if len(row_list) == 100:
            t = threading.Thread(target=task, args=(row_list,))
            t.start()
            row_list = []  # 千万不能用 row_list.clear()
    if row_list:
        t = threading.Thread(target=task, args=(row_list,))
        t.start()
    
    file_object.close()


if __name__ == '__main__':
    run()
  1. python并发编程(下)

    1. 多进程开发

    进程是计算机中资源分配的最小单元;一个进程中可以有多个线程,同一个进程中的线程共享资源;

    进程与进程之间是相互隔离的。

    Python中通过多进程可以利用CPU的多核优势,计算密集型操作适用于多进程。

    1.1 进程介绍

    import multiprocessing
    
    def task():
        pass
    
    if __name__ == '__main__':
        p1 = multiprocessing.Process(target=task)
        p1.start()
    
    import multiprocessing
    
    def task(arg):
        pass
    
    def run():
        p1 = multiprocessing.Process(target=task, args=('xxx',))
        p1.start()
    
    if __name__ == '__main__':
        run()
    

    进程三大模式:

    关于在Python中基于multiprocessing模块操作的进程:
    Depending on the platform, multiprocessing supports three ways to start a process. These start methods are

    • fork,【“拷贝"几乎所有资源】【支持文件对象/线程锁等传参】【unix】【任意位置开始】【快】
      The parent process uses os.fork( ) to fork the Python interpreter. The child process, when it begins, is effectively identical to the parent process. All resources of the parent are inherited by the child process. Note that safely forking a multithreaded process is problematic. Available on Unix only. The default on Unix.
    • spawn,【run参数传必备资源】【不支持文件对象/线程锁等传参】【unix、win】【main代码块开始】【慢】
      The parent process starts a fresh python interpreter process. The child process will only inherit those resources necessary to run the process object’s run.() method. In particular, unnecessary file descriptors and handles from the parent process will not be inherited. Starting a process using this method is rather slow compared to using fork or forkserver. Available on Unix and Windows. The default on Windows and macOS.
    • forkserver,【run参数传必备资源】【不支持文件对象/线程锁等传参】【部分unix】 【main代码块开始】
      When the program starts and selects the forkserver start method, a server process is started. From then on, whenever a new process is needed, the parent process connects to the server and requests that it fork a new process. The fork server process is single threaded so it is safe for it to use os.fork( ) . No unnecessary resources are inherited. Available on Unix platforms which support passing file
      descriptors over Unix pipes.
    import multiprocessing
    multiprocessing.set_start_method("spawn")
    

    官方文档:https://docs.python.org/3/library/multiprocessing.html

    案例,练习题:

    '''
    最终txt文件中的结果为:
    xxx  # fork模式,子进程copy了23行
    alex  # 14行写的alex,由15行flush()方法刷到了硬盘中
    xxx  # 主进程一直停留在26行等待子进程运行完毕后,也将write的内容刷到了硬盘中
    
    import multiprocessing
    
    
    def task():
        print(name)
        file_object.write("alex\n")
        file_object.flush()
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("fork")  # fork模式是子进程会copy所有主进程的变量
    
        name = []
        file_object = open('x1.txt', mode='a+', encoding='utf-8')
        file_object.write("xxx\n")
    
        p1 = multiprocessing.Process(target=task)
        p1.start()
    ''' 
    
    '''
    最终txt文件中的结果为:
    xxx  # fork模式,子进程copy了file_object
    alex  # 39行写的alex,由40行flush()方法刷到了硬盘中
    # 因为49行将48行写的xxx已经刷到了硬盘中,因此后面没有再次出现xxx
    
    import multiprocessing
    
    
    def task():
        print(name)
        file_object.write("alex\n")
        file_object.flush()
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("fork")  # fork模式是子进程会copy所有主进程的变量
    
        name = []
        file_object = open('x1.txt', mode='a+', encoding='utf-8')
        file_object.write("xxx\n")
        file_object.flush()
    
        p1 = multiprocessing.Process(target=task)
        p1.start()
    '''
    
    # 三种模式练习题 fork(MAC)、spawn(windows)、forkserver
    # fork模式 锁的问题
    
    import multiprocessing
    import threading
    import time
    
    def func():
        print("来了!")
        with lock:  # 如果task中没有释放锁,那么所有的子线程都会卡在这里,因为锁已经被子进程的主线程申请走了
            print(666)
            time.sleep(1)
    
    
    def task():
        # 拷贝的锁也是被申请走的状态
        # 被谁申请走了?被子进程的主线程申请走了。
        # 锁谁呢?锁子进程中除了主线程外其他的线程
        for i in range(10):
            t = threading.Thread(target=func)
            t.start()
        time.sleep(2)
        lock.release()  # 子进程中的主线程将锁释放了,这样子进程的子线程就可以申请锁了。
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("fork")
        name = []
        lock = threading.RLock()  # 注意这个是线程锁
        lock.acquire()  # 主进程主线程申请锁,与子进程中copy的锁都没有关系
    
        p1 = multiprocessing.Process(target=task)
        p1.start()
    

    1.2 常见功能

    进程的常见方法:

    • p.start(),当前进程准备就绪,等待被CPU调度(工作单元其实是进程中的线程)。

    • p.join(),等待当前进程的任务执行完毕后再向下继续执行。

      import time
      import multiprocessing
      
      
      def task(arg):
          time.sleep(2)
          print("执行中...")
          
      
      if __name__ == '__main__':
          multiprocessing.set_start_method('spawn')
          p = multiprocessing.Process(target=task, args=('xxx',))
          p.start()
          p.join()
      
          print("继续执行")
      
    • p.daemon = 布尔值,守护进程(必须放在start之前)

      • p.daemon = True,设置为守护进程,主进程执行完毕后,子进程也自动关闭。
      • p.daemon = False,设置为非守护进程,主进程等待子进程,子进程执行完毕后,主进程才结束。
      import time
      import multiprocessing
      
      
      def task(arg):
          time.sleep(2)
          print("执行中...")
          
      
      if __name__ == '__main__':
          multiprocessing.set_start_method('spawn')
          p = multiprocessing.Process(target=task, args=('xxx',))
          p.daemon = True
          p.start()
      
          print("继续执行")
      
    • 进程的名称的设置和获取

      import time
      import multiprocessing
      
      
      def task(arg):
          time.sleep(2)
          print("当前进程的名称:", multiprocessing.current_process().name)
          
      
      if __name__ == '__main__':
          multiprocessing.set_start_method('spawn')
          p = multiprocessing.Process(target=task, args=('xxx',))
          p.name = "哈哈哈哈"
          p.start()
      
          print("继续执行")
      
    • os.getpid():当前进程id,os.getppid():进程父进程id。

    • threading.enumerate():当前线程列表。

      import os
      import time
      import threading
      import multiprocessing
      
      
      def func():
          time.sleep(3)
      
      
      def task(arg):
          for i in range(10):
              t = threading.Thread(target=func)
              t.start()
          print(os.getpid(), os.getppid())  # 打印当前进程id,当前进程父进程id
          print(len(threading.enumerate()))
          time.sleep(2)
          print("当前进程的名称:", multiprocessing.current_process().name)
          
      
      if __name__ == '__main__':
          print(os.getpid())  # 打印主进程的id
          multiprocessing.set_start_method('spawn')
          p = multiprocessing.Process(target=task, args=('xxx',))
          p.name = "哈哈哈哈"
          p.start()
      
          print("继续执行")
      
      
    • 自定义进程类,直接将线程需要做的事写到run方法中。

      import multiprocessing
      
      
      class MyProcess(multiprocessing.Process):
          def run(self):
              print('执行此进程', self._args)
      
      
      if __name__ == '__main__':
          multiprocessing.set_start_method("spawn")
          p = MyProcess(args=('xxx',))
          p.start()
          print("继续执行...")
      
      
    • CPU个数,程序一般创建多少个进程。

      import multiprocessing
      multiprocessing.cpu_count()
      
      # 根据电脑CPU个数来创建进程
      import multiprocessing
      
      if __name__ == '__main__':
          count = multiprocessing.cpu_count()
          for i in range(count - 1):
              p = multiprocessing.Process(target=xxx)
              p.start()
      

    2. 进程间数据的共享

    进程是资源分配的最小单元,每个进程中都维护自己独立的数据,不共享。

    # 进程间数据不共享
    import multiprocessing
    
    
    def task(data):
        data.append(666)
        print("子进程:", data)  # [666]
    
    
    if __name__ == '__main__':
        data_list = []
        p = multiprocessing.Process(target=task, args=(data_list,))
        p.start()
        p.join()
    
        print("主进程:", data_list)  # []
    
    

    如果想要让他们之间进行共享,可以借助一些特殊的东西来实现。

    2.1 共享

    shared memory

    Data can be stored in a shared memory map using value or Array. For example, the following code:

    'c': ctypes.c_char,  'u': ctypes.c_wchar,
    'b': ctypes.c_byte,  'B': ctypes.c_ubyte,
    'h': ctypes.c_short, 'H': ctypes.c_ushort,
    'i': ctypes.c_int,   'I': ctypes.c_uint,(其u表示无符号),
    'l': ctypes.c_long,  'L': ctypes.c_ulong,
    'f': ctypes.c_float, 'd': ctypes.c_double
    
    # Value实现进程间共享
    from multiprocessing import Process, Value
    def func(n, m1, m2) :
        n.value = 888
        m1.value = 'a'.encode('utf-8')
        m2.value = "杰"
    
    if __name__ == '__main__':
        num = Value('i', 666)
        v1 = Value('c')
        v2 = Value('u')
    
        p = Process(target=func, args=(num, v1, v2))
        p.start()
        p.join()
    
        print(num.value)  # 888
        print(v1.value)  # a
        print(v2.value)  # 武
    
    
    # Array实现进程间共享
    from multiprocessing import Process, Array
    
    def f(data_array):
        data_array[0] = 666
    
    
    if __name__ == '__main__':
        arr = Array('i', [11, 22, 33, 44])  # 数组,规定元素类型必须是int,而且只能有4个
    
        p = Process(target=f, args=(arr,))
        p.start()
        p.join()
    
        print(arr[:])  # [666, 22, 33, 44]
    

    Server process

    A manager object returned by Manager() controls a server process which holds Python objects and allows other processes to manipulate them using proxies.

    # 通过Manager()实现进程间数据共享
    
    from multiprocessing import Process, Manager
    
    
    def f(d,l):
        d[1] = '1'
        d['2'] = 2
        d[0.25] = None
        l.append(666)
    
    
    if __name__ == '__main__':
        with Manager() as manager:
            d = manager.dict()  # 通过manager创建了一个特殊的字典可以在进程间共享
            l = manager.list()  # 创建特殊的列表
            
            p = Process(target=f, args=(d, l))
            p.start()
            p.join()
            
            print(d)  # {1: '1', '2': 2, 0.25: None}
            print(l)  # [666]
    
    

    2.2 交换

    multiprocessing supports two types of communication channel between processes

    Queues

    The queue class is a near clone of queue.Queue . For example

    # 通过Queues实现进程间数据共享
    
    import multiprocessing
    
    
    def task(q):
        for i in range(10):
            q.put(i)
    
    
    if __name__ == '__main__':
        queue = multiprocessing.Queue()
    
        p = multiprocessing.Process(target=task, args=(queue,))
        p.start()
        p.join()
    
        print("主进程")
        print(queue.get())  # 0
        print(queue.get())  # 1
        print(queue.get())  # 2
        print(queue.get())  # 3
        print(queue.get())  # 4
    
    

    Pipes

    The pipe() function returns a pair of connection objects connected by a pipe which by default is duplex(two-way). For example:

    双向管道,两边都能接受发送数据

    # 通过Pipe实现进程间数据共享
    
    import time
    import multiprocessing
    
    
    def task(conn):
        time.sleep(1)
        conn.send([111, 22, 33, 44])
        data = conn.recv()  # 阻塞
        print("子进程接收:", data)
        time.sleep(2)
    
    
    if __name__ == '__main__':
        parent_conn, child_conn = multiprocessing.Pipe()
        p = multiprocessing.Process(target=task, args=(child_conn,))
        p.start()
        info = parent_conn.recv()  # 阻塞
        print("主进程接收:", info)
        parent_conn.send(666)
    

    3. 进程锁

    多个进程抢占资源做某些操作,为了防止操作问题,可以通过进程锁来避免。

    多个进程同时抢一个资源,导致数据混乱的问题

    注意:采用Value、Array、Manager()才会发生数据混乱问题,采用Queues、Pipes不会发生数据混乱(内部就是排队)。

    # Value
    import time
    from multiprocessing import Process, Value, Array
    
    
    def func(n,):
        n.value = n.value + 1
    
    
    if __name__ == '__main__':
        num = Value('i', 0)
        p_list = []
    
        for i in range(20):
            p = Process(target=func, args=(num,))
            p.start()
            p_list.append(p)
    
        for p in p_list:
            p.join()
    
        print(num.value)
    
    # Manager()多进程抢资源
    import time
    from multiprocessing import Process, Manager
    
    
    def f(d,):
        d[1] += 1
    
    
    if __name__ == '__main__':
        p_list = []
        with Manager() as manager:
            d = manager.dict()
            d[1] = 0
            for i in range(20):
                p = Process(target=f, args=(d,))
                p.start()
                # p_list.append(p)
    
            for p in p_list:
                p.join()
        # time.sleep(3)
        print(d[1])
    
    # 文件读写 多进程抢资源
    import time
    import multiprocessing
    
    
    def task():
    # 假设文件中保存的内容就是一个值:10
        with open('f1.txt', mode='r', encoding='utf-8') as f:
            current_num = int(f.read())
            print("排队抢票了")
            time.sleep( 1)
            current_num -= 1
        
        with open('f1.txt', mode= 'w', encoding= 'utf-8') as f:
            f.write(str(current_num))
    
    
    if __name__ == '__main__':
        for i in range(10):
            p = multiprocessing.Process(target=task)
            p.start()
        
        time.sleep(5)
    

    解决方式:添加进程锁

    下面代码针对上述多进程对文件读写发生抢占资源现象做了改进。

    # 文件读写 多进程抢资源 添加进程锁解决
    import os
    import time
    import multiprocessing
    
    
    def task(lock):
        # 假设文件中保存的内容就是一个值:10
        print("开始")
        with lock:
            with open('f1.txt', mode='r', encoding='utf-8') as f:
                current_num = int(f.read())
            print(os.getpid(), "排队抢票了")
            time.sleep(1)
            current_num -= 1
            
            with open('f1.txt', mode= 'w', encoding= 'utf-8') as f:
                f.write(str(current_num))
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method('spawn')  # spawn、fork、forkserver
    
        lock = multiprocessing.RLock()  # 进程锁与线程锁不同,可以当作参数传入,线程锁不行会报错
        p_list = []
        for i in range(10):
            p = multiprocessing.Process(target=task, args=(lock,))
            p.start()  # p.join()为什么不在这里写?因为在这里写就是一个一个进程执行失去了并发的意义
            p_list.append(p)
        
        # time.sleep(5)
        # spawn模式下应当添加下列代码,或者time.sleep() 否则有问题
        for p in p_list:
            p.join()
        
    

    特别注意:

    • 进程锁可以当作参数,传入进程任务中,但是线程锁不行。
    • 第28行代码,p.join()为什么不写在这里。
    • spawn模式下,应当写等待每个子进程执行完毕的代码,但是fork模式不用写。

    4. 进程池

    # 进程池简单应用
    import time
    import multiprocessing
    from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
    
    
    def task(num):
        print("执行:", num)  # 执行时发现没打印出来是ide的原因,cmd没问题
        time.sleep(0.5)
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method('spawn')
    
        pool = ProcessPoolExecutor(4)  # 进程池管理4个进程
        for i in range(10):
            pool.submit(task, i)
        print(1)
        print(2)
     
    

    上面的代码主进程不会管子进程运行的怎么样,只管自己往下运行。

    通过pool.shutdown()可以人为添加阻塞功能。

    # 进程池 人为添加阻塞功能 pool.shutdown()
    import time
    from concurrent.futures import ProcessPoolExecutor
    
    
    def task(num):
        print("执行", num)
        time.sleep(2)
    
    
    if __name__ == '__main__':
        pool = ProcessPoolExecutor(4)
    
        for i in range(10):
            pool.submit(task, i)
        
        # 等待进程池中的任务都执行完毕后,再继续往后执行。
        pool.shutdown(True)  # shut.down()报错是ide的原因,cmd中直接运行没有问题
        print("end")
    

    下面代码介绍了进程池回调函数.add_done_callback(函数名)的实现。

    # 进程池回调函数
    import time
    from concurrent.futures import ProcessPoolExecutor
    import multiprocessing
    
    
    def task(num) :
        print("执行", num)
        time.sleep(2)
        return num
    
    def done(res) :
        print(multiprocessing.current_process().pid)  # 主进程处理!
        time.sleep(1)
        print(res.result())
        time.sleep(1)
    
    if __name__ == '__main__':
        pool = ProcessPoolExecutor(4)
    
        for i in range(15):
            fur = pool.submit(task, i)
            fur.add_done_callback(done)  # done的调用由主进程处理(与线程池不同)
    
        print(multiprocessing.current_process().pid)
        pool.shutdown(True)
        time.sleep(3)
    

    注意∶如果在进程池中要使用进程锁,则需要基于Manager中的Lock和RLock来实现。

    # 进程池中资源共享,通过进程锁来控制,用的是Manager中的Lock和RLock
    import time
    import multiprocessing
    from concurrent.futures.process import ProcessPoolExecutor
    
    
    def task (lock):
        print("开始")
        # lock.acquire()
        # lock.relase()
        with lock :
            # 假设文件中保存的内容就是一个值:10
            with open('f1.txt', mode='r', encoding= 'utf-8') as f:
                current_num = int(f.read())
                print("排队抢票了")
                time.sleep(1)
                current_num -= 1
            with open('f1.txt', mode= 'w', encoding='utf-8') as f:
                f.write(str(current_num))
    
    
    if __name__ == '__main__':
        pool = ProcessPoolExecutor(4)
    
        # lock_object = multiprocessing.RLock()  # 不能使用
        manager = multiprocessing.Manager()
        lock_object = manager.RLock()
        
        for i in range(10):
            pool.submit(task, lock_object)
        
        pool.shutdown()
    
    

    进程案例

    案例:计算每天用户访问情况。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zDp8mPnx-1639737881873)(C:\Users\lirenjie\AppData\Roaming\Typora\typora-user-images\image-20211123193250986.png)]

    • 方法1:

      进程间共享最终的字典,进行操作。每个进程处理自己的文件,并且添加到字典中。

      # 进程案例1
      import os
      import time
      from multiprocessing import Manager
      from concurrent.futures import ProcessPoolExecutor
      
      
      def task(file_name, count_dict):
          ip_set = set()
          total_count = 0
          ip_count = 0
          file_path = os.path.join("files", file_name)
          file_object = open(file_path, mode='r', encoding='utf-8')
          
          for line in file_object:
              if not line.strip():
                  continue
              user_ip = line.split(" - -", maxsplit=1)[0].split(",")[0]
              total_count += 1
              if user_ip in ip_set():
                  continue
              ip_count += 1
              ip_set.add(user_ip)
          count_dict[file_name] = {"total": total_count, 'ip': ip_count}
          time.sleep(1)
      
      
      def run():
          # 根据目录初始化字典
          '''
              1、读取目录下所有的文件,每个进程处理一个文件。
          '''
      
          pool = ProcessPoolExecutor(4)
          with Manager() as manager:
              """
              count_dict = {
                  "20210322.log":{'total':10000, 'ip':800}
              }
              """
              count_dict = manager.dict()  # 进程间共享
      
              for file_name in os.listdir("files"):
                  pool.submit(task, file_name, count_dict)
                  
              pool.shutdown(True)
                  
              for k, v in count_dict.items():
                  print(k, v)
      
      
      if __name__ == '__main__':
          run()
      
    • 方法2:

      运用回调函数,结合闭包实现。每个进程负责自己读取的文件。

      # 进程案例2
      import os
      import time
      from concurrent.futures import ProcessPoolExecutor
      
      
      def task(file_name):
          '''
              每一个子进程任务是:统计每一个文件的数目,返回这样的字典:{'total':10000, 'ip':800}
          '''
          ip_set = set()
          total_count = 0
          ip_count = 0
          file_path = os.path.join("files", file_name)
          file_object = open(file_path, mode='r', encoding='utf-8')
          
          for line in file_object:
              if not line.strip():
                  continue
              user_ip = line.split(" - -", maxsplit=1)[0].split(",")[0]
              total_count += 1
              if user_ip in ip_set():
                  continue
              ip_count += 1
              ip_set.add(user_ip)
          time.sleep(1)
          return {"total": total_count, 'ip': ip_count}
      
      
      # 闭包函数是为了传参,本质还是done()
      def outer(info, file_name):
          def done(res, *args, **kwargs):
              info[file_name] = res.result()
      
          return done
      
      
      def run():
          # 根据目录读取文件并初始化字典
          '''
              1. 读取目录下的所有文件,每个进程处理一个文件。
          '''
      
          """
              最终生成这样的字典
              count_dict = {
                  "20210322.log":{'total':10000, 'ip':800}
              }
          """
      
          info = {}  # 主进程中定义的字典
      
          pool = ProcessPoolExecutor(4)
      
          for file_name in os.listdir("files"):
              fur = pool.submit(task, file_name)
              
              fur.add_done_callback(outer(info, file_name))  # 回调函数,是主进程执行,因此info是共享的
      
          pool.shutdown(True)  # 添加阻塞,等待子进程
          for k, v in info.items():
              print(k, v)
      
      
      if __name__ == '__main__':
          run()
      

    5. 协程

    计算机中提供了∶线程、进程用于实现并发编程(真实存在)。
    协程(Coroutine),是程序员通过代码搞出来的一个东西(非真实存在)。

    协程也可以被称为微线程,是一种用户态内的上下文切换技术。
    简而言之,其实就是通过一个线程实现代码块相互切换执行(来回跳着执行)。
    

    协程的意义

    不要让用户手动去切换,而是遇到IO操作时能自动切换。

    在处理IO请求时,协程通过一个线程就可以实现并发的操作。

    协程、线程、进程的区别?

    线程,是计算机中可以被CPU调度的最小单元。
    进程,是计算机资源分配的最小单元(进程为线程提供资源)。
    
    一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。
    
    由于CPython中GIL的存在:
    	-线程,适用于IO密集型操作。
    	-进程,适用于计算密集型操作。
    
    协程,协程也可以被称为微线程,是一种用户态内的上下文切换技术,在开发中结合遇到IO自动切换,就可以通过一个线程实现并发操作。
    所以,在处理Io操作时,协程比线程更加节省开销(协程的开发难度大一些)。
    

    总结

    1. 了解进程的几种模式
    2. 掌握进程和进程池的常见操作
    3. 进程之间的数据共享
    4. 进程锁
    5. 协程、进程、线程的区别(概念)

it(",")[0]
total_count += 1
if user_ip in ip_set():
continue
ip_count += 1
ip_set.add(user_ip)
time.sleep(1)
return {“total”: total_count, ‘ip’: ip_count}

    # 闭包函数是为了传参,本质还是done()
    def outer(info, file_name):
        def done(res, *args, **kwargs):
            info[file_name] = res.result()
    
        return done
    
    
    def run():
        # 根据目录读取文件并初始化字典
        '''
            1. 读取目录下的所有文件,每个进程处理一个文件。
        '''
    
        """
            最终生成这样的字典
            count_dict = {
                "20210322.log":{'total':10000, 'ip':800}
            }
        """
    
        info = {}  # 主进程中定义的字典
    
        pool = ProcessPoolExecutor(4)
    
        for file_name in os.listdir("files"):
            fur = pool.submit(task, file_name)
            
            fur.add_done_callback(outer(info, file_name))  # 回调函数,是主进程执行,因此info是共享的
    
        pool.shutdown(True)  # 添加阻塞,等待子进程
        for k, v in info.items():
            print(k, v)
    
    
    if __name__ == '__main__':
        run()
    ```

## 5. 协程

计算机中提供了∶线程、进程用于实现并发编程(真实存在)。
协程(Coroutine),是程序员通过代码搞出来的一个东西(非真实存在)。

```
协程也可以被称为微线程,是一种用户态内的上下文切换技术。
简而言之,其实就是通过一个线程实现代码块相互切换执行(来回跳着执行)。
```



**协程的意义**:

不要让用户手动去切换,而是遇到IO操作时能自动切换。

在处理IO请求时,协程通过**一个线程**就可以实现并发的操作。



**协程、线程、进程的区别?**

```
线程,是计算机中可以被CPU调度的最小单元。
进程,是计算机资源分配的最小单元(进程为线程提供资源)。

一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。

由于CPython中GIL的存在:
	-线程,适用于IO密集型操作。
	-进程,适用于计算密集型操作。

协程,协程也可以被称为微线程,是一种用户态内的上下文切换技术,在开发中结合遇到IO自动切换,就可以通过一个线程实现并发操作。
所以,在处理Io操作时,协程比线程更加节省开销(协程的开发难度大一些)。
```



## 总结

1. 了解进程的几种模式
2. 掌握进程和进程池的常见操作
3. 进程之间的数据共享
4. 进程锁
5. 协程、进程、线程的区别(概念)

你可能感兴趣的:(python,开发语言,多线程,多进程,锁)