线程

线程与进程类似,只不过它们是在同一个进程下执行的,并共享相同的上下文。

线程包括开始执行、执行顺序和结束三部分,它有一个指令指针,用于记录当前运行的上下文。它们可以被抢占(中断)、挂起(睡眠),这种做法称为—让步。

一个进程中的各个线程与主线程共享同一片数据空间,因此相对于独立的进程而言,线程的信息共享与通信更加方便。线程一般是以并发的方式执行的,因此这种并发与数据共享的机制使得多任务协作成为可能。

要注意的是在单核的CPU中,真正的并发是不可能实现的。在一个进程中线程的执行是这样规划的:每个线程执行一小会,然后让步给其他线程,有时和其他线程进行结果通信。

共享也是存在风险的,如果两个或多个线程访问同一片数据,由于数据访问顺序不同,可能导致结果不一致。

另一个问题就是,线程无法给予公平的执行时间,这是因为一些函数在完成前保持阻塞状态,如果没有对多线程进行修改,会导致CPU的时间分配向这些贪婪的函数。

在python中使用线程

不使用线程的情况

在接下来,用一个例子来展示不使用线程的情况下,程序执行的情况:

from time import ctime, sleep


def loop0():
    print('loop0 start at: ', ctime())
    sleep(4)
    print('loop0 done at: ', ctime())


def loop1():
    print('loop1 start at: ', ctime())
    sleep(2)
    print('loop1 done at: ', ctime())


def main():
    print('starting at ', ctime())
    loop0()
    loop1()
    print('all done at ', ctime())


if __name__ == '__main__':
    main()

运行结果:

starting at  Mon Jun  7 16:48:51 2021
loop0 start at:  Mon Jun  7 16:48:51 2021
loop0 done at:  Mon Jun  7 16:48:55 2021
loop1 start at:  Mon Jun  7 16:48:55 2021
loop1 done at:  Mon Jun  7 16:48:57 2021
all done at  Mon Jun  7 16:48:57 2021

从程序的运行结果来看,在不使用多线程的情况下,程序是按顺序执行的,函数loop0有4秒的休眠时间,而函数loop1有2秒的运行时间,所以,当程序都运行完毕之后需要6秒的时间。

threading

python提供了多个模块来管理与创建线程,其中threading模块便是其中一个,其中threading模块下的Thread对象表示一个执行线程的对象。

下面将使用一个例子来展示使用线程执行程序的情况:

from time import sleep, ctime
import threading


def loop0():
    print('loop0 start at: ', ctime())
    sleep(4)
    print('loop0 done at ', ctime())


def loop1():
    print('loop1 start at: ', ctime())
    sleep(2)
    print('loop1 done at ', ctime())


def main():
    print('start at ', ctime())
    t1 = threading.Thread(target=loop0)	# 创建线程
    t2 = threading.Thread(target=loop1)
    t1.start()	# 启动线程
    t2.start()
    print('All done at ', ctime())


if __name__ == '__main__':
    main()

运行结果:

start at  Mon Jun  7 17:28:51 2021
loop0 start at:  Mon Jun  7 17:28:51 2021
loop1 start at: All done at   Mon Jun  7 17:28:51 2021Mon Jun  7 17:28:51 2021

loop1 done at  Mon Jun  7 17:28:53 2021
loop0 done at  Mon Jun  7 17:28:55 2021

从运行结果中可以看出,程序不再是按顺序执行的,当我执行了t1与t2这两个线程的时候,后面的代码也是会继续往下执行的,因此在上面会看到All done at...已经执行执行完毕。

看到这样的运行结果,也验证了上面所描述的线程执行的规划。

需要注意一点:t1.start()表示可以启动线程,但是该线程什么时候执行,还是要取决于CPU。

重写Thread的run方法

import threading
from time import ctime, sleep


class MyThread(threading.Thread):
    def __init__(self, thread_name):
        super(MyThread, self).__init__(name=thread_name)
        self.thread_name = thread_name

    def run(self):
        print('%s start at ' % self.thread_name, ctime())
        sleep(4)
        print('%s end at ' % self.thread_name, ctime())


def main():
    print('Start at ', ctime())
    t1 = MyThread('loop0')
    t2 = MyThread('loop1')
    t1.start()
    t2.start()
    print('All done at ', ctime())


if __name__ == '__main__':
    main()

在上面的代码中,通过重写Thread对象的run()方法,实现多线程。

运行结果如下所示:

Start at  Mon Jun  7 19:09:34 2021
loop0 start at  Mon Jun  7 19:09:34 2021
loop1 start at  Mon Jun  7 19:09:34 2021
All done at  Mon Jun  7 19:09:34 2021
loop1 end at  loop0 end at  Mon Jun  7 19:09:38 2021
Mon Jun  7 19:09:38 2021

如果是按顺序运行的话,程序的运行时间至少是8秒,但是由于多线程可以并发或并行执行,因此只用了4秒便执行完毕,大大加快了程序运行的效率。

上面的程序都是直接或间接的使用了threading.Thread类。

接下来,可以合并上面的两种创建线程的方式。

import threading
from time import sleep, ctime


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

    def run(self):
        super(MyThread, self).run()


def loop0(arg):
    print('%s start at ' % arg, ctime())
    sleep(4)
    print('%s end at ' % arg, ctime())


def loop1(arg):
    print('%s start at ' % arg, ctime())
    sleep(2)
    print('%s end at ' % arg, ctime())


def main():
    print('start at ', ctime())
    t1 = MyThread('loop0', loop0)
    t2 = MyThread('loop1', loop1)
    t1.start()
    t2.start()
    print('All done at', ctime())



if __name__ == '__main__':
    main()

运行结果如下所示:

start at  Mon Jun  7 19:37:46 2021
loop3 start at  Mon Jun  7 19:37:46 2021
loop1 start at  Mon Jun  7 19:37:46 2021All done at Mon Jun  7 19:37:46 2021

loop1 end at  Mon Jun  7 19:37:48 2021
loop3 end at  Mon Jun  7 19:37:50 2021

同样的,因为线程的并发与并行的特性,使得两个线程可以同时执行,加快了程序运行的速度。

threading.Thread

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():创建线程之后,通过start()方法启动线程,等待CPU调度,为run函数执行做好准备。

run():线程开始执行的入口函数,函数体中会调用用户编写的target()函数。

join():阻塞挂起调用该函数的线程,直到被调用的线程执行完成或超时。通常会在主线程中调用该方法,等待其他线程执行完成。

多线程执行

在主线程中创建若干线程之后,它们之间没有任何协作和同步,除了主线程之外每个线程都是从run开始被执行。

创建线程阻塞

我们可以通过join方法让主线程阻塞,等待其创建的线程执行完成。

import threading
import time


def loop0():
    print('loop0 start at ', time.ctime())
    time.sleep(4)
    print('loop0 end at ', time.ctime())


def loop1():
    print('loop1 start at ', time.ctime())
    time.sleep(4)
    print('loop1 end at ', time.ctime())


def main():
    print('start at ', time.ctime())
    t1 = threading.Thread(target=loop0)
    t2 = threading.Thread(target=loop1)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('All done', time.ctime())


if __name__ == '__main__':
    main()

运行结果:

start at  Mon Jun  7 20:37:08 2021
loop0 start at  Mon Jun  7 20:37:08 2021
loop1 start at  Mon Jun  7 20:37:08 2021
loop0 end at  Mon Jun  7 20:37:12 2021
loop1 end at  Mon Jun  7 20:37:12 2021
All done Mon Jun  7 20:37:12 2021

守护线程与非守护线程

在创建新线程时,主线程会从其父线程继承其线程属性,主线程是普通的非守护线程,默认情况下它所创建的任何线程都是非守护线程。

无法退出的主线程

我们经常需要通过创建线程来执行某项例行任务,或者是提供某种服务。最常见的垃圾收集器,垃圾收集器是在后台运行并回收程序不再使用的垃圾内存。

在默认情况下,新线程通常会生成非守护线程或普通线程,如果新线程在运行,主线程将永远在等待,程序无法退出。

接下来,将用一个例子简单说明主线程无法退出的情况:

import threading
import time


def kitchen_cleaner():
    while True:
        print("Olivia cleaned the kitchen.")
        time.sleep(1)


if __name__ == '__main__':
    olivia = threading.Thread(target=kitchen_cleaner)
    olivia.start()

    print('Barron is cooking...')
    time.sleep(0.6)
    print('Barron is cooking...')
    time.sleep(0.6)
    print('Barron is cooking...')
    time.sleep(0.6)
    print('Barron is done!')

运行结果:

Olivia cleaned the kitchen.
Barron is cooking...
Barron is cooking...
Olivia cleaned the kitchen.
Barron is cooking...
Barron is done!
Olivia cleaned the kitchen.
Olivia cleaned the kitchen.
Olivia cleaned the kitchen.

守护线程

在上面的代码中定义了kitchen_cleaner的函数,它代表一个垃圾收集器这样周期性的在后台清理垃圾。

对于kitchen_cleaner函数来说,每秒执行一次。下面,我将olivia设置为守护线程。

import threading
import time


def kitchen_cleaner():
    while True:
        print("Olivia cleaned the kitchen.")
        time.sleep(1)


if __name__ == '__main__':
    olivia = threading.Thread(target=kitchen_cleaner)
    olivia.daemon = True
    olivia.start()

    print('Barron is cooking...')
    time.sleep(0.6)
    print('Barron is cooking...')
    time.sleep(0.6)
    print('Barron is cooking...')
    time.sleep(0.6)
    print('Barron is done!')

其实,只需要添加上olivia.daemon = True这行代码即可。

运行结果:

Olivia cleaned the kitchen.
Barron is cooking...
Barron is cooking...
Olivia cleaned the kitchen.
Barron is cooking...
Barron is done!

当该程序再次运行时,主程序执行完毕,并且没有其他新线程执行的情况下,程序将会退出。

需要注意的是:

  • 设置守护线程必须在线程开启之前,否则会出现运行时错误。
  • 守护线程不会像其他普通线程一样正常的退出,当程序中所有的非守护线程都执行完毕时,任何剩余的守护线程将在python退出时被丢弃,因此在设计守护线程时要确保主程序退出时对守护线程不会产生任何影响。
Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐