1 为什么有了gil锁还要互斥锁?
1.1 并行和并发
2 进程,线程和协程 。代码如何实现?你在哪里用过 ?
2.1 进程
2.2 线程
2.3 协程
2.4 进程,线程和协程的使用场景
3 什么是鸭子类型
***进程锁是一种关键的同步机制,确保在多进程环境中对共享资源的并发访问是安全的。***
1. **目的:** 进程锁的主要目的是防止多个进程同时修改或访问共享资源,
从而避免数据不一致或竞态条件的问题。
2. **共享资源:** 进程锁适用于控制对共享资源的访问,这些资源可以是共享内存区域、文件等。
3. **同步机制:** 进程锁提供了同步机制,确保在任意时刻只有一个进程可以访问共享资源。
4. **等待机制:** 如果一个进程获得了进程锁,其他进程必须等待该进程释放锁才能继续访问共享资源。
5. **完整性和一致性:** 进程锁的意义在于保护共享资源的完整性和一致性,
防止并发访问导致的竞态条件和数据不一致。
6. **并发安全:** 进程锁是实现多进程并发安全的重要工具之一,
确保多个进程在访问共享资源时的顺序和互斥性。
总体而言,进程锁是在多进程环境中确保数据一致性和避免竞态条件问题的关键机制。通过限制对共享资源的并发访问,它为程序提供了可靠的同步机制。
***竞态条件***
"""
竞态条件(Race Condition)是多线程或多进程程序中的一种并发问题,其出现的根本原因是多个线程或进程
同时访问共享资源,并试图对这些资源进行修改。竞态条件导致程序的行为不确定,因为最终的结果取决于线程
或进程执行的时序,而这个时序是不可预测的。
竞态条件通常发生在以下情况下:
共享资源: 多个线程或进程同时访问和修改共享的数据或资源,比如共享的变量、文件、内存等。
未同步访问: 没有适当的同步机制来保证对共享资源的互斥访问。
执行时序不确定: 多个线程或进程的执行时序是不确定的,导致无法确定哪个线程或进程先执行,哪个后执行。
"""
***共享资源***
"""
共享资源指的是在多线程或多进程环境中,多个执行单元(线程或进程)可以访问和操作的数据或资源。这些资源可以是内存中的变量、文件、数据库连接、网络连接,或者任何多个执行单元需要共同使用的东西。
在并发编程中,共享资源通常是需要被多个执行单元访问和修改的数据,因此需要采取适当的同步措施,以确保并发访问时的数据一致性和程序的正确性。
一些常见的共享资源包括:
1. **共享变量:** 在多线程环境中,多个线程可以同时读取和写入的变量就是一个共享变量。
如果没有适当的同步,对这些变量的并发访问可能导致竞态条件。
2. **共享内存:** 多个进程可以访问的内存区域。在多进程编程中,多个进程可能共享同一块内存,
需要确保对该内存的并发访问是安全的。
3. **文件:** 多个执行单元可以同时读取和写入的文件是共享资源。在多进程或多线程环境中,
需要注意确保对文件的并发访问不会导致数据不一致或损坏。
4. **网络连接:** 多个执行单元可以共享的网络连接或套接字也是一种共享资源。
在多线程或多进程网络编程中,需要考虑如何同步对这些资源的访问。
在处理共享资源时,确保适当的同步机制是至关重要的,以避免潜在的并发问题,如竞态条件、死锁等。
使用锁、信号量、条件变量等同步工具可以帮助确保对共享资源的安全访问。
"""
***进程锁(也称为互斥锁)的总结***
"""
进程锁(也称为互斥锁)是一种用于控制并发访问共享资源的机制。它的主要目的是防止多个进程
同时修改或访问共享资源导致的数据不一致或竞态条件。在多进程环境中,多个进程可能同时访问和修改共享
资源,如共享内存区域、文件等。如果没有适当的同步机制,可能会导致数据损坏、结果不确定
或其他不可预期的问题。进程锁通过提供互斥访问的机制来解决这个问题。当一个进程获得了进程锁后,
其他进程必须等待该进程释放锁才能继续访问共享资源。这样可以确保在任意时刻只有一个进程可以访问
共享资源,从而避免了并发访问导致的问题。进程锁的意义在于保护共享资源的完整性和一致性。
它确保了多个进程在并发访问共享资源时的顺序和互斥性,避免了竞态条件和数据不一致的情况发生。
所以进程锁是实现多进程并发安全的重要工具之一。
"""
def task(i, lock):
# 上锁
lock.acquire()
print('第%s个进程进来了' % i)
print('第%s个进程走了' % i)
# 释放锁
lock.release()
from multiprocessing import Process
from multiprocessing import Lock
if __name__ == '__main__':
lock = Lock()
for i in range(5):
p = Process(target=task, args=(i, lock))
p.start()
# 1 为什么有了gil锁还要互斥锁
-并发和并行
-不能控制什么时候释放gil锁
-gil锁不能锁住共享资源
# 1 GIL 本身就是大的互斥锁
# 2 同一个进程下,资源是共享的----》多条线程可以操作同一个变量
# 3 多个线程操作同一个变量,就会出现数据安全问题
# 4 临界区:指一段代码或一段程序片段,需要在同一时间只能被一个线程执行---》多个线程操作临界区,会出现并发安全问题
# 错误的理解
-有了gil锁,同一时刻,只有一个线程执行,临界区代码,不也只有一个线程在执行吗?只有一个线程在执行,临界区就是安全的,不会出现数据错乱,所以没有必要再加互斥锁了
# 正确的解释:
gil锁释放不是我们控制的,比如在执行临界区中间,释放了,就会数据错乱问题
# 什么时候会释放gil锁?
-1 线程遇到io
-2 时间片轮转,时间片到了,就会自动切换
# 小案例解释
进程中有个变量a=0
临界区:a+=1
线程1要计算: a+=1
1 线程1 拿到gil
2 读取a=0
3 假设时间片到了,释放gil,释放cpu
4 等待下次被调度执行
10 轮到它了,获取gil锁
11 继续往下执行:计算a+1
12 把结果赋值给a ,a=1
13 释放gil锁
线程2要计算: a+=1
5 线程2获得了gil锁
6 读取a=0
7 计算a+1
8 把结果赋值给a ,a=1
9 释放gil锁
# 互斥锁保证数据安全
a=0
线程1要计算: a+=1
1 线程1 拿到gil
# 加锁
2 读取a=0
3 假设时间片到了,释放gil,释放cpu
4 等待下次被调度执行
7 轮到它了,获取gil锁
8 继续往下执行:计算a+1
9 把结果赋值给a ,a=1
10 释放gil锁
线程2要计算: a+=1
5 线程2获得了gil锁
#获取锁,获取不到
6 释放gil锁
11 获得gil锁
#加锁
12 读取a=0
13 计算a+1
14 把结果赋值给a ,a=1
15 释放锁
16 释放gil锁
# gil锁并不锁住临界区,临界区需要我们自己用互斥锁加锁
在网络安装中,"并行"和"并发"是两个相关但不同的概念:
1. **并行(Parallelism):**
- **定义:** 并行是指在同一时刻执行多个任务。这可以在多个处理单元
(例如,多个处理器核心或多台计算机)上同时进行,每个任务都有自己的执行流。
- **例子:** 在网络安装中,并行可能涉及同时下载多个文件或同时处理多个请求。
这可以提高整体性能,尤其是在有多个可用处理资源的情况下。
2. **并发(Concurrency):**
- **定义:** 并发是指在相同的时间间隔内执行多个任务。这不一定意味着它们在同一时刻都在运行,
而是它们之间存在某种交替或重叠。
- **例子:** 在网络安装中,并发可能涉及同时下载和安装软件包。虽然这两个任务不一定同时进行,
但它们在一段时间内交替执行,以最大程度地减少总体安装时间。
在实践中,"并行"和"并发"可能结合使用。例如,在网络安装过程中,可以使用并发下载多个文件,
同时使用并行处理这些下载的文件以加快整体安装速度。这种综合利用可以提高效率和性能。
**概念:** 进程是操作系统中执行的一个程序的实例。
每个进程都有自己的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。
**实现:** 在Python中,可以使用`multiprocessing`模块创建和管理进程。
import multiprocessing
import os
def worker():
print(f"Worker PID: {os.getpid()}")
if __name__ == "__main__":
# 创建进程
process = multiprocessing.Process(target=worker)
# 启动进程
process.start()
# 等待进程结束
process.join()
print("Main process PID:", os.getpid())
**概念:** 线程是进程中的一个执行流,一个进程可以由多个线程同时执行。
**实现:** 在Python中,可以使用`threading`模块创建和管理线程。
import threading
def worker():
print("Worker Thread")
if __name__ == "__main__":
# 创建线程
thread = threading.Thread(target=worker)
# 启动线程
thread.start()
# 等待线程结束
thread.join()
print("Main Thread")
**概念:** 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。
**实现:** 在Python中,可以使用`asyncio`库创建和管理协程。
import asyncio
async def worker():
print("Worker Coroutine")
if __name__ == "__main__":
# 创建事件循环
loop = asyncio.get_event_loop()
# 创建协程任务
task = loop.create_task(worker())
# 运行事件循环
loop.run_until_complete(task)
print("Main Coroutine")
**进程:** 适用于多核CPU,可以充分利用多核优势。每个进程有自己独立的内存空间,相对隔离。
**线程:** 适用于I/O密集型任务,例如网络请求、文件操作等。线程共享同一进程的内存空间,
但需要注意线程安全。
**协程:** 适用于高并发、高IO的场景。协程由用户控制,避免了线程切换的开销,提高了并发性能。
在实际项目中,可能会同时使用这三种技术,根据任务的不同特点选择合适的并发模型。例如,可以使用进程池
处理CPU密集型任务,使用线程池处理I/O密集型任务,使用协程处理高并发、高IO的任务。
**定义:** 鸭子类型是一种动态类型的风格。在鸭子类型中,一个对象的特征不是由继承自特定的类或
实现特定的接口,而是由它所包含的方法和属性决定的。
**思想:** "如果它走起来像鸭子,叫起来也像鸭子,那么它就是鸭子。" 这表示在鸭子类型中,
我们关注的不是对象的类型本身,而是对象的行为。
**实例:** Python 是一种支持鸭子类型的语言。例如,一个函数可能接收一个具有 `len()` 方法的对象,
而不关心对象的具体类型是什么,只要它有合适的方法。
**优势:** 鸭子类型使得代码更加灵活,不需要依赖于特定的继承关系,而是关注对象的能力。
这样可以更好地实现代码的复用和扩展。
class Duck:
def shut(self):
return "嘎嘎!嘎嘎!"
def fly(self):
return "用翅膀飞"
class Person:
def shut(self):
return "我能像鸭子一样嘎嘎叫。"
def fly(self):
return "我用我的手臂飞翔。"
def introduce(entity): # entity实例
print(entity.shut())
print(entity.fly())
# 使用鸭子类型
duck = Duck()
person = Person()
introduce(duck)
introduce(person)
在这个例子中,`Duck` 类和 `Person` 类都没有继承相同的基类,但它们都有 `quack` 和 `fly` 方法。
函数 `introduce` 接受任何具有这两个方法的对象,无论其具体类型是什么。这就是鸭子类型的概念。