我要悄悄学python之多线程

线程

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

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

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

要注意的是在单核的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退出时被丢弃,因此在设计守护线程时要确保主程序退出时对守护线程不会产生任何影响。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容

  • 一、使用threading模块实现线程的创建 实例1 输出结果: import threading首先导入thre...
    Benedict清水阅读 440评论 0 1
  • 进程的概念:以一个整体的形式暴露给操作系统管理,里面包含各种资源的调用。 对各种资源管理的集合就可以称为进程。线程...
    ivan_cq阅读 37,534评论 0 15
  • 线程 线程就是一堆指令,是CPU调度的最小单位 每个程序的内存都是独立的 线程的存在就是让程序并发运行 一个线程可...
    Luo_Luo阅读 486评论 0 0
  • 1、什么是线程(thread)? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作...
    一个菜鸟coder阅读 531评论 0 0
  • 并发:逻辑上具备同时处理多个任务的能力。并行:物理上在同一时刻执行多个并发任务。 举例:开个QQ,开了一个进程,开...