Skip to content

并行线程与输出顺序的影响

在多线程编程中,常常需要处理多个任务的并行执行。但由于多线程的并行特性,输出顺序往往不像单线程程序那样可预测。在这篇博客中,我们将通过一个简单的 Python 示例,探索并行线程如何影响输出顺序,并解释为什么会出现意外的输出格式。

示例代码

以下是一个使用线程和锁的 Python 代码,旨在按顺序打印 a, b, c 三次,并在每次打印后输出数字序列 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
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from threading import Thread, Lock

lock_a = Lock()
lock_b = Lock()
lock_c = Lock()

lock_b.acquire()
lock_c.acquire()

def print_a():
    for _ in range(3):
        lock_a.acquire()
        print('a', end='')
        lock_b.release()

def print_b():
    for _ in range(3):
        lock_b.acquire()
        print('b', end='')
        lock_c.release()

def print_c():
    for _ in range(3):
        lock_c.acquire()
        print('c', end='')
        lock_a.release()

ta = Thread(target=print_a)
tb = Thread(target=print_b)
tc = Thread(target=print_c)

ta.start()
tb.start()
tc.start()

for i in range(3):
    print(i)

ta.join()
tb.join()
tc.join()

for i in range(3):
    print(i)

输出结果

我们期望的输出是 abcabcabc,然后是 0 1 2 两次。然而,实际输出是这样的:

1
2
3
4
5
6
abc0
1
2
abcabc0
1
2

为什么会出现这种输出?

线程并行性

在这个程序中,我们使用了三个线程分别打印字母 a, b, c,并通过锁来确保它们按顺序执行。但是,我们在启动线程之后立即在主线程中执行了 for i in range(3): print(i),这意味着主线程和三个打印线程是同时运行的。

执行顺序

  1. 主线程首先执行:当我们启动三个线程后,主线程并没有等待线程完成,而是直接开始执行第一个 for 循环,打印数字 0。由于主线程和三个线程是并行的,主线程快速地输出 0 1 2,而此时第一个线程也开始执行,并成功打印 abc

  2. 线程的输出:由于我们使用锁确保 a, b, c 按顺序输出,线程的输出在这段时间不会被打乱。所以第一个 abc 会在 0 之后立即被输出。

  3. 主线程和线程竞争输出:在主线程完成 0 1 2 的输出后,线程仍在继续执行,打印剩下的两个 abc。当线程完成后,主线程继续执行,输出第二个 0 1 2

最终,线程的输出和主线程的输出交织在一起,形成了我们看到的输出结果。

join() 方法的作用

为了让主线程等待所有线程完成,我们使用了 ta.join()tb.join()tc.join()join() 方法会阻塞主线程,直到对应的线程完成为止。

但是,join() 是在主线程已经输出完第一个 0 1 2 之后才调用的,所以主线程并不会在开始时等待线程完成任务。这也是为什么我们看到数字 0 出现在第一个 abc 之前。

总结

这个例子展示了线程和主线程在并行执行时如何产生非预期的输出顺序。通过理解 start()join() 的作用以及锁的使用方式,我们可以更好地控制线程的执行顺序。

小结几点要点:

  1. 线程的并行执行:启动线程后,主线程和其他线程是并行运行的,输出会相互交错。
  2. 锁的顺序控制:通过使用锁,保证了 a -> b -> c 的打印顺序,但并不能控制主线程的输出顺序。
  3. join() 的作用join() 确保主线程在线程完成后继续运行,但它不影响启动线程时的输出竞争。

完整输出流程图:

1
2
3
4
5
1. 启动三个线程,线程开始打印第一个 abc
2. 主线程开始打印第一个 0 1 2
3. 线程继续打印 abcabc
4. 主线程等待所有线程完成
5. 主线程打印第二个 0 1 2

通过这个例子,我们能够更好地理解多线程环境下如何控制输出顺序,也为未来处理复杂的多线程问题提供了借鉴。