并发编程之多线程操作篇
多线程简单介绍
多线程,或者说多任务,指的是操作系统同时运行多个任务。例如,听歌、洗衣服、看视频可以同时进行,这种就是多任务。
单核CPU执行多任务:操作系统轮流让各个任务交替执行,任务1执行t1时间,切换到任务2,任务2执行t2时间,再切换到任务3,执行t3时间...如此反复执行,表面上看,每个任务交替执行,但是由于CPU的执行速度很快,在人看来就好像所有任务同时执行。
这里需要注意并发和并行的概念:
- 并发,指任务数多于COU核数,通过操作系统的各种任务调度算法,实现用多个任务一起执行(实际上有一些任务没有在执行,但因为切换任务的速度相当快,看上去一起执行)。
- 并行,指的是任务数小于或等于CPU核数,任务是同时执行的。
threading模块介绍
python中提供了thread和threading模块对线程进行操作,其中thread模块是比较底层的模块,threading模块对thread做了一些包装,使用更方便。
threading模块常用方法
| 方法 | 功能 |
|---|---|
| threading.active_count() | 返回当前处于active状态的Thread对象的个数 |
| threading.current_thread() | 返回当前Thread对象 |
| threading.get_ident() | 返回当前线程的线程标识 |
| threading.enumerate() | 返回当前处于active状态的所有Thread对象列表 |
| threading.main_thread() | 返回主线程对象,启动python解释器的线程对象 |
| threading.stack_size() | 返回创建线程时使用的栈的大小,如果指定 size参数,则用来指定后续创建的线程使用的 栈大小,size必须是0(标识使用系统默认值) 或大于32K的正整数 |
注:线程标识是一个非负整数,并无特殊含义,只是用来标识线程,该整数可能会被循环利用,python3.3及以后版本支持该方法。
threading模块提供了Thread、Lock、RLock、Condition、Event、Timer和Semaphore等类支持多线程。
Thread类使用
Thread是threading提供的最重要也是最基本的类,可以通过该类创建线程并控制线程的运行。
使用Thread创建线程,有两种方式:
为构造函数传递一个可调用对象
继承Thread类并在子类中重写__init__()和run()
threading.Thread类常用的方法和属性如下
| 方法 | 功能 |
|---|---|
| start() | 启动线程 |
| run() | 线程代码,用来实现线程的功能和业务逻辑,可以在子类中重写该方法自定义线程的行为 |
| init(self,group=None,target=None,name=None,args=(),kwargs=None,daemon=None) | 构造函数 |
| is_alive() | 判断线程是否存货 |
| getName() | 返回线程名 |
| setName() | 设置线程名 |
| isDaemon() | 判断线程是否为守护线程 |
| setDaemon() | 设置线程是否为守护线程 |
- 常用属性
| 属性名 | 功能 |
|---|---|
| name | 读取或设置线程的名字 |
| ident | 线程标识,用非0数字或None(线程未被启动) |
| daemon | 标识线程是否为守护线程,默认为False |
| join(timeout=None) | 当timeout=None时,会等待至线程结束; |
threading.Thread类的参数介绍:
threading.Thread(group=None,
target=None,
name=None,
args=(),
kwargs={},
*,
daemon=None)
参数 group 通常默认,作为扩展ThreadGroup类保留。
参数 target 用于run()方法调用的可调用对象,默认为None,标识run()不调用任何对象。
参数 name 线程名称,默认是Thread-N格式狗策划给你的唯一名称,N是十进制数。
参数 args 用于调用目标函数的参数元组,默认是()。
参数 kwargs 用于调用目标函数的关键字参数字典,默认为{}。
参数 daemon 设置线程是否为守护模式,默认为None。
开启线程的两种方式
- 开启线程不需要在main下面执行代码,直接书写即可
- 但是我们还是习惯性的将启动命令写在main下面
方式一:使用构造函数传递可调用对象的方法创建线程
示例1 :使用threading模块创建单线程
import threading
import time
def sing():
print("正在唱歌...")
time.sleep(1)
if __name__ == "__main__":
print("======================调用函数======================")
start_time = time.time()
print("开始时间:%s" % time.ctime())
for i in range(10):
sing()
print("结束时间:%s" % time.ctime())
print(f"总共耗时{time.time() - start_time}s")
print("==========threading.Thread.start()开启单线程==========")
print("开始时间:%s" % time.ctime())
start_time = time.time()
for i in range(10):
t = threading.Thread(target=sing)
t.start()
print("结束时间:%s" % time.ctime())
print(f"总共耗时{time.time() - start_time}s")
显示结果,可以看出来使用threading.Thread启动线程时,使用的时间要短很多。
======================调用函数======================
开始时间:Fri Jan 19 16:36:43 2024
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
结束时间:Fri Jan 19 16:36:53 2024
总共耗时10.00361943244934s
==========threading.Thread.start()开启单线程==========
开始时间:Fri Jan 19 16:36:53 2024
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
正在唱歌...
结束时间:Fri Jan 19 16:36:53 2024
总共耗时0.0010197162628173828s
当调用start()时,才开始真正的创建线程,并开始执行。
示例2:使用threading.Thread创建多线程,并查看当前处于active状态的线程的数量。
import threading
from time import ctime, sleep
def sing():
for i in range(3):
print("singing...%d" % i)
sleep(1)
def dance():
for i in range(3):
print("dancing...%d" % i)
sleep(1)
if __name__ == "__main__":
print("---start---:%s" % ctime())
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
while True:
length = len(threading.enumerate())
print("current thread has %d" % length)
if length <= 1:
break
sleep(0.5)
print("---end---:%s" % ctime())
结果:
---start---:Fri Jan 19 16:43:22 2024
singing...0
dancing...0
current thread has 3
current thread has 3
dancing...1
singing...1
current thread has 3
current thread has 3
singing...2dancing...2
current thread has 3
current thread has 3
current thread has 1
---end---:Fri Jan 19 16:43:25 2024
方式二:继承threading.Thread类
在使用threading模块时,也可以定义一个新的类class,继承threading.Thread,然后重写run()。
继承threading.Thread来定义线程类,其本质是重构Thread类中的run方法
threading.Thread类中的run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。创建自己的线程实例后,通过Thread类的start方法,可以启动该线程。
示例1:创建一个继承自threading.Thread类的子类
import threading
import time
class MyThread(threading.Thread):
def run(self):
for i in range(3):
time.sleep(1)
msg = "I'm " + self.name + " | " + str(i) # name保存当前线程的名字
print(msg)
if __name__ == "__main__":
t = MyThread()
t.start()
# I'm Thread-1 | 0
# I'm Thread-1 | 1
# I'm Thread-1 | 2
示例2:观察线程执行的顺序
import threading
import time
class MyThread(threading.Thread):
def run(self):
for i in range(3):
time.sleep(1)
msg = "I'm " + self.name + " | " + str(i) # name保存当前线程的名字
print(msg)
def test():
for i in range(5):
t = MyThread()
t.start()
if __name__ == "__main__":
test()
# I'm Thread-2 | 0
# I'm Thread-1 | 0
# I'm Thread-4 | 0
# I'm Thread-5 | 0
# I'm Thread-3 | 0
# I'm Thread-1 | 1
# I'm Thread-2 | 1
# I'm Thread-3 | 1
# I'm Thread-5 | 1
# I'm Thread-4 | 1
# I'm Thread-1 | 2
# I'm Thread-2 | 2
# I'm Thread-5 | 2
# I'm Thread-3 | 2
# I'm Thread-4 | 2
每次运行时显示的执行顺序是不确定的。当执行到sleep()的时候,当前线程被阻塞,sleep()结束后当前线程进入就绪状态,等待调度。
线程调度会自行选择一个线程执行。
如上代码只能保证每个线程都能运行完整个run函数,但线程的启动顺序、run函数中每次循环的执行顺序都不能确定。
总结
- 每个线程默认有一个名字,没有指定,python会自动为线程指定一个名字
- 当线程的run()方法结束时该线程完成;
- 无法控制线程调度程序,可以通过别的方式影响线程调度方式。
线程对象的 join 方法
from threading import Thread
import time
def task(name):
print(f'the task {name} is beginning')
time.sleep(3)
print(f'the task {name} is ending')
def main():
t = Thread(target=task, args=('xiao',))
t.start()
# 主线程等待子进程结束之后再运行
t.join()
print(f'the task is main task')
if __name__ == '__main__':
main()
# the task xiao is beginning
# the task xiao is ending
# the task is main task
同一个进程下的多个线程之间数据是共享的
from threading import Thread
import time
money = 999
def task():
global money
money = 99
print(f'task中的money:>>>>{money}')
def main():
print(f'子进程之前的money:>>>>{money}')
t = Thread(target=task)
t.start()
print(f'子进程之后的money:>>>>{money}')
if __name__ == '__main__':
main()
# 子进程之前的money:>>>>999
# task中的money:>>>>99
# 子进程之后的money:>>>>99

浙公网安备 33010602011771号