python多线程

python多线程

本文参考:

Python threading实现多线程 基础篇 - 知乎 (zhihu.com)

Python threading实现多线程 提高篇 线程同步,以及各种锁 - 知乎 (zhihu.com)

进程与线程

进程是资源分配的最小单位,一个程序至少有一个进程。

线程是程序执行的最小单位,一个进程至少有一个线程。

进程都有自己独立的地址空间,内存,数据栈等,所以进程占用资源多。由于进程的资源独立,所以通讯不方便,只能使用进程间通讯(IPC)。

线程共享进程中的数据,他们使用相同的地址空间,使用线程创建快捷,创建开销比进程小。同一进程下的线程共享全局变量、静态变量等数据,所以线程通讯非常方便,但会存在数据同步与互斥的问题,如何处理好同步与互斥是编写多线程程序的难点。

一个进程中可以存在多个线程,在单核CPU中每个进程中同时刻只能运行一个线程,只有在多核CPU中才能存在线程并发的情况。

当线程需要运行但没有运行空间时,会对线程的优先级进行判断,高优先级先运行,低优先级进程让行。

IO密集型程序和CPU密集型程序

Python的多线程,只有用于I/O密集型程序时效率才会有明显的提高

什么是IO密集型?

IO密集型

IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等IO (硬盘/内存) 的读写操作,因此,CPU负载并不高。

密集型的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而程序的逻辑做得不是很好,没有充分利用处理器能力。

CPU 使用率较低,程序中会存在大量的 I/O 操作占用时间,导致线程空余时间很多,通常就需要开CPU核心数数倍的线程。

CPU密集型

CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作CPU读写IO(硬盘/内存)时,IO可以在很短的时间内完成,而CPU还有许多运算要处理,因此,CPU负载很高。

CPU密集表示该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就只有这么多。

CPU使用率较高(例如:计算圆周率、对视频进行高清解码、矩阵运算等情况)的情况下,通常,线程数只需要设置为CPU核心数的线程个数就可以了。 这一情况多出现在一些业务复杂的计算和逻辑处理过程中。比如说,现在的一些机器学习和深度学习的模型训练和推理任务,包含了大量的矩阵运算。

CPU密集型与IO密集型任务的使用说明

  • 当线程等待时间所占比例越高,需要越多线程,启用其他线程继续使用CPU,以此提高CPU的利用率;
  • 当线程CPU时间所占比例越高,需要越少的线程,通常线程数和CPU核数一致即可,这一类型在开发中主要出现在一些计算业务频繁的逻辑中。

全局解释器锁(GIL)

Python代码的执行是由Python虚拟机进行控制。它在主循环中同时只能有一个控制线程在执行,意思就是Python解释器中可以运行多个线程,但是在执行的只有一个线程,其他的处于等待状态。

这些线程执行是有全局解释器锁(GIL)控制,它来保证同时只有一个线程在运行。在多线程运行环境中,Python虚拟机执行方式如下

1.设置GIL
2.切换进线程
3.执行下面的操作之一:
	运行指定数量的字节码指令
	线程主动让出控制权
4.切换线程(线程处于睡眠状态)
5.解锁GIL
6.进入1

python线程的创建(threading)

import threading
from time import sleep,ctime
def func(name,sec):
    print('---开始---', name, '时间', ctime())
    sleep(sec)
    print('***结束***', name, '时间', ctime())
   
#创建Thread实例
t1 = threading.Thread(target=func,args=(1,1)) #传入参数䣌时候要以元组的形式传入
t2 = threading.Thread(target=func,args=(2,2))
# 启动线程运行
t1.start()
t2,start()
# 等待所有线程执行完毕
t1.join()
t2.join()

join()方法只有在你需要等待线程完成然后在做其他事情的时候才是有用的

派生Thread 的子类,并创建子类的实例

我们可以通过继承Thread类,派生出一个子类,使用子类来创建多线程

from threading import Thread
from time import sleep, ctime


# 创建 Thread 的子类
class MyThread(Thread):
    def __init__(self, func, args):
        '''
        :param func: 可调用的对象
        :param args: 可调用对象的参数
        '''
        Thread.__init__(self)   # 不要忘记调用Thread的初始化方法
        self.func = func
        self.args = args

    def run(self):
        self.func(*self.args)


def func(name, sec):
    print('---开始---', name, '时间', ctime())
    sleep(sec)
    print('***结束***', name, '时间', ctime())

def main():
    # 创建 Thread 实例
    t1 = MyThread(func, (1, 1))
    t2 = MyThread(func, (2, 2))
    # 启动线程运行
    t1.start()
    t2.start()
    # 等待所有线程执行完毕
    t1.join()
    t2.join()

if __name__ == '__main__':
    main()

多线程的资源同步问题

一般在多线程代码中,总会有一些特定的函数或代码块不想被多个线程同时执行,如:修改数据库、更新文件或其他会产生程序冲突的类似情况。

如果两个线程的运行顺序不同,他有可能产生不同的结果,或者造成执行的轨迹或行为不相同,这时我们就需要使用到多线程的同步。

当任意数量的线程可以访问临界区的代码,当在同一时刻只能有一个线程可以通过时,就需要使用同步。我们可以选择合适的同步原语,也可以让线程控制机制来执行同步。同步有不同的类型,python也支持多种同步类型,你可以选择最合适的来运行程序。

最常用的同理原语有:锁/互斥,以及信号量。锁是最简单最低级的机制。信号量用于多线程竞争有限资源的情况。

同步锁

加锁与解锁

import threading

lock = threading.Lock()

#加锁
lock.acquire()

#解锁
lock.release()

示例

import time
import threading

# 生成一个锁对象
lock = threading.Lock()


def func():
    global num  # 全局变量
    # lock.acquire()  # 获得锁,加锁
    num1 = num
    time.sleep(0.1)
    num = num1 - 1
    # lock.release()  # 释放锁,解锁
    time.sleep(2)


num = 100
l = []

for i in range(100):  # 开启100个线程
    t = threading.Thread(target=func, args=())
    t.start()
    l.append(t)

# 等待线程运行结束
for i in l:
    i.join()

print(num)

在使用锁前后的运行结果

#使用前
99
使用后
0

在func函数中,我们在num的-1操作前使用了sleep(0.1),当没加锁时,线程被释放,后面的线程执行的都是num=100-1,最后的输出结果为99

Lock 与GIL(全局解释器锁)存在区别

Lock 锁的目的,它是为了保护共享的数据,同时刻只能有一个线程来修改共享的数据,而保护不同的数据需要使用不同的锁。

GIL用于限制一个进程中同一时刻只有一个线程被CPU调度,GIL的级别比Lock高,GIL是解释器级别。

GIL与Lock同时存在,程序执行如下:

1.同时存在两个线程A和B
2.线程A抢占到GIL,进入CPU执行,并加了Lock,但为执行完毕,线程被释放
3.线程B 抢占到GIL,进入CPU执行,执行时发现数据被线程A Lock,于是线程B被阻塞
4.线程B的GIL被夺走,有可能线程A拿到GIL,执行完操作、解锁,并释放GIL
5.线程B再次拿到GIL,才可以正常执行

信号量(Semaphore)

semaphore = threading.Semaphore(5)  # 创建信号量对象,5个线程并发
semaphore.acquire() # 获取共享资源,信号量计数器-1
semaphore.release()  # 释放共享资源,信号量计数器+1

信号量控制规则:当计数器大于0时,那么可以为线程分配资源权限;当计数器小于0时,未获得权限的线程会被挂起,直到其他线程释放资源

import time
import threading
import random

# 创建信号量对象,信号量设置为3,需要有3个线程才启动
semaphore = threading.Semaphore(3)


def func():

    if semaphore.acquire():  # 获取信号 -1
        print(threading.currentThread().getName() + '获得信号量')
        time.sleep(random.randint(1, 5))
        semaphore.release()  # 释放信号 +1
        print(threading.currentThread().getName() + '释放信号量')

l=[]
for i in range(10):
    t1 = threading.Thread(target=func)
    t1.start()
    l.append(t1)
    
for j in l:
    j.join()
    
print("end")

运行结果

Thread-4获得信号量
Thread-5获得信号量
Thread-6获得信号量
Thread-4释放信号量
Thread-6释放信号量
Thread-7获得信号量
Thread-8获得信号量
Thread-7释放信号量
Thread-9获得信号量
Thread-5释放信号量
Thread-10获得信号量
Thread-8释放信号量
Thread-10释放信号量
Thread-11获得信号量
Thread-12获得信号量
Thread-9释放信号量
Thread-13获得信号量
Thread-11释放信号量
Thread-12释放信号量
Thread-13释放信号量
end

根据运行结果来看,最多只有3个线程同时获得资源,只有在有线程释放时,其他线程才能够获得资源

运用信号量进行线程同步

import threading
import time
import random

# 同步两个不同线程,信号量被初始化0
semaphore = threading.Semaphore(0)


def consumer():
    print("-----等待producer运行------")
    semaphore.acquire()  # 获取资源,信号量为0被挂起,等待信号量释放
    print("----consumer 结束----- 编号: %s" % item )


def producer():
    global item  # 全局变量
    time.sleep(3)
    item = random.randint(0, 100)  # 随机编号
    print("producer运行编号: %s" % item)
    semaphore.release()


if __name__ == "__main__":
    for i in range(0, 4):
        t1 = threading.Thread(target=producer)
        t2 = threading.Thread(target=consumer)
        t1.start()
        t2.start()
        t1.join()
        t2.join()
    print("程序终止")

信号量被初始化为0,目的是同步两个或多个线程。线程必须并行运行,所以需要信号量同步。

运行结果

-----等待producer运行------
producer运行编号: 88
----consumer 结束----- 编号: 88
-----等待producer运行------
producer运行编号: 57
----consumer 结束----- 编号: 57
-----等待producer运行------
producer运行编号: 37
----consumer 结束----- 编号: 37
-----等待producer运行------
producer运行编号: 12
----consumer 结束----- 编号: 12
程序终止

Condition 条件变量

condition = threading.Condition()#定义条件变量锁

示例1:生产与消费,线程produce生产产品当产品生产成功后通知线程B使用产品,线程consume使用完产品后通知线程produce继续生产产品。

import threading
import time

# 商品
product = None
# 条件变量对象
con = threading.Condition()


# 生产方法
def produce():
    global product  # 全局变量产品
    if con.acquire():
        while True:
            print('---执行,produce--')
            if product is None:
                product = '袜子'
                print('---生产产品:%s---' % product)
                # 通知消费者,商品已经生产
                con.notify()  # 唤醒消费线程
            # 等待通知
            con.wait()
            time.sleep(2)


# 消费方法
def consume():
    global product
    if con.acquire():
        while True:
            print('***执行,consume***')
            if product is not None:
                print('***卖出产品:%s***' % product)
                product = None
                # 通知生产者,商品已经没了
                con.notify()
            # 等待通知
            con.wait()
            time.sleep(2)


if __name__=='__main__':
    t1 = threading.Thread(target=consume)
    t1.start()
    t2 = threading.Thread(target=produce)
    t2.start()

运行结果

***执行,consume***
---执行,produce--
---生产产品:袜子---
***执行,consume***
***卖出产品:袜子***
---执行,produce--
---生产产品:袜子---
***执行,consume***
***卖出产品:袜子***
---执行,produce--
---生产产品:袜子---
***执行,consume***
***卖出产品:袜子***
---执行,produce--
---生产产品:袜子---
***执行,consume***
***卖出产品:袜子***

Queue保持线程同步

Queue 模块可以实现多生产者与多消费者队列,它可以实现多个线程之间的信息安全交换。

它有几种队列模式:

FIFO队列,先进先出

q = queue.Queue(10)  # FIFO 队列,最多放入10个项目
q.put(123) # 队列中存入项目123

LIFO队列,后进先出,如同栈

q = queue.LifoQueue()   # LIFO 队列,项目数无限 
q.put(123) # 队列中存入项目123

Priority队列,对着中的数据始终保持排序,优先检索最低值。 通常使用 (优先序号, 数据)形式存储数据。不带需要默认对其值进行排序。
注意:Priority队列因为是顺序的,存储的数据必须是要能排序,即相同类型数据

q = queue.PriorityQueue(10) # Priority 队列,最多放入10个项目
q.put((1,'a')) # 队列中存入项目(1,'a')

示例:生产 消费模式

import threading, time
import queue

# 最多存入10个
q = queue.PriorityQueue(10)


def producer(name):
    ''' 生产者 '''
    count = 1
    while True:
        #  生产袜子
        q.put("袜子 %s" % count)  # 将生产的袜子方法队列
        print(name, "---生产了袜子", count)
        count += 1
        time.sleep(0.2)


def consumer(name):
    ''' 消费者 '''
    while True:
        print("%s ***卖掉了[%s]" % (name, q.get()))  # 消费生产的袜子
        time.sleep(1)
        q.task_done()  # 告知这个任务执行完了


# 生产线程
z = threading.Thread(target=producer, args=("张三",))
# 消费线程
l = threading.Thread(target=consumer, args=("李四",))
w = threading.Thread(target=consumer, args=("王五",))

# 执行线程
z.start()
l.start()
w.start()
posted @ 2023-07-21 23:30  Saku1a  阅读(63)  评论(0)    收藏  举报