python多线程

1.1 线程

1.1.1 什么是线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一个线程是一个execution context(执行上下文),即一个cpu执行时所需要的一串指令。

1.1.2 线程的工作方式

假设你正在读一本书,没有读完,你想休息一下,但是你想在回来时恢复到当时读的具体进度。有一个方法就是记下页数、行数与字数这三个数值,这些数值就是execution context。如果你的室友在你休息的时候,使用相同的方法读这本书。你和她只需要这三个数字记下来就可以在交替的时间共同阅读这本书了。

线程的工作方式与此类似。CPU会给你一个在同一时间能够做多个运算的幻觉,实际上它在每个运算上只花了极少的时间,本质上CPU同一时刻只干了一件事。它能这样做就是因为它有每个运算的execution context。就像你能够和你朋友共享同一本书一样,多任务也能共享同一块CPU。

1.2 进程

一个程序的执行实例就是一个进程。每一个进程提供执行程序所需的所有资源。(进程本质上是资源的集合)

一个进程有一个虚拟的地址空间、可执行的代码、操作系统的接口、安全的上下文(记录启动该进程的用户和权限等等)、唯一的进程ID、环境变量、优先级类、最小和最大的工作空间(内存空间),还要有至少一个线程。

每一个进程启动时都会最先产生一个线程,即主线程。然后主线程会再创建其他的子线程。

  • 内存页(同一个进程中的所有线程共享同一个内存空间
  • 文件描述符(e.g. open sockets)
  • 安全凭证(e.g.启动该进程的用户ID)

1.3 进程与线程区别

1.同一个进程中的线程共享同一内存空间,但是进程之间是独立的。

2.同一个进程中的所有线程的数据是共享的(进程通讯),进程之间的数据是独立的。

3.对主线程的修改可能会影响其他线程的行为,但是父进程的修改(除了删除以外)不会影响其他子进程。

4.线程是一个上下文的执行指令,而进程则是与运算相关的一簇资源。

5.同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现。

6.创建新的线程很容易,但是创建新的进程需要对父进程做一次复制。

7.一个线程可以操作同一进程的其他线程,但是进程只能操作其子进程。

8.线程启动速度快,进程启动速度慢(但是两者运行速度没有可比性)。

2 多线程

2.1 线程常用方法

方法注释
start()线程准备就绪,等待CPU调度
setName()为线程设置名称
getName()获取线程名称
setDaemon(True)设置为守护线程
join()逐个执行每个线程,执行完毕后继续往下执行
run()线程被cpu调度后自动执行线程对象的run方法,如果想自定义线程类,直接重写run方法就行了
2.1.1 Thread类

1.普通创建方式

import threading
import time

def run(n):
    print("task", n)
    time.sleep(1)
    print('2s')
    time.sleep(1)
    print('1s')
    time.sleep(1)
    print('0s')
    time.sleep(1)

t1 = threading.Thread(target=run, args=("t1",))
t2 = threading.Thread(target=run, args=("t2",))
t1.start()
t2.start()

"""
task t1
task t2
2s
2s
1s
1s
0s
0s
"""

2.继承threading.Thread来自定义线程类

其本质是重构Thread类中的run方法

import threading
import time


class MyThread(threading.Thread):
    def __init__(self, n):
        super(MyThread, self).__init__()  # 重构run函数必须要写
        self.n = n

    def run(self):
        print("task", self.n)
        time.sleep(1)
        print('2s')
        time.sleep(1)
        print('1s')
        time.sleep(1)
        print('0s')
        time.sleep(1)


if __name__ == "__main__":
    t1 = MyThread("t1")
    t2 = MyThread("t2")

    t1.start()
    t2.start()

  

2.1.2 计算子线程执行的时间

注:sleep的时候是不会占用cpu的,在sleep的时候操作系统会把线程暂时挂起。

join()  #等此线程执行完后,再执行其他线程或主线程
threading.current_thread()      #输出当前线程
import threading
import time

def run(n):
    print("task", n,threading.current_thread())    #输出当前的线程
    time.sleep(1)
    print('3s')
    time.sleep(1)
    print('2s')
    time.sleep(1)
    print('1s')

strat_time = time.time()

t_obj = []   #定义列表用于存放子线程实例

for i in range(3):
    t = threading.Thread(target=run, args=("t-%s" % i,))
    t.start()
    t_obj.append(t)
    
"""
由主线程生成的三个子线程
task t-0 <Thread(Thread-1, started 44828)>
task t-1 <Thread(Thread-2, started 42804)>
task t-2 <Thread(Thread-3, started 41384)>
"""

for tmp in t_obj:
    t.join()            #为每个子线程添加join之后,主线程就会等这些子线程执行完之后再执行。

print("cost:", time.time() - strat_time) #主线程

print(threading.current_thread())       #输出当前线程
"""
<_MainThread(MainThread, started 43740)>
"""

  

2.1.3 统计当前活跃的线程数

由于主线程比子线程快很多,当主线程执行active_count()时,其他子线程都还没执行完毕,因此利用主线程统计的活跃的线程数num = sub_num(子线程数量)+1(主线程本身)

import threading
import time

def run(n):
    print("task", n)    
    time.sleep(1)       #此时子线程停1s

for i in range(3):
    t = threading.Thread(target=run, args=("t-%s" % i,))
    t.start()

time.sleep(0.5)     #主线程停0.5秒
print(threading.active_count()) #输出当前活跃的线程数

"""
task t-0
task t-1
task t-2
4
"""

  由于主线程比子线程慢很多,当主线程执行active_count()时,其他子线程都已经执行完毕,因此利用主线程统计的活跃的线程数num = 1(主线程本身)

import threading
import time


def run(n):
    print("task", n)
    time.sleep(0.5)       #此时子线程停0.5s


for i in range(3):
    t = threading.Thread(target=run, args=("t-%s" % i,))
    t.start()

time.sleep(1)     #主线程停1秒
print(threading.active_count()) #输出活跃的线程数
"""
task t-0
task t-1
task t-2
1
"""

  此外我们还能发现在python内部默认会等待最后一个进程执行完后再执行exit(),或者说python内部在此时有一个隐藏的join()。

2.2 守护进程

我们看下面这个例子,这里使用setDaemon(True)把所有的子线程都变成了主线程的守护线程,因此当主进程结束后,子线程也会随之结束。所以当主线程结束后,整个程序就退出了。

import threading
import time

def run(n):
    print("task", n)
    time.sleep(1)       #此时子线程停1s
    print('3')
    time.sleep(1)
    print('2')
    time.sleep(1)
    print('1')

for i in range(3):
    t = threading.Thread(target=run, args=("t-%s" % i,))
    t.setDaemon(True)   #把子进程设置为守护线程,必须在start()之前设置
    t.start()

time.sleep(0.5)     #主线程停0.5秒
print(threading.active_count()) #输出活跃的线程数
"""
task t-0
task t-1
task t-2
4

Process finished with exit code 0
"""