多线程编程

本文深入探讨Python中多线程编程的实现方法,包括_thread与threading模块的区别,线程的基本操作如创建、启动、同步及通信。通过具体案例如火车站售票场景,详细解析线程状态管理、锁机制、事件与条件变量的应用,以及如何避免死锁。最后,通过面向对象的方式优化多线程程序。

python3中将thread模块进行了规范内置,更名为_thread,友好的提醒如果你不是并发之编程的骨灰级爱好者,请不要尝试使用_thread进行操作,而是推荐使用操作更加灵活使用更加简洁的threading模块进行并发编程的处理。
_thread模块多线程并发任务的简单实现如下:

import _thread
import time

# 定义函数,函数执行循环遍历指定的循环
def test(num):
    for i in range(num):
        print(_thread.get_indent(), ":", i)


# 通过_thread.start_new_thread()启动两个线程,分别执行test函数
_thread.start_new_thread(test,(2, ))
_thread.start_new_thread(test, (3,))

# 让主线程休眠3s, 等待子线程执行结束
time.steep(3)

1 python中的多线程

官方推荐的_threading模块的多线程并发编程机制,结合时下流行的面向过程/面向对象的编程处理模式,主要有两种操作方式
1.函数式的线程创建方式,适合面向过程程序的并发编程实现
2.面向对象的创建方式,适合面向对象程序的并发编程实现

1.1 threading模块属性和方法
名称描述
Thread线程类,用于创建和管理线程
Event事件类,用于线程同步
Condition条件类,用于线程同步
Lock/RLock锁类,用于线程同步
Timer延时线程,用于在一定事件后执行一个函数
Semaphore/BoundedSemaphore信号量类,用于线程同步
active_count()/activeCount()获取当前 alive 状态的所有线程数量
current_thread()/currentThread()获取当期正在执行的线程对象
get_ident()获取运行中程序当前线程的唯一编号
enumerate()获取所有 alive 状态线程列表
local线程局部数据类
stack_size([size])获取线程占用内存栈的大小
main_thread()获取主线程
1.2 Thread类型属性和方法
名称描述
__init__(group,target,name,args,kwargs)构造方法,创建线程类型
is_alive()/isAlive()判断当前线程是否 alive 状态
run()线程执行方法,自定义线程必须重写该函数
start()线程启动方法
join([timeout=None])线程独占,等待当前线程运行结束或者超时
ident标识当前线程的唯一编号
name当前线程名称
daemon布尔值,判断当前线程是否守护线程

2 函数式开发实现

通过threading模块的Thread函数,可以实现多线程程序的运行
案例需求:火车站窗口售票

单线程实现模式:相当于只有一个窗口售票

import time
# 总票数
count = 10


def sale_ticket():
    '''售票函数,每次出票一张'''
    global count
    while count > 0:
    print("售出一张票", count)
    count -= 1
    time.sleep(0.5)
    else:
    print("售票结束,没有票了..")

if __name__ == "__main__":
    # 循环去窗口买票
    sale_ticket()

多线程售票:相当于多个窗口同时售票

# 引入模块
import threading, time

count = 10


def sale_ticket():
    global count
    while count > 0:
        print(threading.current_thread().getName(), "售出一张票:", count)
        count -= 1
        time.sleep(0.5)
    else:
        print(threading.current_thread().getName(), "没有票了")


if __name__ == "__main__":
    # 定义多个线程(窗口)
    t1 = threading.Thread(name="窗口 1", target=sale_ticket)
    t2 = threading.Thread(name="窗口 2", target=sale_ticket)
    t3 = threading.Thread(name="窗口 3", target=sale_ticket)
    t4 = threading.Thread(name="窗口 4", target=sale_ticket)
    t5 = threading.Thread(name="窗口 5", target=sale_ticket)

    # 启动五个窗口同时售票
    t1.start()
    t2.start()
    t3.start()
    t4.start()
    t5.start()
2.1 线程状态-join

join:线程join状态是独占模式,当前线程独占CPU运行单元,必须等待当前线程执行完成或者超时之后,才能运行其他线程

依然是火车售票代码,窗口1线程join执行

# 引入模块
import threading, time

count = 10


def sale_ticket():
    global count
    while count > 0:
        print(threading.current_thread().getName(), "售出一张票:", count)
        count -= 1
        time.sleep(0.5)
    else:
        print(threading.current_thread().getName(), "没有票了")


if __name__ == "__main__":
    # 定义多个线程(窗口)
    t1 = threading.Thread(name="窗口 1", target=sale_ticket)
    t2 = threading.Thread(name="窗口 2", target=sale_ticket)
    t3 = threading.Thread(name="窗口 3", target=sale_ticket)
    t4 = threading.Thread(name="窗口 4", target=sale_ticket)
    t5 = threading.Thread(name="窗口 5", target=sale_ticket)

    # 启动五个窗口同时售票
    t1.start()
    # 线程 t1 调用 join,独占模式运行,等待 t1 线程运行结束或者超时,才能继续运行其他线程
    t1.join()
    t2.start()
    t3.start()
    t4.start()
    t5.start()
2.2 线程状态-daemon

线程对象的daemon属性用于标识某个线程是否守护线程
一旦主线程执行退出,无论守护线程是否执行完成,都会直接退出!

改造售票程序【将窗口设置为守护线程:假装主线程就是火车站】
火车站一旦关闭,窗口不论剩余是否有票,都会直接退出

# 引入模块
import threading, time

count = 10


def sale_ticket():
    global count
    while count > 0:
        print(threading.current_thread().getName(), "售出一张票:", count)
        count -= 1
        time.sleep(0.5)
    else:
        print(threading.current_thread().getName(), "没有票了")


if __name__ == "__main__":
    # 定义线程(窗口)
    t1 = threading.Thread(name="窗口 1", target=sale_ticket)
    t1.daemon = True
    # 启动售票
    t1.start()
2.3 线程管理-锁【Lock/Rlock】

多线程程序在运行过程中,由于多个线程防问的是同一部分数据,很容易造成共享数据访问冲突的现象e,如果一旦出现冲突程序就会出现执行结果不符合期望的结果

修改售票程序,两个窗口同时售票,修改代码如下:

# 引入模块
import threading, time

cunt = 10


def sale_ticket():
    global count
    while count > 0:
        print(threading.current_thread().getName(), "售出一张票:", count)
        time.sleep(0.5)
        count -= 1
    else:
        print(threading.current_thread().getName(), "没有票了")


if __name__ == "__main__":
    # 定义线程(窗口)
    t1 = threading.Thread(name="窗口 1", target=sale_ticket)
    t2 = threading.Thread(name="窗口 2", target=sale_ticket)

    # 启动售票
    t1.start()
    t2.start()

可能你并没有从上述程序中看到任何BUG的存在,其实上述程序已经造孽了,原因在于多个线程访问共享数据的问题被一行time.sleep(0.5)给放大了。此时共享数据的修改操作,在多线程的情况下,是需要通过锁定的方式进行独占修改的!
python中提供了两种线程锁的操作
同步锁/互斥锁:Lock
可重用锁:RLock
锁的操作主要是获取锁和释放锁两种
acquire()获取锁,上锁,锁定
release()释放前,开锁,解锁
修改上述售票程序,给售票代码添加线程锁

# 引入模块
import threading, time

count = 10

def sale_ticket():
    global count
    # 添加锁定数据
    lock.acquire()
    while count > 0:
        print(threading.current_thread().getName(), "售出一张票:", count)
        time.sleep(0.5)
        count -= 1
    else:
        print(threading.current_thread().getName(), "没有票了")
    # 释放锁
    lock.release()


if __name__ == "__main__":
    # 定义锁对象
    lock = threading.Lock()
    # 定义线程(窗口)
    t1 = threading.Thread(name="窗口 1", target=sale_ticket)
    t2 = threading.Thread(name="窗口 2", target=sale_ticket)

    # 启动售票
    t1.start()
    t2.start()

此时在运行程序,就不会再出现重复售票的情况了

2.4 线程管理-死锁【Dead Lock】

线程锁固然功能强大,可以管理多个线程之间的共享数据问题
但是同时它的强大也带来了比较纠结的问题,需要开发人员对于锁定的数据有一个良好的认知,否则特别容易造成死锁的现象,比较著名的哲学家吃饭问题就是死锁的典型代表
由于计算机运算速度较快,所以有两种方案可以将问题放大
给执行函数添加休眠时间
添加线程数量
死锁并不是每次都会出现的,而是程序在执行过程中,根据系统CPU时间片的切换机制恰好遇到了重复上锁的情况,就会死锁

import threading, time


def zhexuejia1():
    if lock1.acquire():
        if lock2.acquire():
        # time.sleep(0.01)
        print("你给我叉子,我就给你刀子")
        lock2.release()
    lock1.release()


def zhexuejia2():
    if lock2.acquire():
        if lock1.acquire():
            # time.sleep(0.01)
            print("你给我刀子,我就给你叉子")
            lock1.release()
        lock2.release()


if __name__ == "__main__":

    lock1 = threading.Lock()
    lock2 = threading.Lock()

    for i in range(1000):
        zxj = threading.Thread(name="哲学家 A" + str(i), target=zhexuejia1)
        zxj2 = threading.Thread(name="哲学家 B" + str(i), target=zhexuejia2)
        zxj.start()
        zxj2.start()

实际项目开发过程中,一定要注意死锁情况的影响
这样的情况可以通过可重用锁Rlock进行锁定处理!

2.5 线程管理-事件【Event】

线程锁解决了多个线程访问共享数据时冲突的问题,如果多个线程之间需要通信应该怎么解
决呢?此时就需要用到多个线程之间的可以用于互相通信的处理对象了,该处理对象必须满
足如下基本条件
该对象能同时被多个线程访问
该对象可以被标记不同的状态
*该对象可以用于控制线程等待|运行之间的切换

python提供了一个事件对象Event,可以基本满足上述条件,完成线程之间的通信
名称描述
set()添加一个标记状态
isSet()/is_set()检查事件对象是否被标记
clear()清除标记状态
wait()事件对象操作的当前线程等待,直到该对象被标记状态
2.6 油条的故事-Event实现

需求:一个冬天的早晨,顾客去小摊贩哪里买油条;由于油条都是现炸,所以顾客需要等待
小摊贩生产油条;小摊贩生产好油条之后顾客可以进餐;结束后打招呼离开
分析:这里有两个线程:小摊贩线程和顾客线程,顾客线程运行开始必须等待,小摊贩线程
工作生产油条,当油条生产出来之后唤醒顾客线程,此时小摊贩线程等待;顾客就餐完毕之
后准备离开,唤醒小摊贩进程结账走人!

'''
小贩:生产油条
顾客:消费油条
需求:顾客去消费
两个线程的通信:事件对象:threading.Event
set()添加标记
wait()线程等待-如果当前事件对象被标记~继续运行
clear()清除标记
'''
import threading, time

# 定义事件对象
event = threading.Event()


def xiao_fan():
    print("XF:炸油条.....")
    time.sleep(2)
    # 添加标记
    event.set()
    event.clear()
    print("XF:卖油条")
    event.wait()
    print("XF:结账完毕,谢谢光临")


def gu_ke():
    # 线程等待~等待事件对象被标记
    event.wait()
    print("GK:买油条")
    print("GK:吃油条")
    time.sleep(2)
    print("GK:油条真好吃..")
    event.set()
    event.clear()


if __name__ == "__main__":
    xf = threading.Thread(target=xiao_fan)
    gk = threading.Thread(target=gu_ke)
    xf.start()
    gk.start()
2.7 线程管理-条件【Condition】

生产者消费者问题
生产者负责生产食物,将食物存储在列表中;消费者负责消费,也就是从列表中删除数据;
这里的存储食物的列表,我们限制了长度,最多容纳 20 个食物数据

condition对象的属性和方法
名称描述
acquire()锁定
release()解锁
wait()释放锁,同时阻塞当前线程,等待被唤醒
wait_for()释放锁,同时阻塞当前线程,等待被唤醒
notify()唤醒
notify_all()唤醒所有等待该 condition 条件的线程
2.8 生产者消费者 问题-Condition实现

需求:生产者消费者问题,描述的是多个线程之间的通信处理方式和手段。
多个生产者线程生产食品放到指定的食品容器中,并唤醒所有的消费者线程开始就餐
如果食品容器容量饱和,则所有生产者线程等待
多个消费者线程在指定的食品容器中获取食物就餐,并唤醒所有的生产者线程开始生产
如果食品容器中没有任何食品了,则所有消费者线程等待

import threading, time, random

# 定义食物列表
foods = list()

# 创建一个线程条件对象
con = threading.Condition()


def product():
    '''生产者函数:负责生产数据'''
    while True:
        time.sleep(0.5)
        con.acquire()
        if len(foods) < 20:
            _no = random.randint(0, 20)
            print("生产者{}生产了:".format(threading.current_thread().getName()), _no)
            foods.append(_no)
            print("PRO--", len(foods))
            con.notify()
        else:
            con.wait()
            print("生产者{}----等待".format(threading.current_thread().getName()))

        con.release()


def consumer():
    '''消费者函数:负责删除数据'''
    while True:
        time.sleep(0.5)
        con.acquire()
        if len(foods) > 0:
            _no = foods.pop()
            print("消费者{}消费了".format(threading.current_thread().getName()), _no)
            print("CUS--", len(foods))
            con.notify()
        else:
            con.wait()
            print("消费者{}---等待".format(threading.current_thread().getName()))
        con.release()


if __name__ == "__main__":
    # 创建多个生产者线程
    for i in range(5):
        p = threading.Thread(name="_p" + str(i), target=product)
        p.start()

    # 创建多个消费者线程
    for j in range(2):
        c = threading.Thread(name="_c" + str(j), target=consumer)
        c.start()
2.9 线程管理-队列【Queue】

多线程并发编程的重点,是线程之间共享数据的访问问题和线程之间的通信问题
为了解决线程之间数据共享问题,PYTHON 提供了一个数据类型【队列】可以用于在多线程
并发模式下,安全的访问数据而不会造成数据共享冲突

python 中的 queue 模块提供的队列类型 Queue 的操作模式如下
名称描述
put([timeout=None])向队列中添加数据,队列如果满了,一直阻塞直到超时或者队列中有数据被删除之后添加成功
get([timeout=None])从队列中获取数据,如果队列为空,一直阻塞直到超时或者队列中添加数据之后获取成功
2.10 生产者消费者问题-Queue队列实现
import threading, time, random, queue

foods = queue.Queue(5)


def product():
    while True:
        time.sleep(0.5)
        try:
            _no = random.randint(0, 10)
            print(threading.current_thread().getName(), "生产者开始生产食物:", _no)
            foods.put(_no, timeout=1)
        except:
            print(threading.current_thread().getName(),"篮子中食物已满,等待消费者消费")


def cusumer():
    while True:
        time.sleep(0.5)
        try:
            _no = foods.get(timeout=1)
            print(threading.current_thread().getName(),"消费者消费食物", _no)
        except:
            print(threading.current_thread().getName(),"篮子空了,等待生产者生产数据")


if __name__ == "__main__":
    # 生产者
    for i in range(5):
        p = threading.Thread(name="_p" + str(i), target=product)
        p.start()

    # 消费者
    for j in range(2):
        c = threading.Thread(name="_c" + str(j), target=cusumer)
        c.start()

3 面向对象多线程基本语法

面向对象的多线程,主要是让自定义类型派生自 threading.Thread 类
重写 Thread 类型的 run()方法,然后创建自定义线程类的对象之后,调用 start()方法启动

# 引入依赖的模块
import threading, time

# 定义锁对象
lock = threading.Lock()

# 定义票数量
count = 20

class TicketSaleWindow(threading.Thread):

    '''自定义线程类型,继承 threading.Thread 类型'''
    def __init__(self, name):
        '''初始化函数:初始化线程名称'''
        super().__init__(name=name)

    def run(self):
        '''重写线程执行 run()方法,该方法会再 start()方法调用时自动执行'''
        global count
        while True:
            time.sleep(0.5)
            lock.acquire()
            if count > 0:
                print(threading.current_thread().getName(), "售出:", count)
                count -= 1
                lock.release()
            else:
                print(threading.current_thread().getName(), "售罄了")
                lock.release()
                break


if __name__ == "__main__":
    for i in range(3):
        # 创建线程类型
        win = TicketSaleWindow("窗口" + str(i))
        # 启动线程
        win.start()

转载于:https://www.cnblogs.com/chenliang0309/p/9781738.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值