Python3进阶篇(三)——多线程

前言:
阅读这篇文章我能学到什么?
  一个应用程序就相当于一个进程,该进程创建时就具有一个主线程(内核线程),主线程可以创建其他子线程(用户线程),当存在子线程时就形成了多线程。多线程可以使得运行程序在宏观上同时执行多个任务,在一定程度上加快软件执行速度。线程的操作涉及到:线程创建、同步、退让、抢占等。请阅读这篇文章学习它。

1 了解并创建线程

1.1 了解线程

  当进程被创建时,操作系统将会为它创建一个主线程,也即内核线程,注意它是操作系统创建的。用户可以通过主线程创建子线程,或称用户线程。不论主线程还是子线程,每个独立的线程都有一个程序的入口,对应在代码中就是入口函数。一个进程可以有多个线程,线程是进程的执行单元。宏观上各个线程可以是同时执行的,微观上依然是CPU分时间片执行。Python3为我们提供了操作线程的类,这简化了线程开发的步骤。旧的线程模块是_thread,Python3提供了新的模块’threading’操作线程。

1.2 创建线程

  我们尝试通过模块’_thread’进行线程创建。
  代码示例:

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

def Run(ThreadName, Speed):
while True:
time.sleep(Speed) #延迟一定时间,单位为s
print(ThreadName, "----", time.ctime(time.time()))

_thread.start_new_thread(Run, ("Thread-1", 3))
_thread.start_new_thread(Run, ("Thread-2", 5))

Run("ThreadMain", 4)

  运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
Thread-1 ---- Sun Jun 21 13:02:44 2020
ThreadMain ---- Sun Jun 21 13:02:45 2020
Thread-2 ---- Sun Jun 21 13:02:46 2020
Thread-1 ---- Sun Jun 21 13:02:47 2020
ThreadMain ---- Sun Jun 21 13:02:49 2020
Thread-1 ---- Sun Jun 21 13:02:50 2020
Thread-2 ---- Sun Jun 21 13:02:51 2020
ThreadMain ---- Sun Jun 21 13:02:53 2020
Thread-1 ---- Sun Jun 21 13:02:53 2020
Thread-2 ---- Sun Jun 21 13:02:56 2020
Thread-1 ---- Sun Jun 21 13:02:56 2020
ThreadMain ---- Sun Jun 21 13:02:57 2020
Thread-1 ---- Sun Jun 21 13:02:59 2020

  该示例中总共有三个线程,函数start_new_thread()用于创建线程并指定线程的入口函数,并且开始开始运行线程。最后别忘了主线程也在运行。从打印的时间信息可以看出,三个线程是各自独立运行的。

1.3 创建线程类

  通常为了更好的封装性和代码重用性,我们会将线程类进行二次封装。这里我们尝试通过模块threading封装一个线程类。
  代码示例:

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
import threading
import time

class myThread(threading.Thread):
def __init__(self, Name, Speed):
threading.Thread.__init__(self) #调用父类构造函数
self.Name = Name
self.Speed = Speed

def run(self): #重载run函数
print("开始线程:", self.Name)
EntryFunction(self.Name, self.Speed)
print("结束子线程:", self.Name)

def EntryFunction(Name, Speed): #线程入口函数
for n in range(5):
time.sleep(Speed)
print(Name, "----", time.ctime(time.time()))

Thread1 = myThread("Thread-1", 3)
Thread2 = myThread("Thread-2", 5)

Thread1.start()
Thread2.start()
#等待子线程结束
Thread1.join()
Thread2.join()

print("主线程结束")

  运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
开始线程: Thread-1
开始线程: Thread-2
Thread-1 ---- Sun Jun 21 14:35:59 2020
Thread-2 ---- Sun Jun 21 14:36:01 2020
Thread-1 ---- Sun Jun 21 14:36:02 2020
Thread-1 ---- Sun Jun 21 14:36:05 2020
Thread-2 ---- Sun Jun 21 14:36:06 2020
Thread-1 ---- Sun Jun 21 14:36:08 2020
Thread-2 ---- Sun Jun 21 14:36:11 2020
Thread-1 ---- Sun Jun 21 14:36:11 2020
结束子线程: Thread-1
Thread-2 ---- Sun Jun 21 14:36:16 2020
Thread-2 ---- Sun Jun 21 14:36:21 2020
结束子线程: Thread-2
主线程结束

  线程对象示例化后通过调用start()函数可启动线程,其将从线程run()函数开始执行,我们重载了父类的run()函数。join()函数使得主线程会等待子线程结束。

2 线程同步

  一些数据资源需要被多个线程操作,多个线程同时对同一块数据修改,可能出现不可预料的结果。因为线程操作数据是需要时间的,若在数据修改完成之前其他线程也进行修改则导致数据出现预料之外的情况。可以通过线程锁实现线程同步,线程锁有两种状态,即锁定和未锁定。
  代码示例:

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
41
import threading
import time

#1~5 5组数据及其有效标志,Couter表示剩余有效数据数,Length表示数据总个数
Data = {"Counter":5, "Length":5, 1:True, 2:True, 3:True, 4:True, 5:True} #用于测试的线程共享数据

ThreadLock = threading.Lock() #创建线程锁

class myThread(threading.Thread):
def __init__(self, Name, Speed):
threading.Thread.__init__(self) #调用父类构造函数
self.Name = Name
self.Speed = Speed

def run(self): #重载run函数
print("开始线程:", self.Name)
GetData(self.Name, self.Speed)
print("结束子线程:", self.Name)

def GetData(Name, Speed): #线程入口函数
while Data["Counter"] > 0:
time.sleep(Speed)
print(Name, "----", time.ctime(time.time()))
ThreadLock.acquire() #获取线程锁,拿不到锁则等待
for n in range(1, Data["Length"] + 1):
if Data[n] == True:
Data[n] = False
Data["Counter"] -= 1
print(Name, "----", "Get: ", n)
break
ThreadLock.release() #释放线程锁

Thread1 = myThread("Thread-1", 3)
Thread2 = myThread("Thread-2", 5)

Thread1.start()
Thread2.start()
Thread1.join()
Thread2.join()

print("主线程结束")

  运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
开始线程: Thread-1
开始线程: Thread-2
Thread-1 ---- Sun Jun 21 17:24:55 2020
Thread-1 ---- Get: 1
Thread-2 ---- Sun Jun 21 17:24:57 2020
Thread-2 ---- Get: 2
Thread-1 ---- Sun Jun 21 17:24:58 2020
Thread-1 ---- Get: 3
Thread-1 ---- Sun Jun 21 17:25:01 2020
Thread-1 ---- Get: 4
Thread-2 ---- Sun Jun 21 17:25:02 2020
Thread-2 ---- Get: 5
结束子线程: Thread-2
Thread-1 ---- Sun Jun 21 17:25:04 2020
结束子线程: Thread-1
主线程结束

  我们创建了一个字典,字典内有5组数据,通过线程去将数据挨个置为失效,为了保证同一组数据不会被多个线程重复操作,并且是按顺序失效这5组数据,我们需要给数据操作的代码段加上线程锁,以此来保证每次只能有一个线程操作这5组数据。acquire()函数使得每次只有一个线程能获得线程锁,即进行后续的数据操作,其他线程则会等待,知道其他线程释放锁后并且自己获得锁,release()函数使得线程释放获得的锁。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import queue
import threading
import time

exitFlag = 0

class myThread (threading.Thread):
def __init__(self, Name, q):
threading.Thread.__init__(self)
self.Name = Name
self.q = q
def run(self):
print ("开启线程:" + self.Name)
process_data(self.Name, self.q)
print ("退出线程:" + self.Name)

def process_data(Name, q):
while not exitFlag: #线程退出标志
queueLock.acquire()
if not workQueue.empty(): #队列非空
data = q.get()
print ("%s processing %s" % (Name, data))
queueLock.release()
time.sleep(1) #为了演示短暂延迟,防止一个线程全部执行完

threadList = ["Thread-1", "Thread-2", "Thread-3"]
PriorityList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = queue.Queue(10)
threads = []
threadID = 1

# 创建新线程
for ThreadName in threadList:
thread = myThread(ThreadName, workQueue) #创建线程
thread.start() #执行线程
threads.append(thread) #添加到线程队列
threadID += 1

# 填充队列
for Priority in PriorityList: #主线程进行队列填充
workQueue.put(Priority) #添加到队列中

# 等待队列清空
while not workQueue.empty(): #主线程等待直到队列为空
pass

# 通知线程是时候退出
exitFlag = 1 #主线程通知子线程结束

# 等待所有线程完成
for t in threads: #主线程等待所有子线程结束后结束
t.join()

print ("退出主线程")

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
开启线程:Thread-1
开启线程:Thread-2
开启线程:Thread-3
Thread-2 processing One
Thread-3 processing Two
Thread-1 processing Three
Thread-2 processing Four
Thread-3 processing Five
退出线程:Thread-1
退出线程:Thread-2
退出线程:Thread-3
退出主线程

  上面例子通过三个线程去访问队列,不管线程谁先执行,最终访问的顺序都是OneFive