测试工具开发 - 多线程编程技术

一、多线程的概述

1. 进程与线程

  • 进程 :在系统中正在运行的一个 应用程序 ,程序一旦运行就是进程。
    • 进程 —- 资源分配的最小单位
  • 线程 : 系统分配处理时间资源的基本单元,或者说进程之内独立执行的一个单元试行流。
    • 线程 —- 程序执行的最小单位。
  • 进程 中最少有一个线程。

2.进程的组成

  • 内存 : 每个进程的内存是相互独立的。
  • 文件/网络句柄 : 它们是所有的进程所共有的。
  • 线程

    ::: tip
    所有的 线程 共享 该进程的所有资源。
    :::

3. 进程与线程—-汇总

  1. 进程有分配已大部分的内存,而线程只需要分配一部分栈就可以了。
  2. 一个程序至少有一个进程,一个进程至少有一个线程。
  3. 进程是资源分配的最小单位,线程是程序执行的最小单位。
  4. 一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

4.多线程

  1. 使用线程可以把 占据长时间 的程序中的任务放到后台去处理。
  2. 用户界面可以更加吸引人。
  3. 程序的运行速度可能加快。
  4. 在一些 等待的任务 的实现上,线程就比较有用。

5.多线程的执行方式

  • 串行 :依次执行所有的线程,上一个线程没有执行完之前,下一个线程不会执行。
  • 并行 : 所有线程同时执行。
  • 并发 : 所有线程依次 交替 执行。

二、Threading 模块

1. Threading 模块 常用方法

  • Thread() : 创建线程,target 为需要执行的函数,args 为执行函数的实参(参数为元组)
  • run() : 用以表示线程活动的方法。
  • start() : 启动线程活动。
  • join([time]) : 等待至线程中止。这阻塞调用线程直至线程的 join() 方法被调用中止 - 正常退出或者抛出未处理的异常 - 或者是可选的超时发生。
  • isAlive() : 返回线程是否活动的。
  • gitName() : 返回线程名。
  • setName() : 设置线程名。
  • threading.currentThread() : 返回当前的线程变量。
  • threading.enumerate() : 返回一个包含正在运行的线程的 list正在运行 指线程启动后、结束前,不包含 启动前和终止后的线程。
  • threading.activeCount() : 返回正在运行的线程数量,与 len(threading.enumerate()) 有相同的结果。

2.多线程案例

普通方法:

1
2
3
4
5
6
7
8
9
10
11
12
import time

def doing(something):
time.sleep(2)
print("正在做 >>> :", something)

start_time = time.time()
doing("在学习")
doing("在加班")
end_time = time.time()

print("使用时间:", end_time-start_time)

多线程方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import threading
import time

def doing(something):
print("正在做 >>> :", something)
time.sleep(2)

start_time = time.time()
# 1.创建线程
"""
target : 需要执行的函数名
args : 执行函数的实参(参数为元组)一个参数时需要加 逗号“,”
"""
a = threading.Thread(target=doing, args=("在学习",))
b = threading.Thread(target=doing, args=("在加班",))

# 2.启动子线程
a.start()
b.start()

# 3.阻塞主线程,直到子线程全部结束
a.join()
b.join()
end_time = time.time()

print("使用时间:", end_time-start_time)

三、 多线程技术

1. 全局解释器锁(GIL)

CPython 中,无论 CPU 有多少核,同时只能执行 一个线程,这是用于 GIL 的存在导致的。

可以把 GIL 看作是执行任务的 “通行证”,并且在一个 Python 进程中, GIL 只有一个

示例:

某些情况下,多个线程同时操作对一个变量时,可能会导致数据混乱,所以需要 加锁 ,来防止变量依次更改 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import threading
import time

money = 1000
lock = threading.Lock()


def test(num):
global money
lock.acquire() # 加锁

userMoney = money
userMoney += num
time.sleep(2)
money = userMoney

lock.release() # 解锁


if __name__ == '__main__':
# 创建子线程
t1 = threading.Thread(target=test, args=(500,))
t2 = threading.Thread(target=test, args=(-200,))

# 启动子线程
t1.start()
t2.start()

# 阻塞
t1.join()
t2.join()

print(f"余额为:{money}")

2.守护进程

在进程想要退出进程的时候,不需要等待自己运行结束,直接退出 即可,

1
t1.setDaemon(True)

示例:

不论子线程是否执行结束,主线程执行完后直接结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import threading
import time


def doing(something):
print("正在做 >>> :", something)
time.sleep(2)
print(something, "结束了")


if __name__ == '__main__':
start_time = time.time()

a = threading.Thread(target=doing, args=("在学习",))
b = threading.Thread(target=doing, args=("在加班",))

a.setDaemon(True) # 守护线程
b.setDaemon(True)

# 2.启动子线程
a.start()
b.start()

for one in range(2):
print("主线程在运行:", one)

end_time = time.time()
print("使用时间:", end_time-start_time)

::: tip

  1. 如果只有 t1.start(),没有 t1.join()t1.setDaemon(True)
    • 所有线程全部运行完成,这用会出现 主线程结束后,子线程继续运行 , 直到结束。
  2. 如果只有 t1.start()t1.join(),没有 t1.setDaemon(True)
    • 主线程结束前会等待所有的子线程运行完成,在结束。
  3. 如果有 t1.start()t1.setDaemon(True) ,没有 t1.join()
    • 主线程完成, 所有子线程都 中断
      :::

3. 死锁

在线程间 共享多个资源 的时候,如果两个线程分别占有一部分资源并且 同时等待 对方的资源,就会造成 死锁

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import threading
import time

lockA = threading.Lock()
lockB = threading.Lock()


# 面试官
def fool():
lockA.acquire() # 上锁
print("请解释什么是死锁")
time.sleep(1)

lockB.acquire() # 上锁
print("发 offer")
time.sleep(1)

lockA.release() # 解锁
lockB.release()


# 面试者
def user():
lockB.acquire()
print("请给我发 offer")
time.sleep(1)

lockA.acquire()
print("解释了什么是死锁")
time.sleep(1)

lockA.release()
lockB.release()


t1 = threading.Thread(target=fool)
t2 = threading.Thread(target=user)

t1.start()
t2.start()

4.递归锁

为了支持在同一线程中多次请求同一资源, Python 提供了 “可重入锁” : threading.RLock

RLock 内部维护着一个 Lock 和一个 counter**变量 counter 记录了 acquire 的次数 , 从而使得资源可以被多次 acquire** 。

::: tip
递归锁内部维护着一把 和一个 计数器
每次 上锁,计数器 ,每次 解锁 ,计数器
计数器可以大于零也可以等于零,但不能 小于零
:::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import threading
import time

lockR = threading.RLock()


def fool():
lockR.acquire()
print("请解释什么是死锁")
time.sleep(1)

lockR.acquire()
print("发 offer")
time.sleep(1)

lockR.release()
lockR.release()


def user():
lockR.acquire()
print("请给我发 offer")
time.sleep(1)

lockR.acquire()
print("解释了什么是死锁")
time.sleep(1)

lockR.release()
lockR.release()


t1 = threading.Thread(target=fool)
t2 = threading.Thread(target=user)

t1.start()
t2.start()