python线程

python的知识回顾。本章是线程。

1)简单的概念

并行是指两个或者多个事件在同一时刻发生,而并发是指两个或多个事件在同一时间间隔发生。. 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
(1)直接创建 Thread ,将一个 callable 对象从类的构造器传递进去,这个 callable 就是回调函数,用来处理任务。

(2)编写一个自定义类继承 Thread,然后复写 run() 方法,在 run() 方法中编写任务处理代码,然后创建这个 Thread 的子类。
Thread 的构造方法中,最重要的参数是 target,所以我们需要将一个 callable 对象赋值给它,线程才能正常运行。

import threading
import time

def test():

    for i in range(5):
        print('test ',i)
        time.sleep(1)


thread = threading.Thread(target=test)
thread.start()

(3)thread的生命周期
1.创建对象时,代表 Thread 内部被初始化。
2.调用 start() 方法后,thread 会开始运行。
3.thread 代码正常运行结束或者是遇到异常,线程会终止。

2)Thread 的 is_alive() 方法查询线程是否还在运行

is_alive() 返回 True 的情况是 Thread 对象被正常初始化,start() 方法被调用,然后线程的代码还在正常运行

import threading
import time

def test():

    for i in range(5):
        print(threading.current_thread().name+' test ',i)
        time.sleep(0.5)


thread = threading.Thread(target=test,name='TestThread')
# thread = threading.Thread(target=test)
thread.start()

for i in range(5):
    print(threading.current_thread().name+' main ', i)
    print(thread.name+' is alive ', thread.isAlive())
    time.sleep(1)


***************************输出*********************************
TestThread test  0
MainThread main  0
TestThread is alive  True
TestThread test  1
MainThread main  1
TestThread is alive  True
TestThread test  2
TestThread test  3
MainThread main TestThread test   24

TestThread is alive  True
MainThread main  3
TestThread is alive  False
MainThread main  4
TestThread is alive  False

3)join()提供线程阻塞

默认的情况是,join() 会一直等待对应线程的结束,但可以通过参数赋值,等待规定的时间就好了。在start()方法后,添加join方法。

thread.start()
thread.join()

join规定时间:
timeout 是一个浮点参数,单位是秒

 join(timeout=None):

4)Thread 中的 daemon 属性

TestThread 中 daemon 属性默认是 False,这使得 MainThread 需要等待它的结束,自身才结束
达到MainThread 结束,子线程也立马结束,在子进程中添加:

thread = threading.Thread(target=test,name='TestThread',daemon=True)5
import threading
import time

def test():

    for i in range(5):
        print(threading.current_thread().name+' test ',i)
        time.sleep(2)


thread = threading.Thread(target=test,name='TestThread')
# thread = threading.Thread(target=test,name='TestThread',daemon=True)
thread.start()


for i in range(5):
    print(threading.current_thread().name+' main ', i)
    print(thread.name+' is alive ', thread.isAlive())
    time.sleep(1)


*******************输出***********************
TestThread test  0
MainThread main  0
TestThread is alive  True

MainThread main  1
TestThread is alive  True
TestThread test  1
MainThread main  2
TestThread is alive  True
MainThread main  3
TestThread is alive  True
TestThread test  2
MainThread main  4
TestThread is alive  True
TestThread test  3
TestThread test  4

5)自定义类继承Thread

import threading
import time


class TestThread(threading.Thread):

    def __init__(self,name,age):
        threading.Thread.__init__(self)
        self.name = name
        self.age = age

    def run(self):
        for i in range(5):
            # print(threading.current_thread().name + ' test ', i)
            print('{}岁的{}正在进行第{}次奔跑'.format(str(self.age),self.name,i))
            time.sleep(1)


thread = TestThread(age = 4,name = 'dog')
thread.start()
thread.join()
thread2 = TestThread(age = 3,name = 'dog2')
thread2.start()
*******************输出***********************
4岁的dog正在进行第0次奔跑
4岁的dog正在进行第1次奔跑
4岁的dog正在进行第2次奔跑
4岁的dog正在进行第3次奔跑
4岁的dog正在进行第4次奔跑
3岁的dog2正在进行第0次奔跑
3岁的dog2正在进行第1次奔跑
3岁的dog2正在进行第2次奔跑
3岁的dog2正在进行第3次奔跑
3岁的dog2正在进行第4次奔跑

6)threading 模块中 Lock 类的用法

1.acquire(blocking=True, timeout=-1):请求对 Lock 或 RLock 加锁,其中 timeout 参数指定加锁多少秒。
2.release():释放锁。
Lock 和 RLock 的区别如下:

  • threading.Lock:它是一个基本的锁对象,每次只能锁定一次,其余的锁请求,需等待锁释放后才能获取。
  • threading.RLock:它代表可重入锁(Reentrant Lock)。对于可重入锁,在同一个线程中可以对它进行多次锁定,也可以多次释放。如果使用 RLock,那么 acquire() 和 release() 方法必须成对出现。如果调用了 n 次 acquire() 加锁,则必须调用 n 次 release() 才能释放锁。
    RLock 锁具有可重入性。也就是说,同一个线程可以对已被加锁的 RLock 锁再次加锁,RLock 对象会维持一个计数器来追踪 acquire() 方法的嵌套调用,线程在每次调用 acquire() 加锁后,都必须显式调用 release() 方法来释放锁。所以,一段被锁保护的方法可以调用另一个被相同锁保护的方法。

Lock 是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程在开始访问共享资源之前应先请求获得 Lock 对象。当对共享资源访问完成后,程序释放对 Lock 对象的锁定。
线程安全的类具有如下特征:

  • 该类的对象可以被多个线程安全地访问。
  • 每个线程在调用该对象的任意方法之后,都将得到正确的结果。
  • 每个线程在调用该对象的任意方法之后,该对象都依然保持合理的状态。
    7)信号量
    互斥锁同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。
import threading
import time

def run(n, semaphore):
    semaphore.acquire()   #加锁
    time.sleep(1)
    print("run the thread:%s\n" % n)
    semaphore.release()     #释放

if __name__ == '__main__':
    num = 0
    semaphore = threading.BoundedSemaphore(5)  # 最多允许5个线程同时运行
    for i in range(22):
        t = threading.Thread(target=run, args=("t-%s" % i, semaphore))
        t.start()
    while threading.active_count() != 1:
        pass  # print threading.active_count()
    else:
        print('-----all threads done-----')

8)事件

python线程的事件用于主线程控制其他线程的执行,事件是一个简单的线程同步对象,其主要提供以下几个方法:

  • clear 将flag设置为“False”
  • set 将flag设置为“True”
  • is_set 判断是否设置了flag
  • wait 会一直监听flag,如果没有检测到flag就一直处于阻塞状态
    事件处理的机制:全局定义了一个“Flag”,当flag值为“False”,那么event.wait()就会阻塞,当flag值为“True”,那么event.wait()便不再阻塞。
    #利用Event类模拟红绿灯
import threading
import time

event = threading.Event()


def lighter():
    count = 0
    event.set()     #初始值为绿灯
    while True:
        if 5 < count <=10 :
            event.clear()  # 红灯,清除标志位
            print("\33[41;1mred light is on...\033[0m")
        elif count > 10:
            event.set()  # 绿灯,设置标志位
            count = 0
        else:
            print("\33[42;1mgreen light is on...\033[0m")

        time.sleep(1)
        count += 1

def car(name):
    while True:
        if event.is_set():      #判断是否设置了标志位
            print("[%s] running..."%name)
            time.sleep(1)
        else:
            print("[%s] sees red light,waiting..."%name)
            event.wait()
            print("[%s] green light is on,start going..."%name)

light = threading.Thread(target=lighter,)
light.start()

car = threading.Thread(target=car,args=("MINI",))
car.start()

9)全局解释器锁

 在非python环境中,单核情况下,同时只能有一个任务执行。多核时可以支持多个线程同时执行。但是在python中,无论有多少核,同时只能执行一个线程。究其原因,这就是由于GIL的存在导致的。

GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。GIL只在cpython中才有,因为cpython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在pypy和jpython中是没有GIL的。
Python多线程的工作过程:
python在使用多线程的时候,调用的是c语言的原生线程。

  • 拿到公共数据
  • 申请gil
  • python解释器调用os原生线程
  • os操作cpu执行运算
  • 当该线程执行时间到后,无论运算是否已经执行完,gil都被要求释放
  • 进而由其他进程重复上面的过程
  • 等其他进程执行完后,又会切换到之前的线程(从他记录的上下文继续执行),整个过程是每个线程执行自己的运算,当执行时间到就进行切换(context switch)。
    python针对不同类型的代码执行效率也是不同的:
    1、CPU密集型代码(各种循环处理、计算等等),在这种情况下,由于计算工作多,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好。
    2、IO密集型代码(文件处理、网络爬虫等涉及文件读写的操作),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。
    使用建议?
    python下想要充分利用多核CPU,就用多进程。因为每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。

10)多线程

if __name__ == '__main__':
    provinces = ["北京市", "天津市", "上海市", "重庆市", "河北省""河南省", "云南省", "辽宁省", "黑龙江省", "湖南省",
"广西壮族自治区", "广东省", "海南省"]
    arr = ["原数据"]
    po = Pool(processes=5)  # 允许开几个进程
    for province in provinces:
        print("Add task:", province)
        # 开启进程运行workMulti函数,传入参数province,arr。
        # 注意最后一个逗号是必须的,不是多余的
        po.apply_async(workMulti, args=(province, arr,))
    print("AAA****************************")
    po.close()  # 关闭进程池入口,此后不能再向进程池中添加任务了
    print("BBB****************************")
    po.join()  # 阻塞等待,只有进程池中所有任务都完成了才往下执行
    print("CCC****************************")
    
  **********************输出******************************
Add task: 北京市
Add task: 天津市
Add task: 上海市
Add task: 重庆市
Add task: 河北省
Add task: 河南省
Add task: 云南省
Add task: 辽宁省
Add task: 黑龙江省
Add task: 湖南省
Add task: 广西壮族自治区
Add task: 广东省
Add task: 海南省
AAA****************************
BBB****************************
['原数据']
2022-07-15 11:14:14 finish.... 北京市
['原数据']
2022-07-15 11:14:14 finish.... 天津市
['原数据']
2022-07-15 11:14:14 finish.... 上海市
['原数据']
2022-07-15 11:14:14 finish.... 重庆市
['原数据']
2022-07-15 11:14:14 finish.... 河北省
['原数据']
2022-07-15 11:14:24 finish.... 河南省
['原数据']
2022-07-15 11:14:24 finish.... 云南省
['原数据']
2022-07-15 11:14:24 finish.... 辽宁省
['原数据']
2022-07-15 11:14:24 finish.... 黑龙江省
['原数据']
2022-07-15 11:14:24 finish.... 湖南省
['原数据']
2022-07-15 11:14:34 finish.... 广西壮族自治区
['原数据']
2022-07-15 11:14:34 finish.... 广东省
['原数据']
2022-07-15 11:14:34 finish.... 海南省
CCC****************************

11)注意事项:

1、关于进程的开启代码一定要放在if name == ‘main’:代码之下,不能放到函数中或其他地方。
2、po.apply_async(workMulti, args=(province,arr,))开启进程调用workMulti,需要几个参数传几个,最后需要加一个逗号,因为其传递的参数是tuple类型。
3、进程之间的参数变量是不共享的, 在某个进程中修改其函数参数, 在其他进程中是不可见的。这里每次打印的都是[‘原数据’]足以说明。
4、进程池接受任务并非阻塞式。这里进程池虽然只开5个,但它可以一次性接受很多任务, 任务的执行由进程池Pool自行安排,这里打印的Add task是连续的,并不需要等待进程池有空进程。
5、这里为何要调用workMulti而不直接调用work?
答:假如work函数中报错,你会发现程序看起来运行正常,你将发现不了错误。通过在workMulti中加入try:…except:…以及traceback.print_exc(),我们可以打印出进程运行的异常。
6、多进程如何接收计算结果?
答:方法1:使用callback回调方法,我没使用过,不详细说明
方法2:在进程中把计算结果保存下来,如保存到数据库或文件,所有进程计算结束后再提取。我多采用这种方法,这种方法在部分进程计算失败后仍然能保留计算成功的那些结果。
7、开启多少个进程合适?
答:看你跑的是什么类型的任务。如果是计算密集型(耗CPU的),建议开启和CPU核心线程数一样的进程,如果同时你还要操作计算机或进行其他任务,那最好再留出一点CPU,不然将会卡死。如果是耗时型(不耗CPU,如网络请求等),可以考虑开多一些进程,不需要考虑CPU核心线程数。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

如鸿毛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值