Python多任务——线程

多任务概念

什么叫 多任务 呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听音乐,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。

现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?

答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

真正的并行执行多任务只能在 多核CPU 上实现,但是,由于任务数量远远多于 CPU 的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

线程 thread :是 操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

  • 并发性:是指两个或多个事件在同一时间间隔内发生,。
  • 并行性:是指两个或多个事件在同一时刻发生。

单线程执行任务

程序模拟 边唱歌,边玩游戏

  1. """
  2. Python多线程的使用
  3. """
  4. import time
  5. def sing(name):
  6. for i in range(3):
  7. print("%s正在唱歌...%d" % (name, i))
  8. time.sleep(1)
  9. def play(name):
  10. for i in range(3):
  11. print("%s正在玩游戏...%d" % (name, i))
  12. time.sleep(1)
  13. def single_thread_demo():
  14. """
  15. 单线程案例
  16. """
  17. name = 'hui'
  18. sing(name)
  19. play(name)
  20. def main():
  21. single_thread_demo()
  22. if __name__ == '__main__':
  23. main()

运行结果如下:

  1. hui正在唱歌...0
  2. hui正在唱歌...1
  3. hui正在唱歌...2
  4. hui正在玩游戏...0
  5. hui正在玩游戏...1
  6. hui正在玩游戏...2
  7. [Finished in 6.2s]

很显然程序并没有完成唱歌和玩游戏同时进行的要求,但生活中却很多这样的场景,例如边唱歌、边跳舞,唱跳一起进行。在Python中想实现多任务同时进行,可以使用 多线程、多进程、协程 等技术

Python多线程的实现

常用模块

Python 线程中常用的两个模块为:

  • _thread
  • threading(推荐使用)

Python的 _thread 模块是比较底层的模块,Python的 threading 模块是对 _thread 做了一些封装的,可以更加方便的被使用。

使用方式

Python中使用线程有以下方式:

  • 函数式 _thread.start_new_thread()
  • 线程对象 threading.Thread()
  • 自定义类继承 threading.Thread

_thread模块(函数式)

  1. _thread.start_new_thread ( function, args[, kwargs] )

参数说明:

  • function 线程函数。
  • args 传递给线程函数的参数,他必须是个tuple类型。
  • kwargs 可选参数。
  1. """
  2. Python多线程的使用
  3. """
  4. import time
  5. import _thread
  6. def sing(name):
  7. for i in range(3):
  8. print("%s正在唱歌...%d" % (name, i))
  9. time.sleep(1)
  10. def play(name):
  11. for i in range(3):
  12. print("%s正在玩游戏...%d" % (name, i))
  13. time.sleep(1)
  14. def single_thread_demo():
  15. """
  16. 单线程案例
  17. """
  18. name = 'hui'
  19. sing(name)
  20. play(name)
  21. def fun_multithread():
  22. """函数式使用多线程"""
  23. name = 'hui'
  24. # 启动唱歌、玩游戏线程
  25. sing_thread = _thread.start_new_thread(sing, (name, ))
  26. play_thread = _thread.start_new_thread(play, (name, ))
  27. def main():
  28. # single_thread_demo()
  29. fun_multithread()
  30. # 防止主线程先执行完,导致子线程没有执行
  31. time.sleep(3)
  32. if __name__ == '__main__':
  33. main()

运行结果如下

  1. 第一种运行结果
  2. hui正在唱歌...0hui正在玩游戏...0
  3. hui正在唱歌...1hui正在玩游戏...1
  4. hui正在玩游戏...2hui正在唱歌...2
  5. [Finished in 3.1s]
  6. 第二种运行结果
  7. hui正在唱歌...0
  8. hui正在玩游戏...0
  9. hui正在唱歌...1
  10. hui正在玩游戏...1
  11. hui正在唱歌...2
  12. hui正在玩游戏...2
  13. [Finished in 3.1s]
  14. 第......

与单线程对比

  1. hui正在唱歌...0
  2. hui正在唱歌...1
  3. hui正在唱歌...2
  4. hui正在玩游戏...0
  5. hui正在玩游戏...1
  6. hui正在玩游戏...2
  7. [Finished in 6.2s]

很明显使用多线程并发的操作,耗时更短,也完成了 边唱歌边玩游戏 的模拟。

注意: 由于操作系统分给每个线程的时间片不一样,调度的先后顺序也不同,所以会有很多不一样的运行结果。

threading模块 (线程对象)

  1. """
  2. Python多线程的使用
  3. """
  4. import time
  5. import _thread
  6. import threading
  7. def sing(name):
  8. for i in range(3):
  9. print("%s正在唱歌...%d" % (name, i))
  10. time.sleep(1)
  11. def play(name):
  12. for i in range(3):
  13. print("%s正在玩游戏...%d" % (name, i))
  14. time.sleep(1)
  15. def thread_obj_demo():
  16. """
  17. 使用线程对象创建多线程
  18. """
  19. name = 'hui'
  20. # 创建线程
  21. sing_thread = threading.Thread(target=sing, args=(name,))
  22. play_thread = threading.Thread(target=play, args=(name,))
  23. # 启动线程
  24. sing_thread.start()
  25. play_thread.start()
  26. def main():
  27. thread_obj_demo()
  28. # 防止主线程先执行完,导致子线程没有执行
  29. time.sleep(3)
  30. if __name__ == '__main__':
  31. main()

运行结果如下

  1. hui正在唱歌...0
  2. hui正在玩游戏...0
  3. hui正在玩游戏...1
  4. hui正在唱歌...1
  5. hui正在唱歌...2hui正在玩游戏...2
  6. [Finished in 3.1s]

threading模块常用方法
  • threading.currentThread(): 返回当前的线程变量。
  • threading.enumerate(): 返回一个包含正在运行的线程的 list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  • threading.activeCount(): 返回正在运行的线程数量,与 len(threading.enumerate()) 有相同的结果。
  1. """
  2. python threading模块的常用方法
  3. """
  4. import time
  5. import threading
  6. def test1():
  7. print('------test1-------')
  8. time.sleep(3)
  9. def test2():
  10. print('------test2-------')
  11. time.sleep(3)
  12. def main():
  13. t1 = threading.Thread(target=test1)
  14. t2 = threading.Thread(target=test2)
  15. t1.start()
  16. t2.start()
  17. print('activeCount: %d' % threading.activeCount())
  18. print(threading.enumerate())
  19. while threading.activeCount() != 1:
  20. time.sleep(1)
  21. print(threading.enumerate())
  22. print(threading.enumerate())
  23. if __name__ == '__main__':
  24. main()

运行结果

  1. ------test1-------
  2. ------test2-------
  3. activeCount: 3
  4. [<_MainThread(MainThread, started 8680)>, <Thread(Thread-1, started 4256)>, <Thread(Thread-2, started 7932)>]
  5. [<_MainThread(MainThread, started 8680)>, <Thread(Thread-1, started 4256)>, <Thread(Thread-2, started 7932)>]
  6. [<_MainThread(MainThread, started 8680)>, <Thread(Thread-1, started 4256)>, <Thread(Thread-2, started 7932)>]
  7. [<_MainThread(MainThread, started 8680)>]
  8. [<_MainThread(MainThread, started 8680)>]

threading.Thread类构造

当线程对象一但被创建,其活动一定会因调用线程的 start() 方法开始。这会在独立的控制线程调用 run() 方法。

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, _, _daemon=None*)

调用这个构造函数时,必需带有关键字参数。参数如下:

  • group 应该为 None;为了日后扩展 ThreadGroup 类实现而保留。
  • target 是用于 run() 方法调用的可调用对象。默认是 None,表示不需要调用任何方法。
  • name 是线程名称。默认情况下,由 “Thread-N“ 格式构成一个唯一的名称,其中 N 是小的十进制数。
  • args 是用于调用目标函数的参数元组。默认是 ()
  • kwargs 是用于调用目标函数的关键字参数字典。默认是 {}

如果不是 Nonedaemon 参数将显式地设置该线程是否为守护模式。 如果是 None (默认值),线程将继承当前线程的守护模式属性。

如果子类型重载了构造函数,它一定要确保在做任何事前,先发起调用基类构造器(Thread.__init__())。

threading.Thread类方法及属性
  • start()
    开始线程活动。它在一个线程里最多只能被调用一次。它安排对象的 run() 方法在一个独立的控制进程中调用。如果同一个线程对象中调用这个方法的次数大于一次,会抛出 RuntimeError
  • run()
    代表线程活动的方法。你可以在子类型里重载这个方法。 标准的 run() 方法会对作为 target 参数传递给该对象构造器的可调用对象(如果存在)发起调用,并附带从 argskwargs 参数分别获取的位置和关键字参数。
  • join(timeout=None)
    等待,直到线程终结。这会阻塞调用这个方法的线程,直到被调用 join() 的线程终结 — 不管是正常终结还是抛出未处理异常 — 或者直到发生超时,超时选项是可选的。当 timeout 参数存在而且不是 None 时,它应该是一个用于指定操作超时的以秒为单位的浮点数(或者分数)。因为 join() 总是返回 None ,所以你一定要在 join() 后调用 is_alive() 才能判断是否发生超时 — 如果线程仍然存活,则 join() 超时。当 timeout 参数不存在或者是 None ,这个操作会阻塞直到线程终结。一个线程可以被 join() 很多次。如果尝试加入当前线程会导致死锁, join() 会引起 RuntimeError 异常。如果尝试 join() 一个尚未开始的线程,也会抛出相同的异常。
  • name
    只用于识别的字符串。它没有语义。多个线程可以赋予相同的名称。 初始名称由构造函数设置。
  • getName()
  • setName()
    旧的 name 取值/设值 API;直接当做特征属性使用它。
  • ident
    这个线程的 ‘线程标识符’,如果线程尚未开始则为 None 。这是个非零整数。参见 get_ident() 函数。当一个线程退出而另外一个线程被创建,线程标识符会被复用。即使线程退出后,仍可得到标识符。
  • is_alive()
    返回线程是否存活。当 run() 方法刚开始直到 run() 方法刚结束,这个方法返回 True 。模块函数 enumerate() 返回包含所有存活线程的列表。
  • daemon
    一个表示这个线程是(True)否(False)守护线程的布尔值。一定要在调用 start() 前设置好,不然会抛出 RuntimeError 。初始值继承于创建线程;主线程不是守护线程,因此主线程创建的所有线程默认都是 daemon = False。当没有存活的非守护线程时,整个Python程序才会退出。
  • isDaemon()
  • setDaemon()
    旧的 name 取值/设值 API;建议直接当做特征属性使用它。

自定义类继承 threading.Thread

通过使用 threading 模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用 threading 模块时,往往会定义一个新的子类 class,只要继承 threading.Thread 就可以了,然后重写 run 方法

  1. """
  2. Python多线程的使用
  3. """
  4. import time
  5. import threading
  6. class MyThread(threading.Thread):
  7. # def __init__(self):
  8. # super().__init__()
  9. def run(self):
  10. for i in range(3):
  11. time.sleep(1)
  12. msg = "I'm "+self.name+' @ '+str(i) #name属性中保存的是当前线程的名字
  13. print(msg)
  14. def main():
  15. t = MyThread()
  16. t.start()
  17. if __name__ == '__main__':
  18. main()

如果子类型重载了构造函数,它一定要确保在做任何事前,先发起调用基类构造器(Thread.__init__())。

  1. super().__init__() # 需先调用父类构造

运行结果如下:

  1. I'm Thread-1 @ 0
  2. I'm Thread-1 @ 1
  3. I'm Thread-1 @ 2

结论:

pythonthreading.Thread 类有一个 run() 方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过 Thread 类的 start()方法,可以启动该线程,交给 python 虚拟机进行调度,当该线程获得执行的机会时,就会调用 run()方法执行线程

总结

  • 每个线程默认有一个名字,尽管上面的例子中没有指定线程对象的 name,但是 python 会自动为线程指定一个名字。
  • 当线程的 run() 方法结束时该线程完成。
  • 无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。

公众号

新建文件夹X

大自然用数百亿年创造出我们现实世界,而程序员用几百年创造出一个完全不同的虚拟世界。我们用键盘敲出一砖一瓦,用大脑构建一切。人们把1000视为权威,我们反其道行之,捍卫1024的地位。我们不是键盘侠,我们只是平凡世界中不凡的缔造者 。