Python学习2.0多线程编程实战教学

一、Python多线程编程基础

1.线程和进程

线程:是在进程里启动的,能调动进程的任何资源,一个进程里多个线程资源共享
进程:是正在运行的程序实体,包括它所占据的系统资源,进程独立存在,不共享资源

Linux只支持多进程,windows支持多线程(开发时注意最终使用场景)

2.并发和并行

并行是真正的同时进行,并发只是用户看上去像同时进行(速度快)

3.Python中全局锁GIL

保证同一时间只有一个线程使用CPU,防止资源错乱,所以Python中都是并发,没有并行
使用多进程可以避免全局锁,(测试一般用多线程)Python多线程适用异步IO密集型(服务器响应,非CPU)
探索异步编程(如asyncio库),处理I/O密集型任务

二、Python实现多线程编程的两种方式

1._thread,threading(常用模块)基础使用

_thread使用手册
重点关注:start_new_thread()
threading使用手册
重点关注:current_thread(),Thread Objects

注意:主线程结束子线程也结束,所以需延迟主线程结束时间

案例一:用_thread完成搬砖

  • 基础设置,导入模块,搬砖函数
import _thread
import threading     --- 导入2种多线程模块和时间模块

import time

global brick_list    --- 把 brick_list 定义为全局变量
brick_list = ['砖头1','砖头2','砖头3','砖头4','砖头5','砖头6','砖头7','砖头8','砖头9','砖头10',
              '砖头11','砖头12','砖头13','砖头14','砖头15','砖头16','砖头17','砖头18','砖头19','砖头20',
              '砖头21','砖头22','砖头23','砖头24','砖头25','砖头26','砖头27','砖头28','砖头29','砖头30',
              '砖头31','砖头32','砖头33','砖头34','砖头35','砖头36','砖头37','砖头38','砖头39','砖头40',
              '砖头41','砖头42','砖头43','砖头44','砖头45','砖头46','砖头47','砖头48','砖头49','砖头50',
              '砖头51','砖头52','砖头53','砖头54','砖头55','砖头56','砖头57','砖头58','砖头59','砖头60',
              '砖头61','砖头62','砖头63','砖头64','砖头65','砖头66','砖头67','砖头68','砖头69','砖头70',
              '砖头71','砖头72','砖头73','砖头74','砖头75','砖头76','砖头77','砖头78','砖头79','砖头80',
              '砖头81','砖头82','砖头83','砖头84','砖头85','砖头86','砖头87','砖头88','砖头89','砖头90',
              '砖头91','砖头92','砖头93','砖头94','砖头95','砖头96','砖头97','砖头98','砖头99','砖头100']


def action():                           ---  创建一个action函数
    while True:                         ---  死循环
        if len(brick_list) == 0:        --- 当砖头数为0的时候可以停下来
            break  ---  break打断死循环
        brick_list.pop()                ---  搬砖  默认从后往前删除,删除后返回删掉的元素,循环一次删除一个
        print(threading.current_thread().name + "还剩%s" , brick_list ) 
                                        --- 打印线程名,和该线程搬完之后剩多少砖
        time.sleep(0.2)                 --- 限制搬砖的速度,0.2s搬一次,不限制的话搬砖会迅速完成

def main():                             --- 定义main函数,我们要运行的代码
      ***********************
      ***********************
      *****************(待填写)
    
if __name__ == '__main__':              --- 仅执行当前文件
    main()
  • 主线程搬砖,及运行结果
def main():                             --- 定义main函数,我们要运行的代码
    action()                            --- 直接调用action,一个人搬,不使用多线程

在这里插入图片描述

  • 单一子线程搬砖,及运行结果
def main():                              --- 定义main函数,我们要运行的代码
    _thread.start_new_thread(action,())  --- 开启一个子线程(调用函数action,不给action传参)  请一个人搬
    time.sleep(20)                       --- 主进程延迟20s结束  =  砖头数100 * 搬砖速度0.2

在这里插入图片描述

  • 两个子线程搬砖,及运行结果
def main():                              --- 定义main函数,我们要运行的代码
    _thread.start_new_thread(action,())  --- 开启一个子线程(调用函数action,不给action传参)  请一个人搬
    _thread.start_new_thread(action, ()) --- 开启一个子线程,请两个人搬
    time.sleep(10)         --- 主进程延迟10s   =  砖头数100 * 搬砖速度0.2 / 子线程数2

在这里插入图片描述

  • 100个子线程搬砖,及运行结果
def main():                              --- 定义main函数,我们要运行的代码
    for i in range(100):                 ---100人搬砖(调用函数action,不传参)  
        _thread.start_new_thread(action,())
        
    time.sleep(10)                       # 主进程延迟10s

在这里插入图片描述

案例二:用threading完成搬砖

  • 基础设置,导入模块,搬砖函数
import _thread
import threading     --- 导入2种多线程模块和时间模块

import time

global brick_list    --- 把 brick_list 定义为全局变量
brick_list = ['砖头1','砖头2','砖头3','砖头4','砖头5','砖头6','砖头7','砖头8','砖头9','砖头10',
              '砖头11','砖头12','砖头13','砖头14','砖头15','砖头16','砖头17','砖头18','砖头19','砖头20',
              '砖头21','砖头22','砖头23','砖头24','砖头25','砖头26','砖头27','砖头28','砖头29','砖头30',
              '砖头31','砖头32','砖头33','砖头34','砖头35','砖头36','砖头37','砖头38','砖头39','砖头40',
              '砖头41','砖头42','砖头43','砖头44','砖头45','砖头46','砖头47','砖头48','砖头49','砖头50',
              '砖头51','砖头52','砖头53','砖头54','砖头55','砖头56','砖头57','砖头58','砖头59','砖头60',
              '砖头61','砖头62','砖头63','砖头64','砖头65','砖头66','砖头67','砖头68','砖头69','砖头70',
              '砖头71','砖头72','砖头73','砖头74','砖头75','砖头76','砖头77','砖头78','砖头79','砖头80',
              '砖头81','砖头82','砖头83','砖头84','砖头85','砖头86','砖头87','砖头88','砖头89','砖头90',
              '砖头91','砖头92','砖头93','砖头94','砖头95','砖头96','砖头97','砖头98','砖头99','砖头100']


def action():                           ---  创建一个action函数
    while True:                         ---  死循环
        if len(brick_list) == 0:        --- 当砖头数为0的时候可以停下来
            break  ---  break打断死循环
        brick_list.pop()                ---  搬砖  默认从后往前删除,删除后返回删掉的元素,循环一次删除一个
        print(threading.current_thread().name + "还剩%s" , brick_list ) 
                                        --- 打印线程名,和该线程搬完之后剩多少砖
        time.sleep(0.2)                 --- 限制搬砖的速度,0.2s搬一次,不限制的话搬砖会迅速完成

def main():                             --- 定义main函数,我们要运行的代码
      ***********************
      ***********************
      *****************(待填写)
    
if __name__ == '__main__':              --- 仅执行当前文件
    main()
  • 两个子线程搬砖,及运行结果
  1. threading 本身不会随主线程结束自动退出
  2. threading 的   daemon=True  时为守护线程,主线程运行完毕,守护线程也会自动退出。  daemon=False 时不为守护线程,不会随主线程结束自动退出
def main():
    t1 = threading.Thread(target=action,args=(),name="1号工作者",daemon=False)
                                                            --- daemon=True时是守护线程
    t2 = threading.Thread(target=action,args=(),name="2号工作者",daemon=False)
    t1.start()        --- threading 本身不会随主线程结束自动退出,无需对时间进行限制
    t2.start()

if __name__ == '__main__':
    main()

在这里插入图片描述




2.使用threading模块进行多线程编程具体介绍

Threading对象描述
ThreadThread对象
Lock锁原语对象 (原语:指不可分割的多个操作)

锁原语对象:就是把线程执行任务过程中的多个指令变成不可以被分割的(人造),从而锁定任务,让其他线程无法访问。

Thread对象描述
name线程名
ident线程标志
daemon表示是否是守护线程
_ _ init_ _(group=None, tatget=None,args=(), kwargs ={},verbose=None,daemon=None实例化一个线程对象,需要有一个可调用的target
join (timeout=None)阻塞,直到达到timeout或者其他线程执行完毕
ident线程标志
getName()返回线程名
setName (name)设置线程名
isAlivel /is_alive ()线程是否存活
isDaemon()判断是否是守护线程
setDaemon(daemonic)把线程的守护标志设定为True或者False(必须在线程 start()之前调用)

1)线程阻塞

线程阻塞概念:把当前运行的线程阻塞,让其停止运行,直至所有其他线程运行完毕后,再继续执行。
语法:Thread( ).join( )

  • 用途:当所有子线程都是守护线程,而主线程很快就会结束时,那么我们可以使用.join( )方法,阻塞主线程的运行,让子线程运行完毕之后,再继续运行主线程。
  • 举例:用 join 代替 time
def main():
    t1 = threading.Thread(target=action,args=(),name="1号工作者",daemon=True)
                                                            --- daemon=True时是守护线程
    t1.start()        --- threading 本身不会随主线程结束自动退出,无需对时间进行限制
    t1.join()

2)守护线程和非守护线程

守护线程其他线程都运行结束后,守护线程立即结束
非守护线程无论其他线程有没有运行结束,本线程都必须正常运行结束后才会结束
  • 守护线程举例:
import threading
from time import sleep

def action(max):
    for i in range(max):
        print(threading.current_thread().name + "%s次循环" % i)
    sleep(1)

def main():
    t1 = threading.Thread(target=action, args=(1000,),name="后台线程")    --- 启动后台线程,设置循环1000次
    t1.daemon = True                    --- 在start之前,需要设置守护线程的开关
    t1.start()                          --- 启动后台线程

    for i in range(10):                 --- 让主线程继续运行一会儿,打印循环输出的次数
        print(threading.current_thread().name + "主线程循环了%s" % i)     --- 打印主线程循环的次数,设置为10print("主线程运行结束了")

if __name__ == '__main__':
    main()
  • 结果:主线程和守护线程一起执行10次结束
    在这里插入图片描述

3)线程锁(Lock)和信号量

线程锁(Lock)为了防止线程与线程之间资源共享导致的线程安仝问题,对访问的资源加上线程锁,控制线程先后顺序
信号量控制访问同一资源的线程数量
① 线程锁
  • 不加线程锁,子线程运行结果混乱
import threading
import time

result = 0              ----- 全局参数为0
def add(max):             ----  等待传参,参数为循环运行次数
    global result
    for i in range(max):
        result = result + 1           ----- 累加运算
    print(threading.current_thread().name,result)

def main():
    t1_list = []               --- 创建3个子线程放入列表,调用add函数,传入参数10
    for i in range(3):
        t1_list.append(threading.Thread(target=add, args=(10,)))

                               --- 启动列表里的3个子线程
    for i in range(3):
        t1_list[i].start()

if __name__ == '__main__':
    main()

实际结果应为10000000,20000000,30000000
在这里插入图片描述

  • 加线程锁,相同子线程运行结果与预期相同
threading,Lock( )对象方法描述
lock.acquire()获取锁
lock.release()释放锁
import threading
import time

result = 0                ---- 全局参数为0
lock = threading.Lock()   ----  获取Lock对象
def add(max):             ----  等待传参,参数为循环运行次数
    global result
    lock.acquire()        ----  加锁,锁住全局变量,控制线程顺序,第一个线程结束再运行第二个
    for i in range(max):
        result = result + 1        ----- 累加运算
    lock.release()        ----  解锁
    print(threading.current_thread().name,result)

def main():
    t1_list = []          --- 创建3个子线程放入列表,调用add函数,传入参数10
    for i in range(3):
        t1_list.append(threading.Thread(target=add, args=(10000000,)))

                          --- 启动列表里的3个子线程
    for i in range(3):
        t1_list[i].start()

if __name__ == '__main__':
    main()

在这里插入图片描述

② 信号量
threading,Semaphore (3)同一时间只能有3个线程处于运行状态
semaphore .acquire()获取信号量,信号量减一
semaphore .release()释放信号量,信号量加一
  • 举例:控制进入地铁人的数量
import threading,time

semaphore = threading.Semaphore(500)    --- 获取信号量对象,只准许500人进入地铁站,执行运输任务

def action(counts):
    print("进入地铁的人数为%s" % counts)           ----  打印未做控制的进入人数
    semaphore.acquire()                 --- 获取信号量,信号量减一
    print("%s号乘客进入地铁站台" % counts)         ----  打印控制后进入的人数
    time.sleep(10)                               ----  设置控制时间
    semaphore.release()                 --- 释放信号量,信号量加一
    print("%s号乘客离开地铁站台" % counts)


def main():
    for i in range(10000):              --- 启动10000个线程人进入地铁
        threading.Thread(target=action,args=(i,)).start()

if __name__ == '__main__':
    main()

结果 :使用信号量后,人数被加以限制
在这里插入图片描述

4)重写Threading方法的run函数,实现自定义Threading

自定义方法可以帮助我们使用更多需要的功能,如:自定义线程名字

①导入threading模块

import threading

②继承threading.Thread类的类

class MyThread(threading.Thread):        --- MyThread类能访问到所有threading.Thread类中的属性、方法等
    def __init__(self,func,args,name=None):     --- 继承后要初始化 __init__方法
        threading.Thread.__init__(self)         --- 两种初始化写法
        -------- super().__init__()             --- 两种初始化写法
        self.func = func
        self.args = args
        self.name = name                 --- 把自定义的内容保存为类属性,使其可以在MyThread里使用

③重写run方法

    def run(self): 
        return self.func(*self.args)        ---* 代表可以传入多个参数

④定义要执行的任务方法

def add(x,y):
    result = x + y
    print(threading.current_thread().name +" " + str(result))

⑤在main方法中启动多线程执行任务

def main():
    t1 = MyThread(add,(1,2),"t")
    t2 = MyThread(add, (11, 22),"T")

    t1.start()                          --- 启动线程
    t2.start()

⑥启动main方法

if __name__ == '__main__':
    main()

3. 队列

队列一种数据结构
队列的类型双向队列(deque)、先进先出队列、后进先出队列、优先级队列等等
队列的特点在python中,一切皆对象,所以任何乐西都可以放入队列中,使用队列进行管理。如类、方法、变量等等
队列的应用亿级吞吐量消息队列Kafka;标准消息队列RabbitMq等等;线程池是基于队列技术实现;

点击这里 --> 队列用法介绍

用法:
from queue import Queue
q = Queue(maxsize = 10) ~ ~ ~ ~ ~ ~ 创建队列

  应用场景:  
        q.put() 向队伍插入一条数据,并将其标记为“未完成任务”
        q.task_done() 减少一条“未完成任务”标记
        q.join()  所有任务完成后,停止阻塞,让队列继续工作
  • 入门案例
from queue import Queue
q = Queue(maxsize=10)  --- 创建队列,maxsize=10是指队列的长度等于10
q.put(1)           --- 插入数据
q.put("a")         --- 再次插入数据
        
print(q.get())     --- 提取数据并打印,不设置队列类型时,默认为先进先出
print(q.get())
  • 案例,生产者消费者模型:厨师做10个菜通知10个食客吃饭
from idlelib.mainmenu import menudefs
                                  --- 案例:做10个菜通知10个食客吃饭   生产者消费者模型
from queue import Queue
import threading
import time
import random

q = Queue()                  --- 创建队列
food_menu =  ["佛跳墙","红烧肉","烤鸭","清炒时蔬","狮子头","松鼠桂鱼","水煮鱼","地三鲜","凉拌秋葵","凉皮"]

def make_lunch():            --- 创建生产者厨师
    print("已经开始做菜了")
    while True:
        if q.full():         --- 判断队列是否已满
            print("菜已经全部上齐了")
            q.join()         --- 阻塞队列,停止做菜

        food_number = random.randint(0,len(food_menu)-1)     --- 随机0-9,列表下标
        q.put(food_menu[food_number])                        --- 按随机顺序做菜单里的10个菜
        print("做好了%s" % food_menu[food_number])
        time.sleep(0.1)             --- 控制做菜的频率


def eat_lunch():      --- 创建消费者食客
    time.sleep(10)    --- 等菜上桌
    if q.empty():
        print("厨师快点上菜,饿死了")
    else:
        food = q.get()
        print("吃了%s" % food)
        time.sleep(0.5)        --- 每吃一个菜都需要休息
        q.task_done()          --- 通知服务员这个菜吃完了

if __name__ == '__main__':             --- 启动两个函数分别担当厨师和食客
    cooker = threading.Thread(target=make_lunch,args=(),name="cooker")
    eater = threading.Thread(target=eat_lunch,args=(),name="eater")

    cooker.start()
    eater.start()

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

4.线程池

存放线程的池子,线程池可以控制线程的启动数量和属性状态,从而达到节省系统资源的目的

线程池与信号量Semaphore的区别
区别1线程池控制线程数量,而信号量控制的是并发数量
区别2超过信号量规定数量的线程,已经启动了,状态是挂起;
线程池中,超过了线程池规定数量的线程,没有启动,只能等待启动
Python实现线程池的两种方式
方式1concurrent.futures并发中的ThreadPoolExecutor
方式2threadpool库(测试常用于多线程执行用例)



举例:多线程搬100块砖

  • 1)打开pycharm终端,安装threadpool库
    在这里插入图片描述
pip install threadpool
  • 2)导包
import threading
import time
from threadpool import ThreadPool,makeRequests
---      ThreadPool控制线程池大小,makeRequests控制执行的请求
global brick_list    # 把 brick_list 定义为全局变量
brick_list = ['砖头1','砖头2','砖头3','砖头4','砖头5','砖头6','砖头7','砖头8','砖头9','砖头10',
              '砖头11','砖头12','砖头13','砖头14','砖头15','砖头16','砖头17','砖头18','砖头19','砖头20',
              '砖头21','砖头22','砖头23','砖头24','砖头25','砖头26','砖头27','砖头28','砖头29','砖头30',
              '砖头31','砖头32','砖头33','砖头34','砖头35','砖头36','砖头37','砖头38','砖头39','砖头40',
              '砖头41','砖头42','砖头43','砖头44','砖头45','砖头46','砖头47','砖头48','砖头49','砖头50',
              '砖头51','砖头52','砖头53','砖头54','砖头55','砖头56','砖头57','砖头58','砖头59','砖头60',
              '砖头61','砖头62','砖头63','砖头64','砖头65','砖头66','砖头67','砖头68','砖头69','砖头70',
              '砖头71','砖头72','砖头73','砖头74','砖头75','砖头76','砖头77','砖头78','砖头79','砖头80',
              '砖头81','砖头82','砖头83','砖头84','砖头85','砖头86','砖头87','砖头88','砖头89','砖头90',
              '砖头91','砖头92','砖头93','砖头94','砖头95','砖头96','砖头97','砖头98','砖头99','砖头100']

lock = threading.Lock()   # 加锁,一次只运行一个线程
# 定义搬砖的动作
def action(brick):
    lock.acquire()    # 加锁,锁住全局变量
    print(threading.current_thread().name,brick)   # 通过输出表示完成搬砖的动作
    lock.release()    # 解锁
    time.sleep(0.2)
  • 3)创建线程池
def main():
    # 创建线程池
    threadpool_brick = ThreadPool(5)   # 最多运行5个线程
  • 4)添加任务
 # 创建执行的任务
    requests = makeRequests(action,brick_list)
  • 5)执行任务
# 将任务添加到线程池中,并执行
    # for i in requests:
    #     threadpool_brick.putRequest(i)   # 简化成列表推导式
    [threadpool_brick.putRequest(i) for i in requests]   # 列表推导式,把requests请求放到线程池里
  • 6)等待执行
    threadpool_brick.wait()   # 等待执行完成
if __name__ == '__main__':
    main()

结果:线程池控制同一时间最多5个线程启动进行搬砖,结束后线程会不断重新从1开始计数到5。与之不同,信号量会直接计数到100

在这里插入图片描述

5. 标准库 concurrent.futures:创建多线程用来并发测试,简化代码

  • 导入
from concurrent.futures import ThreadPoolExecutor
  • 用法 with ThreadPoolExecutor(max_workers=最大线程数量) as executor:
  •           executor.submit(运行函数, 传入参数)
    
from concurrent.futures import ThreadPoolExecutor
import time
import threading

global thread_my_list
thread_my_list = ["听歌","吃饭","看戏","聊天"]

def doing(doing_name):
    print(threading.current_thread().name + "执行的责任为%s" % doing_name)


def main():
    for i in range(len(thread_my_list)):
        with ThreadPoolExecutor(max_workers=len(thread_my_list)) as executor:    # 固定写法
            executor.submit(doing, thread_my_list[i])                      # 固定写法

if __name__ == '__main__':
    main()
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值