本文讨论了线程同步的概念。多线程用Python编程语言编写。
线程之间的同步
线程同步被定义为一种机制, 可确保两个或多个并发线程不会同时执行某些特定的程序段, 即关键部分.
关键部分是指程序中访问共享资源的部分。
例如, 在下图中, 3个线程尝试同时访问共享资源或关键部分。
并发访问共享资源可能导致比赛条件.
当两个或多个线程可以访问共享数据并且它们试图同时更改它们时, 就会发生竞争状态。结果, 变量的值可能是不可预测的, 并且取决于过程的上下文切换的时间而变化。
考虑下面的程序以了解竞争条件的概念:
import threading
# global variable x
x = 0
def increment():
"""
function to increment global variable x
"""
global x
x + = 1
def thread_task():
"""
task for thread
calls increment function 100000 times.
"""
for _ in range ( 100000 ):
increment()
def main_task():
global x
# setting global variable x as 0
x = 0
# creating threads
t1 = threading.Thread(target = thread_task)
t2 = threading.Thread(target = thread_task)
# start threads
t1.start()
t2.start()
# wait until threads finish their job
t1.join()
t2.join()
if __name__ = = "__main__" :
for i in range ( 10 ):
main_task()
print ( "Iteration {0}: x = {1}" . format (i, x))
输出如下:
Iteration 0: x = 175005
Iteration 1: x = 200000
Iteration 2: x = 200000
Iteration 3: x = 169432
Iteration 4: x = 153316
Iteration 5: x = 200000
Iteration 6: x = 167322
Iteration 7: x = 200000
Iteration 8: x = 169917
Iteration 9: x = 153589
在上面的程序中:
- 两个线程t1和t2创建于主要任务函数和全局变量X设置为0。
- 每个线程都有一个目标函数thread_task在其中增量函数被称为100000次。
- 增量函数将增加全局变量X在每个通话中减1。
的预期最终价值X是200000但我们在10次迭代中得到的主要任务函数是一些不同的值。
这是由于线程同时访问共享变量而发生的X。价值的这种不可预测性X只不过是比赛条件.
下图显示了如何
比赛条件
发生在以上程序中:
请注意,
X
上图中的是12, 但由于竞赛条件, 结果是11!
因此, 我们需要一个工具来在多个线程之间进行适当的同步。
使用锁
穿线模块提供了锁上课以应对比赛条件。锁是使用信号操作系统提供的对象。
信号量是一个同步对象, 它控制并行编程环境中多个进程/线程对公共资源的访问。它只是操作系统(或内核)存储中指定位置的值, 每个进程/线程都可以检查然后更改。取决于找到的值, 进程/线程可以使用该资源, 或者将发现该资源已在使用中, 并且必须等待一段时间才能重试。信号量可以是二进制的(0或1), 也可以具有其他值。通常, 使用信号量的进程/线程会检查该值, 然后(如果它使用了资源)则更改该值以反映该值, 以便后续的信号量用户将知道等待。
锁该类提供以下方法:
- 获取([阻止]):获取锁。锁可以是锁定的也可以是非锁定的。
- 在将阻塞参数设置为的情况下调用true(默认设置), 直到锁解锁后, 线程执行才会被阻止, 然后将锁设置为已锁定并返回true.
- 在将阻塞参数设置为的情况下调用false, 不会阻止线程执行。如果锁定已解锁, 则将其设置为锁定并返回true否则返回false立即。
- 发布() :释放锁。
- 锁锁定后, 将其重置为解锁状态, 然后返回。如果其他任何线程被阻塞, 等待锁解锁, 则允许其中一个线程继续进行。
- 如果锁已解锁, 则线程错误被提出。
考虑下面给出的示例:
import threading
# global variable x
x = 0
def increment():
"""
function to increment global variable x
"""
global x
x + = 1
def thread_task(lock):
"""
task for thread
calls increment function 100000 times.
"""
for _ in range ( 100000 ):
lock.acquire()
increment()
lock.release()
def main_task():
global x
# setting global variable x as 0
x = 0
# creating a lock
lock = threading.Lock()
# creating threads
t1 = threading.Thread(target = thread_task, args = (lock, ))
t2 = threading.Thread(target = thread_task, args = (lock, ))
# start threads
t1.start()
t2.start()
# wait until threads finish their job
t1.join()
t2.join()
if __name__ = = "__main__" :
for i in range ( 10 ):
main_task()
print ( "Iteration {0}: x = {1}" . format (i, x))
输出如下:
Iteration 0: x = 200000
Iteration 1: x = 200000
Iteration 2: x = 200000
Iteration 3: x = 200000
Iteration 4: x = 200000
Iteration 5: x = 200000
Iteration 6: x = 200000
Iteration 7: x = 200000
Iteration 8: x = 200000
Iteration 9: x = 200000
让我们尝试逐步理解上面的代码:
首先,
锁
使用以下对象创建对象:
lock = threading.Lock()
然后,
锁
作为目标函数参数传递:
t1 = threading.Thread(target=thread_task, args=(lock, ))
t2 = threading.Thread(target=thread_task, args=(lock, ))
在目标函数的关键部分, 我们使用
lock.acquire()
方法。一旦获得了锁, 其他任何线程都无法访问关键部分(此处,
增量
功能), 直到使用释放锁为止
lock.release()
方法。
lock.acquire()
increment()
lock.release()
正如你在结果中看到的那样, X每次得出200000(这是预期的最终结果)。
这是下面给出的示意图, 描述了上述程序中锁的实现:
这使我们到本教程系列的结尾
Python中的多线程
.
最后, 这里是多线程的一些优点和缺点:
优点:
- 它不会阻止用户。这是因为线程彼此独立。
- 由于线程并行执行任务, 因此可以更好地利用系统资源。
- 增强了多处理器计算机上的性能。
- 多线程服务器和交互式GUI专门使用多线程。
缺点:
- 随着线程数量的增加, 复杂性也随之增加。
- 共享资源(对象, 数据)的同步是必要的。
- 调试困难, 结果有时无法预测。
- 可能导致饥饿的潜在死锁, 即某些线程可能设计不佳
- 构造和同步线程需要占用大量CPU /内存。
如果发现任何不正确的地方, 或者想分享有关上述主题的更多信息, 请写评论。
注意怪胎!巩固你的基础Python编程基础课程和学习基础知识。
首先, 你的面试准备可通过以下方式增强你的数据结构概念:Python DS课程。