Python多任务编程之线程

多任务编程:

  1. 作用:充分利用计算机多核资源,提高程序的运行效率。
  2. 实现方案:多进程;多线程
  3. 相关概念:
    1. 并发:多个任务在同一时间间隔内发生。表面上看像是多个任务同时进行,实际是任务在时间片上的轮转(即多个任务在内核上以极短的时间快速切换),也就是说,每个时刻只有一个任务占有内核资源。
    2. 并行:多个任务利用计算机多核资源而同时执行,称这些多个任务间为并行关系

一、线程概述

1、线程的概念

  • 线程(Thread)是操作系统能够进行运算调度(分配内核)的最小单位,它被包含在进程之中,是进程的实际运作单位,一条线程指的是进程中一个单一顺序的控制流。
  • 线程是OS直接支持的执行单元,一个进程中的线程既可以并行地执行不同任务。如,对于一个视频播放进程,显示视频用一个线程,播放音频用另一个线程,两个线程同时工作,才能同时观看画面和声音同步的视频。

2、其他说明

  1. 线程属于进程的一部分,一个可包含多个线程
  2. 多个线程之间的运行互不影响、各自运行
  3. 一个进程中的所有线程共享这个进程的资源
  4. 线程的创建和销毁所消耗的资源远小于进程
  5. 各线程具备各自的线程信息

3、Python中的线程模块

Python中的标准款提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需使用threading模块即可。

二、基于threading模块的多线程

1、思路

Thread创建流程

2、实现方法

①.创建线程对象

from threading import Thread
t = Thread([group, [target, [name, [args, [kwargs, [daemon]]]]]])
参数:
         group:保留参数,为以后版本而保留
         target:可调用对象,一般绑定要执行的目标函数;run()方法将调用此对象,默认值None,表示不调用任何内容
         name:当前线程名称,默认创建格式为Thread-N
         args:传递给target函数的参数元组
         kwargs:传递给target函数的参数字典
         daemon:默认None,表示从创建的进程继承;实际用户自定义进程一般为默认值为False

②.启动线程

t.start()
功能:开始线程活动,它在一个线程里做多只能被调用一次,大于一次将抛异常RuntimeError。

③.回收线程

t.join(timeout=None)
功能:等待,直到线程结束

import threading, os
from time import sleep

# 线程函数
def music(loop, m_name):
    global a
    a = 10000
    print(a)
    for i in range(loop):
        sleep(2)
        print(os.getpid(), "Play music: " + m_name)

if __name__ == "__main__":
    a = 1
    # 创建线程对象(分支线程)
    t = threading.Thread(target=music, args=(5, ), kwargs={'m_name': "你不是真正的快乐"})

    t.start() # 开启分支线程
    # 主线程任务
    for i in range(3):
        sleep(3)
        print(os.getpid(), "Have a dance ...")

    t.join()  # 回收线程
    print("main thread a :", a)  # 此处的a值已被线程修改
④.线程对象的其他相关方法/属性

t.name             线程名称
t.setName()     设置线程名称
t.getName()     获取线程名称
t.is_alive()        判断线程实例是否在运行
t.daemon         设置主线程和分支线程的退出关系
t.setDaemon() 设置daemon属性值
t.isDaemon()    判断是否为守护线程

from threading import Thread
from time import sleep

def fun():
    sleep(3)
    print("线程属性测试")

if __name__ == "__main__":
    t = Thread(target=fun)
    print(t.daemon)
    t.daemon = True                     # 主线程退出分支线程也退出
    t.start()

    print("Thread name:", t.getName())  # 打印线程名称
    t.setName("owhyt")
    print(t.name)
    
    print("is alive:", t.is_alive())    # 线程是否存活
⑤.自定义线程类——重写Thread类的run()方法
  1. 创建步骤
    1. 继承Thread类
    2. 重写__init__方法添加自定义属性,super方法加载父类属性
    3. 重写run()方法
  2. 使用方法
    1. 实例化自定义线程类
    2. 调用start()方法,会自动执行run()方法
    3. 调用join()回收进程
from threading import Thread
from time import sleep, ctime

class MyThread(Thread):
    def __init__(self, target, args=(), kwargs=None, name=None):
        super().__init__()
        self.target = target
        self.args = args
        self.kwargs = kwargs
        self.name = name

    def run(self):
        print(self.name)
        self.target(*self.args, **self.kwargs)

def player(sec, song):
    for i in range(2):
        print("Playing %s:%s" % (song, ctime()))
        sleep(sec)

if __name__ == "__main__":
    t = MyThread(target=player, args=(3,), kwargs={'song': '画'}, name='happy')
    t.start()
    t.join()

三、线程间通信——全局变量(共享内存)

Ⅰ 相关概念

  • 从第一个线程示例代码来看,在一个进程内的所有线程共享全局变量。使用全局变量完成多线程间数据共享的过程,称为线程间通信。
  • 全局变量可以被多个线程共享,我们将有全局变量性质的、多个进程或线程都可以操作的数据资源,称为共享资源
  • 将一次只能被一个进程所使用的资源称为临界资源
  • 对临界资源的操作代码段为临界区

Ⅱ 争夺共享资源的问题及解决方法

  • 问题:由于线程可以对全局变量随意修改,这就可能造成多线程之间对全局变量的混乱操作。
  • 解决方法:同步互斥机制

Ⅲ 线程/进程间的制约关系——同步互斥机制

  1. 同步:也称直接制约关系,多线程或多进程为完成任务所形成的一种协同工作关系,并按需在某些位置上协调它们的工作次序,而等待、传递信息所产生的制约关系。
  2. 互斥:也称间接制约关系,当一个进程/线程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源。
  3. 同步互斥在进程间通信的应用举例
    1.TCP网络通信对网络网络缓冲区的访问是互斥的
    2.多个打印任务(进程)对打印机的访问是同步互斥的
  4. 为禁止多进程或多线程同时进入临界区,同步互斥机制应遵循以下准则
    1.空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区
    2.忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待
    3.有限等待。对请求访问临界区的进程,应保证能在有限时间内进入临界区
    4.让权等待。当进程不能进入临界区时,应立即释放处理器,防止进程盲等

Ⅳ Python线程同步互斥的常用方法

1.线程锁 threading.Lock
①.简述

  为防止多个线程同时读写共享资源而给资源引入的一个状态:锁定或非锁定。即,当一个线程占有资源时会进行加锁处理,此时其他线程无法操作该资源,直到解锁后方能操作。互斥保证每次只有一个线程进行写入操作,以确保多线程下数据的唯一性和正确性。

②.实现方法

mutex = threading.Lock()
功能:创建锁对象

mutex.acquire([blocking=True])
功能:上锁,在已上锁的情况再次锁定会导致阻塞
参数&返回值:
blocking 默认值为True,表示阻塞到锁被占用的线程释放为止,然后将上锁并返回True
              设置为False时,当无法获取锁时立即返回False

mutex.release()
功能:解锁
说明:当锁对象处于未锁定状态而调用该方法,或,锁对象与调用acquire方法的锁对象不是同一个时,程序将出错

mutex = Lock()
mutex.acquire()
# 临界区
mutex.release()
with Lock() as mutex:
      # 临界区
      # with代码块自动加锁和解锁
from threading import Thread, Lock
import time
n = 100  # 共100张电影票

def task(mutex):
    global n
    # mutex.acquire()
    # temp = n
    # time.sleep(0.3)
    # n = temp - 1
    # print("购票成功,剩余%d张电影票" % n)
    # mutex.release()
    with mutex as m:
        tmp = n
        time.sleep(0.1)
        n = tmp - 1
        print("购票成功,剩余%d张电影票" % n)

if __name__ == "__main__":
    mutex = Lock()
    threads = []
    for i in range(10):
        t = Thread(target=task, args=(mutex, ))
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
③.死锁及其处理

⑴ 定义
  多个进程或线程因资源竞争而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。这种状态称为死锁。
⑵ 死锁产生的必要条件

  1. 互斥条件:进程要求对所分配的资源进行排他性控制,即,在一段时间内某资源仅为一个进程所占有。
  2. 不剥夺条件:进程所获资源在未使用完毕前,不能被其他进程强行夺走,换言之,只能由获得该资源的进程主动释放
  3. 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  4. 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中的下一个进程所请求。

⑶ 预防死锁

  • 设置某些限制条件,破坏产生死锁的四个必要条件中的一个或几个,以防止发生死锁。但由于所施加的限制条件往往太严格,可能会导致系统资源利用率降低。
  • 预防死锁的方法
    1.在恰当的位置设置一定的延时时间。
    2.使用重载锁RLock(),允许线程对资源进行重复加锁。
    • 针对复杂程序容易对临界资源重复加锁的情况
    • RLock的内部维护着一个Lock和一个counter变量,counter变量记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
  1. 示例一:在适当的位置设置一定的延时时间
import time, threading

# 账户类
class Account:
    def __init__(self, id, balance, lock):
        self.id = id  # 用户id
        self.balance = balance  # 存款金额
        self.lock = lock  # 锁

    # 取钱
    def withdraw(self, amount):
        self.balance -= amount

    # 存钱
    def deposit(self, amount):
        self.balance += amount

    # 获取账户金额的函数
    def get_balance(self):
        return self.balance

# 转账函数
def transfer(from_, to, amount):
    if from_.lock.acquire():    # 如果上锁成功则返回True(锁住自己的账户)
        from_.withdraw(amount)  # 自己账户金额减少
        # time.sleep(0.5)       # 设置该休眠时间导致死锁
        if to.lock.acquire():   # 对方账户上锁
            to.deposit(amount)  # 对方账户金额增加
            to.lock.release()   # 对方账户解锁
        from_.lock.release()    # 自己账户解锁
    print("转账完成")

if __name__ == "__main__":
    # 创建两个账户
    Hange = Account('Abby', 5000, threading.Lock())
    Levi = Account('Levi', 3000, threading.Lock())

    t = threading.Thread(target=transfer, args=(Hange, Levi, 1500))
    t2 = threading.Thread(target=transfer, args=(Levi, Hange, 1000))
    t.start()
    # time.sleep(2)  # 错开两次转账的时机,避免死锁
    t2.start()
    t.join()
    t2.join()

    print("Hange:", Hange.get_balance())
    print("Levi:", Levi.get_balance())
  1. 示例二:使用重载锁RLock()
from threading import Thread, Lock, RLock
import time
num = 0
# lock = Lock()
lock = RLock()

class MyThread(Thread):
    def fun1(self):
        global num
        with lock:
            num -= 1

    def fun2(self):
        global num
        if lock.acquire():
            num += 1
            if num > 5:
                self.fun1()
            print("num =", num)
            lock.release()

    def run(self):
        while True:
            time.sleep(2)
            self.fun2()

for i in range(10):
    t = MyThread()
    t.start()
    t.join()
2.事件对象threading.Event

  这是线程之间通信的最简单机制之一:一个线程发出事件信号,而其他线程等待该信号。而该机制通过阻塞行为和解除阻塞行为,来达到同步互斥的目的。

实现方法

e = Event()       # 创建线程Event对象;       一个Event对象管理一个内部标志。
e.wait([timeout=None])  # 阻塞等待e事件对象被set;    阻塞线程直到内部变量为true
e.set()         # 设置e事件对象,使wait结束阻塞; 将内部标志设置为false
e.clear()         # 使e事件对象回到未被设置状态   将内部标志设置为false
e.is_set()        # 查看当前e是否被设置      当且仅当内部标志为true时返回True

代码示例如下,可对比threading.lock第一个示例:

from threading import Thread, Lock, Event
import time
n = 100  # 共100张电影票

def task():
    global n
    temp = n
    time.sleep(0.3)
    n = temp - 1
    print("购票成功,剩余%d张电影票" % n)
    e.set()

if __name__ == "__main__":
    threads = []
    for i in range(10):
        e = Event()
        t = Thread(target=task)
        threads.append(t)
        t.start()
        e.wait()
    for t in threads:
        t.join()

Ⅴ 使用队列进行进程间通信

  说到底,在主线程定义消息队列Queue,还是属于一种全局变量。
  使用queue模块的Queue在线程间通信通常应用于生产者消费者模式。产生数据的模块称为生成者,处理数据的模块称为消费者。在生产者和消费者之间的缓冲区称为仓库。生产者负责网仓库运输商品,而消费者负责从仓库里取出商品,这就构成了生产者消费者模式。

from queue import Queue
import random,threading,time

# 生产者类
class Producer(threading.Thread):
    def __init__(self, name,queue):
        threading.Thread.__init__(self, name=name)
        self.data=queue
    def run(self):
        for i in range(5):
            print("生成者%s将产品%d加入队列!" % (self.getName(), i))
            self.data.put(i)
            time.sleep(random.random())
        print("生成者%s完成!" % self.getName())

# 消费者类
class Consumer(threading.Thread):
    def __init__(self,name,queue):
        threading.Thread.__init__(self,name=name)
        self.data=queue
    def run(self):
        for i in range(5):
            val = self.data.get()
            print("消费者%s将产品%d从队列中取出!" % (self.getName(),val))
            time.sleep(random.random())
        print("消费者%s完成!" % self.getName())

if __name__ == '__main__':
    print('-----主线程开始-----')
    queue = Queue()        # 实例化队列
    producer = Producer('Producer',queue)   # 实例化线程Producer,并传入队列作为参数
    consumer = Consumer('Consumer',queue)   # 实例化线程Consumer,并传入队列作为参数
    producer.start()    # 启动线程Producer
    consumer.start()    # 启动线程Consumer
    producer.join()     # 等待线程Producer结束
    consumer.join()     # 等待线程Consumer结束
    print('-----主线程结束-----')

四、Python线程的GIL(全局解释器锁)问题

1、概念

  由于在设计Python解释器时,开发人员加入了解释器锁,导致Python解释器同一时刻只能解释执行一个线程,无法并行执行,大大降低了线程的执行效率。

2、影响

  因为遇到阻塞时线程会主动让出解释器,去解释其他线程。所以,Python多线程适合执行多阻塞高延迟IO任务,且执行效率比单线程更高。但是,其他情况对效率提升没有帮助。
  在无阻塞状态下,多线程程序和单线程程序的执行效率差不多,甚至还不如单线程效率。但是,使用多进程解决相同的任务,效率明显提升。

3、解决措施

①.尽量使用进程完成无阻塞的并发行为。
②.不使用Cython解释器(C语言开发),转而用Jython解释器(Java开发)或IronPython解释器(.Net开发)等。

4、示例——GIL问题对计算/IO密集型程序运行的影响

代码如下:

import time
from threading import Thread

# 计算密集型函数
def count(x, y):
    c = 0
    while c < 7000000:
        c += 1
        x += 1
        y += 1

# io密集型函数
def io():
    write()
    read()

def write():
    f = open('test', 'w')
    for i in range(1500000):
        f.write("hello world\n")
    f.close()

def read():
    f = open('test')
    lines = f.readlines()
    f.close()

if __name__ == "__main__":
    # 比较计算密集型程序单线程运行10次和10线程运行的耗时结果
    time1 = time.time()
    for i in range(10):
        count(1, 1)
    time2 = time.time()
    run_time = time2 - time1
    print("计算密集型程序单线程运行10次耗时:", run_time)

    time3 = time.time()
    jobs = []
    for i in range(10):
        t = Thread(target=count, args=(1, 1))
        jobs.append(t)
        t.start()
    for i in jobs:
        i.join()
    time4 = time.time()
    run_time2 = time4 - time3
    print("计算密集型程序10线程运行一次耗时:", run_time2)

    # 比较IO密集型程序单线程运行10次和10线程运行的耗时结果
    time5 = time.time()
    for i in range(10):
        io()
    time6 = time.time()
    run_time3 = time6 - time5
    print("IO密集型程序单线程运行10次耗时:", run_time3)

    time7 = time.time()
    tasks = []
    for i in range(10):
        t = Thread(target=io)
        tasks.append(t)
        t.start()
    for i in jobs:
        i.join()
    time8 = time.time()
    run_time4 = time8 - time7
    print("IO密集型程序10线程运行一次耗时:", run_time4)

运行结果如下:

计算密集型程序单线程运行10次耗时: 10.351589679718018
计算密集型程序10线程运行1次耗时: 12.838714838027954
IO密集型程序单线程运行10次耗时: 6.566418409347534
IO密集型程序10线程运行1次耗时: 0.0047419071197509766

五、进程与线程的区别联系

1.进程时系统进行资源分配和调度的独立单位,线程是进程的一个实体,时CPU调度和分派的基本单位。
2.进程空间相互独立,数据互不干扰,有专门的通信方法;同一个进程中的多线程是内存共享的,它们使用全局变量进行通信。
3.进程的创建和销毁所耗资源比线程的多。
4.多个线程共享进程资源,在共享资源时往往需要同步互斥处理。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值