Python多任务编程(2)线程
学习目标
线程的介绍
在Python中,想要实现多任务除了使用进程,还可以使用线程来完成,线程是实现多任务的另外一种方式。
概念:线程是进程中执行代码的一个分支,每个执行分支(线程)想要工作执行代码需要CPU进行调度,也就是说线程是CPU调度的基本单位,每个进程至少都有一个线程,而这个线程就是我们通常说的主线程。
线程的使用
如果两个函数代码块需要同时执行,那么可以在一个进程内利用主线程开辟子线程分别进行多线程任务执行,
在Python程序中使用多线程使用threading模块,我们先导入threading模块。
然后使用threading模块下的Thread类创建一个实例对象。
Thread([group [,target [,name [,args [,kwargs]]]]])
- group: 指定线程组,目前只能使用None
- target:执行的目标任务名(一个函数或者一个方法)
- name:线程名字,一般不用设置,默认为Thread-N
- args:以元组方式给执行任务传参
- kwargs:以字典方式给执行任务传参
启动线程同样使用start方法,使用案例如下。
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
|
import threading
import time
def sing():
for i in range(3):
print('执行唱歌中')
time.sleep(0.1)
current_thread = threading.current_thread()
print('唱歌',current_thread)
def dance():
for i in range(3):
print('执行跳舞中')
time.sleep(0.1)
current_thread = threading.current_thread()
print('跳舞:',current_thread)
if __name__ == '__main__':
thread_dance = threading.Thread(target=dance)
thread_dance.start()
sing()
"""
执行跳舞中
执行唱歌中
执行跳舞中
执行唱歌中
执行跳舞中
执行唱歌中
跳舞: <Thread(Thread-1, started 123145434329088)>
唱歌 <_MainThread(MainThread, started 4438251008)>
"""
|
执行带有参数的任务
- args:使用元组方式进行传参数。
- kwargs:使用字典方式进行传参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import threading
def add_two(n,m):
print(n+m)
def add_three(i,j,k):
print(i+j+k)
if __name__ == '__main__':
thread1 = threading.Thread(target=add_two,args=(2,3))
thread2 = threading.Thread(target=add_three,kwargs={'i':1,'j':2,"k":3})
thread1.start()
thread2.start()
# 5
# 6
|
⚠️注意:
-
线程之间的执行是无序的;
-
主线程等待所有的子进程执行结束后结束;
-
线程之间共享全局变量;
-
线程之间共享全局变量数据可能会出现问题。
验证无序这一点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import threading
import time
def task():
time.sleep(1)
print(threading.current_thread())
if __name__ == '__main__':
for i in range(10):
threads = threading.Thread(target=task)
threads.start()
"""
<Thread(Thread-3, started 123145377505280)>
<Thread(Thread-7, started 123145444663296)>
<Thread(Thread-8, started 123145461452800)>
<Thread(Thread-2, started 123145360715776)>
<Thread(Thread-6, started 123145427873792)>
<Thread(Thread-10, started 123145495031808)>
<Thread(Thread-9, started 123145478242304)>
<Thread(Thread-1, started 123145343926272)>
<Thread(Thread-4, started 123145394294784)>
<Thread(Thread-5, started 123145411084288)>
CPU来调度线程,线程的顺序也是无序的;进程的顺序无序的,它是由操作系统调度进程,
"""
|
验证等待子线程结束主线程才结束这一点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import threading
import time
def zixiancheng():
for i in range(5):
print('子线程正在执行')
time.sleep(0.1)
if __name__ == '__main__':
child_thread = threading.Thread(target=zixiancheng)
child_thread.start()
time.sleep(0.2)
print('主线程执行完成')
"""
子线程正在执行
子线程正在执行
主线程执行完成
子线程正在执行
子线程正在执行
子线程正在执行
"""
|
设置守护主线程可以让主线程结束后所有子线程跟着销毁,也可以使用setDaemon(True)为守护进程。
1
2
3
4
5
6
7
8
9
10
11
12
|
if __name__ == '__main__':
child_thread = threading.Thread(target=zixiancheng)
child_thread.daemon = True
# child_thread.setDaemon(True)
child_thread.start()
time.sleep(0.2)
print('主线程执行完成')
"""
子线程正在执行
子线程正在执行
主线程执行完成
"""
|
验证线程之间共享全局变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import threading
list = []
def add_list():
for i in range(3):
list.append(i)
def read_list():
print(list)
if __name__ == '__main__':
thread1 = threading.Thread(target=add_list)
thread2 = threading.Thread(target=read_list)
thread1.start()
thread1.join()
thread2.start()
"""
[0, 1, 2]
"""
|
以上代码的运行逻辑是从主进程开辟的主线程创建一个子线程,因为多线程在同一个进程内,所以多线程可以共享全局变量,也就是说线程的开辟不是使用的是拷贝思维。
共享全局变量时数据会出现问题的验证如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import threading
glo_num = 0
def add_1():
global glo_num
for i in range(1000000):
glo_num += 1
print(glo_num)
def add_2():
global glo_num
for i in range(1000000):
glo_num += 1
print(glo_num)
if __name__ == '__main__':
first_thread = threading.Thread(target=add_1)
second_thread = threading.Thread(target=add_2)
first_thread.start()
second_thread.start()
"""
1236347
1329801
"""
|
主进程中存在一个主进程,将主线程分别创建了两个子进程,每个子进程都拿全局进行增加操作,由于全局变量是共享的,在两个线程同时进行操作的时候谁先谁先读取谁再递增无法是均匀分配的状态,如果加入线程等待 (join),则可以解决此问题。也就是使线程保持线程同步,同一时刻只有一个线程在操作全局变量。
互斥锁
互斥锁也能在多个线程同时进行全局变量操作时,保证线程同步,使操作共享全局数据不发生错误。
概念:对共享数据进行锁定,保证同一时刻只能有一个线程去操作。互斥锁是多个线程一起去抢,抢到锁的线程先执行对共享数据的操作,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其他等待的线程再去抢这个锁。
使用:导入threading模块中的Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。
1
2
3
4
5
6
7
|
# 创建锁
mutex = threading.Lock()
# 上锁
mutex.acquire()
# 这里编写代码保证同一时刻只有一个线程去操作,对共享数据进行锁定。
# 释放锁
mutex.release()
|
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
glo_num = 0
mutex = threading.Lock() # 创建互斥锁对象
def add_1():
mutex.acquire() # 1方法开始使用就上锁
global glo_num
for i in range(1000000):
glo_num += 1
print(glo_num)
mutex.release() # 1方法使用完开始释放锁
def add_2():
mutex.acquire() # 2方法等待完成后开始上锁
global glo_num
for i in range(1000000):
glo_num += 1
print(glo_num)
mutex.release() # 2方法使用完就开始释放锁
if __name__ == '__main__':
first_thread = threading.Thread(target=add_1)
second_thread = threading.Thread(target=add_2)
first_thread.start()
second_thread.start()
"""
1000000
2000000
"""
|
加上互斥锁都是把多任务改成单任务去执行,保证了数据的准确性,但是多任务性能下降。如果其中一个线程不加锁还是会发生全局变量的同时变化。
死锁
死锁是在使用互斥锁出现问题的一种情况,表现为一个线程一直等待另一线程互斥锁的释放,比如以下情况,当下标越界时,函数直接跳出,并没有互斥锁的释放,所以会持续运行程序,无法停止,产生死锁情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import threading
mutex = threading.Lock()
def get_value(index):
mutex.acquire() # 上锁
my_list = [1,2,3]
if index >= len(my_list): # 防止越界
return # 取值不成功忘记释放互斥锁
print(my_list[index])
mutex.release() # 解锁
if __name__ == '__main__':
for i in range(10): # 创建10个线程执行同一个任务
thread = threading.Thread(target=get_value,args=(i,))
thread.start()
|
⚠️注意:使用互斥锁时一定要记得释放互斥锁,避免产生死锁场景。
线程和进程对比
关系对比
- 线程是依附在进程里的,没有进程就没有线程
- 一个进程默认一条线程,进程可以创建多个线程。
区别对比
- 进程之间不共享全局变量,线程之间是共享全局变量的,不过线程要注意资源竞争的问题,解决办法:互斥锁或者是线程同步。
- 创建进程的资源开销要比创建线程的资源开销要大。
- 进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位。
- 线程不能够独立执行,必须依存在进程中;多进程开发比单进程多线程开发稳定性要强。
优缺点对比
进程的优点就是可以使用多核心,缺点就是资源开销大;线程的优点就是资源开销小,缺点就是不能使用多核。
和计算密集型相关的相关操作使用多进程,文件写入、文件下载,IO操作都用多线程。
总结
- 线程是Python实现多任务时的另一种方式,线程依附于进程,是CPU资源调度的单位,进程中执行代码的是线程。
- Python中使用线程导入
threading
模块,target参数指定目标任务,name给线程命名,group指定线程组,默认为None,创建实例时传参数可以使用元组和字典两种方式。
- 在使用线程时,线程的调度是无序的,时而无序时而有序,主线程会等待所有子线程结束完毕后结束(可以使用setDeamon设置子线程为守护主线程,或者改deamon实例对象的属性值为True来实现主线程结束后子线程销毁操作),线程可以共享全局数据,但是会有资源竞争问题,导致数据不按预期操作(可以使用互斥锁或者是线程同步方式join解决)。
- 互斥锁的意思就是锁定共享资源,保证同一时刻只有一个线程对共享资源进行操作,多个线程去抢锁,抢到的线程先执行,执行完成后释放资源,再给下一个抢锁的线程如此往复。互斥锁保证了线程的顺序执行,提高了处理数据的准确性,但丢失了并行执行的效率。
- 产生死锁的情况就是在使用互斥锁时一个线程一直在等待另一个线程互斥锁的释放。比如一个线程执行一个函数时突然跳出了函数,没有执行互斥锁释放语句,就会造成死锁状态,程序不会停止。
- 线程和进程的不同:
- 关系不同,线程依赖于进程执行。
- 线程共享全局变量但资源存在竞争,进程不共享全局变量。
- 进程使用多核心,线程使用CPU调度。
- 多进程开发比单进程多线程开发要稳定。
- 计算密集型操作使用多进程,IO频繁操作使用多线程。