leetcode(多线程) 按序打印

按序打印
1、题目:我们提供了一个类:

public class Foo { public void one() { print("one"); } public void two() { print("two"); } public void three() { print("three"); } }

三个不同的线程将会共用一个 Foo 实例。
线程 A 将会调用 one() 方法
线程 B 将会调用 two() 方法
线程 C 将会调用 three() 方法
请设计修改程序,以确保 two() 方法在 one() 方法之后被执行,three() 方法在 two() 方法之后被执行。
示例 1:
输入: [1,2,3]
输出: "onetwothree"
解释:
有三个线程会被异步启动。
输入 [1,2,3] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 two() 方法,线程 C 将会调用 three() 方法。
正确的输出是 "onetwothree"。
示例 2:
输入: [1,3,2]
输出: "onetwothree"
解释:
输入 [1,3,2] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 three() 方法,线程 C 将会调用 two() 方法。
正确的输出是 "onetwothree"。
注意:
尽管输入中的数字似乎暗示了顺序,但是我们并不保证线程在操作系统中的调度顺序。
你看到的输入格式主要是为了确保测试的全面性。
2、解题思路: 并发问题
并发问题来自并发计算的场景,该场景下,程序在多线程(或多进程)中同时执行。
同时进行并不是完全指进程或线程在不同的物理 CPU 上独立运行,更多情况下,是在一个物理 CPU 上交替执行多个线程或进程。并发既可以在线程中,也可以在进程中。
并发主要为多任务的情况设计。当如果应用不当,可能会引发一些漏洞。按照情况不同,可以分为三种:
竞态条件:由于多进程之间的竞争执行,导致程序未按照期望的顺序输出。
死锁:并发程序等待一些必要资源,导致没有程序可以执行。
资源不足:进程被永久剥夺了运行所需的资源。
此题中存在竞态条件。下面展示一个竞态条件的例子。
假设有一个方法 withdraw(amount),如果请求量小于当前余额,则从当前余额中减去请求量,然后返回余额。方法定义如下:
balance = 500 def withdraw(amount): if (amount < balance): balance -= amount return balance

我们 期望 该方法执行后余额永远不会为负。
但是有可能出现竞态条件,使得余额变为负数。假设两个线程同时使用不同的参数执行该方法。
例如:
线程 1 执行 withdraw(amount=400),线程 2 执行 withdraw(amount=200)。这两个线程的执行顺序如下图所示。在每个时刻只执行一条语句。
leetcode(多线程) 按序打印
文章图片

上述流程执行结束后,余额变成负数,这并不是期望的输出。
无竞争并发
并发问题有一个共同特征:多个线程/进程之间共享一些资源(例如:余额)。由于无法消除资源共享的约束,防止并发问题就变成了 资源共享的协调 问题。
根据这个思路,如果可以确保程序中 关键部分代码的独占性(例如:检查和减少余额),就可以防止程序进入不一致的状态。
竞争条件的解决方案为:需要某些关键部分代码具有排他性,即在给定的时间内,只有一个线程可以进入关键部分代码。
可以将这种机制看做限制关键部分代码访问的锁。在前面示例的关键部分代码加锁,即检查余额和减少余额的语句。然后重新运行两个线程,会有下图的执行顺序:
leetcode(多线程) 按序打印
文章图片

在该机制下,一旦一个线程进入关键部分,它就可以阻止其他线程进入该关键部分。
例如,在时间点 3,线程 2 进入关键部分,那么在时间点 4,如果没有锁保护,线程 1 就可能进入关键部分。最后两个线程同时运行,保证系统的一致性,并确保余额正确。
如果该线程未被授权进入关键代码,可以认为该线程被阻塞或进入睡眠状态。
例如,线程 1 在时间点 4 被阻塞,之后关键部分被释放,可以通知其他等待线程。线程 2 在时间点 5 释放了关键部分,就可以通知 线程 1 进入。
这种机制还具有唤醒其他等待线程的功能。
总之,为了防止出现并发竞争状态,需要一种具有两种功能的机制:1)关键部分的访问控制;2)通知阻塞线程。
思路:
题目要求按顺序依次执行三个方法,且每个方法都在单独的线程中运行。为了保证线程的执行顺序,可以在方法之间创建一些依赖关系,即第二个方法必须在第一个方法时候执行,第三个方法必须在第二个方法之后执行。
方法对之间的依赖关系形成了所有方法的特定的执行顺序。例如 A leetcode(多线程) 按序打印
文章图片

依赖关系可以通过并发机制实现。使用一个共享变量 firstJobDone 协调第一个方法与第二个方法的执行顺序,使用另一个共享变量 secondJobDone 协调第二个方法与第三个方法的执行顺序。
算法:
(1) 首先初始化共享变量 firstJobDone 和 secondJobDone,初始值表示所有方法未执行。
(2) 方法 first() 没有依赖关系,可以直接执行。在方法最后更新变量 firstJobDone 表示该方法执行完成。
(3) 方法 second() 中,检查 firstJobDone 的状态。如果未更新则进入等待状态,否则执行方法 second()。在方法末尾,更新变量 secondJobDone 表示 second() 执行完成。
(4) 方法 third() 中,检查 secondJobDone 的状态。与方法 second() 类似,执行 third() 之前,需要先等待 secondJobDone 的状态。
leetcode(多线程) 按序打印
文章图片

3、代码:
from threading import Lock, Threaddef One(): print('one', end='')def Two(): print('two', end='')def Three(): print('three', end='')class Foo(): def __init__(self): self.firstJobDone = Lock() self.secondJobDone = Lock() # 获取锁 self.firstJobDone.acquire() self.secondJobDone.acquire()def first(self, printFirst: 'Callable[[], None]') -> None: printFirst() # 释放锁 self.firstJobDone.release()def second(self, printSecond: 'Callable[[], None]') -> None: with self.firstJobDone: printSecond() # 释放锁 self.secondJobDone.release()def third(self, printThird: 'Callable[[], None]') -> None: with self.secondJobDone: printThird()if __name__ == '__main__': foo = Foo() callablelist = [foo.first, foo.second, foo.third] callablelistargs = [One, Two, Three] order = [1, 3, 2] A = Thread(target=callablelist[order[0]-1], args=(callablelistargs[order[0]-1],)) B = Thread(target=callablelist[order[1]-1], args=(callablelistargs[order[1]-1],)) C = Thread(target=callablelist[order[2]-1], args=(callablelistargs[order[2]-1],)) A.start() B.start() C.start()

【leetcode(多线程) 按序打印】4、测试结果:
leetcode(多线程) 按序打印
文章图片

    推荐阅读