python必坑知识点(并发编程)
1 进程和线程
线程,是计算机中可以被cpu调度的最小单元(真正在工作)。
进程,是计算机资源分配的最小单元(进程为线程提供资源)。
一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。
通过 进程 和 线程 都可以将 串行 的程序变为 并发
2 多进程和多线程
多进程的开销要比多线程的开销大
from multiprocessing import Process
from threading import Thread
2.1 线程常见方法
`t.start()`,当前线程准备就绪(等待CPU调度,具体时间是由CPU来决定)。
`t.join()`,等待当前线程的任务执行完毕后再向下继续执行。
`t.setDaemon(布尔值)` ,守护线程(必须放在start之前)
- `t.setDaemon(True)`,设置为守护线程,主线程执行完毕后,子线程也自动关闭。
- `t.setDaemon(False)`,设置为非守护线程,主线程等待子线程,子线程执行完毕后,主线程才结束。(默认)
线程名称的设置和获取
import threading
def task(arg):
# 获取当前执行此代码的线程
name = threading.current_thread().getName()
print(name)
for i in range(10):
t = threading.Thread(target=task, args=(11,))
t.setName('日魔-{}'.format(i))
t.start()
自定义线程类,直接将线程需要做的事写到run方法中。
import threading
class MyThread(threading.Thread):
def run(self):
print('执行此线程', self._args) # self._args获取传入的参数
t = MyThread(args=(100,))
t.start()
3 GIL全局锁
全局解释器锁(Global Interpreter Lock),CPython
特有,让一个进程中同一个时刻只能有一个线程可以被CPU调用
如果程序想利用 计算机的多核优势,让CPU同时处理一些任务,适合用多进程开发(即使资源开销大)。
如果程序不利用 计算机的多核优势,适合用多线程开发。
- 计算密集型,用多进程,例如:大量的数据计算【累加计算示例】。
- IO密集型,用多线程,例如:文件读写、网络数据传输【下载抖音视频示例】。
4 线程安全
一个进程中可以有多个线程,且线程共享所有进程中的资源。
多个线程同时去操作一个"东西",可能会存在数据混乱的情况
5 线程锁
Lock
和RLock
RLock
支持多次申请锁和多次释放;Lock不支持。开发中常用RLock
。
-
Lock
,同步锁。import threading num = 0 lock_object = threading.Lock() def task(): print("开始") lock_object.acquire() # 第1个抵达的线程进入并上锁,其他线程就需要再此等待。 global num for i in range(1000000): num += 1 lock_object.release() # 线程出去,并解开锁,其他线程就可以进入并执行了 print(num) for i in range(2): t = threading.Thread(target=task) t.start()
-
RLock
,递归锁。import threading num = 0 lock_object = threading.RLock() def task(): print("开始") lock_object.acquire() # 第1个抵达的线程进入并上锁,其他线程就需要再此等待。 global num for i in range(1000000): num += 1 lock_object.release() # 线程出去,并解开锁,其他线程就可以进入并执行了 print(num) for i in range(2): t = threading.Thread(target=task) t.start()
6 死锁
死锁,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。
两种情况
1.单个线程中连续的获取锁
2.两个或多个线程手上拿着对方需要的锁
7 线程池
Python3
中官方才正式提供线程池。
线程不是开的越多越好,开的多了可能会导致系统的性能更低了。
不建议:无限制的创建线程。
建议:使用线程池
# 等待线程池的任务执行完毕
pool.shutdown(True)
# 任务执行完后,在干点其他事
def done(response):
print("任务执行后的返回值", response.result())
future = pool.submit(task, url)
future.add_done_callback(done)
# 或者
future_list = []
future = pool.submit(task, url)
future_list.append(future)
pool.shutdown(True)
for fu in future_list:
print(fu.result())
8 单例模式
面试题:手写单例模式
单例模式:每次实例化类的对象时,都是最开始创建的那个对象,不再重复创建对象。
-
简单实现
class Singleton: instance = None def __init__(self, name): self.name = name def __new__(cls, *args, **kwargs): # 返回空对象 if cls.instance: return cls.instance cls.instance = object.__new__(cls) return cls.instance obj1 = Singleton('alex') obj2 = Singleton('SB') print(obj1,obj2)
-
多线程实现(加锁)
import threading class Singleton: instance = None lock = threading.RLock() def __init__(self, name): self.name = name def __new__(cls, *args, **kwargs): # 加性能 if cls.instance: return cls.instance with cls.lock: if cls.instance: return cls.instance cls.instance = object.__new__(cls) return cls.instance def task(): obj = Singleton('x') print(obj) for i in range(10): t = threading.Thread(target=task) t.start()
9 进程
进程相关必须在if __name__ == '__main__':下运行
关于在Python中基于multiprocessiong
模块操作的进程:
Depending on the platform, multiprocessing
supports three ways to start a process. These start methods are
fork,【“拷贝”几乎所有资源】【支持文件对象/线程锁等传参】【unix】【任意位置开始】【快】
The parent process uses
os.fork()
to fork the Python interpreter. The child process, when it begins, is effectively identical to the parent process. All resources of the parent are inherited by the child process. Note that safely forking a multithreaded process is problematic.Available on Unix only. The default on Unix.spawn,【run参数传必备资源】【不支持文件对象/线程锁等传参】【unix、win】【main代码块开始】【慢】
The parent process starts a fresh python interpreter process. The child process will only inherit those resources necessary to run the process object’s
run()
method. In particular, unnecessary file descriptors and handles from the parent process will not be inherited. Starting a process using this method is rather slow compared to using fork or forkserver.Available on Unix and Windows. The default on Windows and macOS.forkserver,【run参数传必备资源】【不支持文件对象/线程锁等传参】【部分unix】【main代码块开始】
When the program starts and selects the forkserver start method, a server process is started. From then on, whenever a new process is needed, the parent process connects to the server and requests that it fork a new process. The fork server process is single threaded so it is safe for it to use
os.fork()
. No unnecessary resources are inherited.Available on Unix platforms which support passing file descriptors over Unix pipes.
import multiprocessing
multiprocessing.set_start_method("spawn")
官方文档:https://docs.python.org/3/library/multiprocessing.html
9.1 案例
import multiprocessing
def task():
print(name)
file_object.write("alex\n")
file_object.flush()
if __name__ == '__main__':
multiprocessing.set_start_method("fork")
name = []
file_object = open('x1.txt', mode='a+', encoding='utf-8')
file_object.write("武沛齐\n")
p1 = multiprocessing.Process(target=task)
p1.start()
# 文件内容
"""
武沛齐
alex
武沛齐
"""
import multiprocessing
def task():
print(name)
file_object.write("alex\n")
file_object.flush()
if __name__ == '__main__':
multiprocessing.set_start_method("fork")
name = []
file_object = open('x1.txt', mode='a+', encoding='utf-8')
file_object.write("武沛齐\n")
file_object.flush()
p1 = multiprocessing.Process(target=task)
p1.start()
# 文件内容
"""
武沛齐
alex
"""
import multiprocessing
import threading
import time
def func():
print("来了") # 阻塞,等待主线程task释放锁
with lock:
print(666)
time.sleep(1)
def task():
# 拷贝的锁也是被申请走的状态
# 被谁申请走了? 被子进程中的主线程申请走了
for i in range(10):
t = threading.Thread(target=func)
t.start()
time.sleep(2)
lock.release()
if __name__ == '__main__':
multiprocessing.set_start_method("fork")
name = []
lock = threading.RLock()
lock.acquire()
# print(lock)
# lock.acquire() # 申请锁
# print(lock)
# lock.release()
# print(lock)
# lock.acquire() # 申请锁
# print(lock)
p1 = multiprocessing.Process(target=task)
p1.start()
进程的名称的设置和获取
import os
import time
import threading
import multiprocessing
def func():
time.sleep(3)
def task(arg):
for i in range(10):
t = threading.Thread(target=func)
t.start()
print(os.getpid(), os.getppid())
print("线程个数", len(threading.enumerate()))
time.sleep(2)
print("当前进程的名称:", multiprocessing.current_process().name)
if __name__ == '__main__':
print(os.getpid())
multiprocessing.set_start_method("spawn")
p = multiprocessing.Process(target=task, args=('xxx',))
p.name = "哈哈哈哈"
p.start()
print("继续执行...")
自定义进程类,直接将线程需要做的事写到run方法中。
import multiprocessing
class MyProcess(multiprocessing.Process):
def run(self):
print('执行此进程', self._args)
if __name__ == '__main__':
multiprocessing.set_start_method("spawn")
p = MyProcess(args=('xxx',))
p.start()
print("继续执行...")
CPU个数,程序一般创建多少个进程?(利用CPU多核优势)。
import multiprocessing
multiprocessing.cpu_count()
import multiprocessing
if __name__ == '__main__':
count = multiprocessing.cpu_count()
for i in range(count - 1):
p = multiprocessing.Process(target=xxxx)
p.start()
9.2 数据共享
Shared memory
Data can be stored in a shared memory map using Value
or Array
. For example, the following code
'c': ctypes.c_char, 'u': ctypes.c_wchar,
'b': ctypes.c_byte, 'B': ctypes.c_ubyte,
'h': ctypes.c_short, 'H': ctypes.c_ushort,
'i': ctypes.c_int, 'I': ctypes.c_uint, (其u表示无符号)
'l': ctypes.c_long, 'L': ctypes.c_ulong,
'f': ctypes.c_float, 'd': ctypes.c_double
from multiprocessing import Process, Value, Array
def func(n, m1, m2):
n.value = 888
m1.value = 'a'.encode('utf-8')
m2.value = "武"
if __name__ == '__main__':
num = Value('i', 666)
v1 = Value('c')
v2 = Value('u')
p = Process(target=func, args=(num, v1, v2))
p.start()
p.join()
print(num.value) # 888
print(v1.value) # a
print(v2.value) # 武
from multiprocessing import Process, Value, Array
def f(data_array):
data_array[0] = 666
if __name__ == '__main__':
arr = Array('i', [11, 22, 33, 44]) # 数组:元素类型必须是int; 只能是这么几个数据。
p = Process(target=f, args=(arr,))
p.start()
p.join()
print(arr[:])
Server process
A manager object returned by Manager()
controls a server process which holds Python objects and allows other processes to manipulate them using proxies.
from multiprocessing import Process, Manager
def f(d, l):
d[1] = '1'
d['2'] = 2
d[0.25] = None
l.append(666)
if __name__ == '__main__':
with Manager() as manager:
d = manager.dict()
l = manager.list()
p = Process(target=f, args=(d, l))
p.start()
p.join()
print(d)
print(l)
9.3 交换
multiprocessing
supports two types of communication channel between processes
Queues
The Queue
class is a near clone of queue.Queue
. For example
import multiprocessing
def task(q):
for i in range(10):
q.put(i)
if __name__ == '__main__':
queue = multiprocessing.Queue()
p = multiprocessing.Process(target=task, args=(queue,))
p.start()
p.join()
print("主进程")
print(queue.get())
print(queue.get())
print(queue.get())
print(queue.get())
print(queue.get())
Pipes
The Pipe()
function returns a pair of connection objects connected by a pipe which by default is duplex (two-way). For example:
import time
import multiprocessing
def task(conn):
time.sleep(1)
conn.send([111, 22, 33, 44])
data = conn.recv() # 阻塞
print("子进程接收:", data)
time.sleep(2)
if __name__ == '__main__':
parent_conn, child_conn = multiprocessing.Pipe()
p = multiprocessing.Process(target=task, args=(child_conn,))
p.start()
info = parent_conn.recv() # 阻塞
print("主进程接收:", info)
parent_conn.send(666)
9.4 进程锁
import time
import multiprocessing
import os
def task(lock):
print("开始")
lock.acquire()
# 假设文件中保存的内容就是一个值:10
with open('f1.txt', mode='r', encoding='utf-8') as f:
current_num = int(f.read())
print(os.getpid(), "排队抢票了")
time.sleep(0.5)
current_num -= 1
with open('f1.txt', mode='w', encoding='utf-8') as f:
f.write(str(current_num))
lock.release()
if __name__ == '__main__':
multiprocessing.set_start_method("spawn")
lock = multiprocessing.RLock()
process_list = []
for i in range(10):
p = multiprocessing.Process(target=task, args=(lock,))
p.start()
process_list.append(p)
# spawn模式,需要特殊处理。
for item in process_list:
item.join()
9.5 进程池
进程回调函数跟线程不同,进程回调是由主进程调用的
import time
from concurrent.futures import ProcessPoolExecutor
def task(num):
print("执行", num)
time.sleep(2)
if __name__ == '__main__':
pool = ProcessPoolExecutor(4)
for i in range(10):
pool.submit(task, i)
# 等待进程池中的任务都执行完毕后,再继续往后执行。
pool.shutdown(True)
print(1)
import time
from concurrent.futures import ProcessPoolExecutor
import multiprocessing
def task(num):
print("执行", num)
time.sleep(2)
return num
def done(res):
print(multiprocessing.current_process())
time.sleep(1)
print(res.result())
time.sleep(1)
if __name__ == '__main__':
pool = ProcessPoolExecutor(4)
for i in range(50):
fur = pool.submit(task, i)
fur.add_done_callback(done) # done的调用由主进程处理(与线程池不同)
print(multiprocessing.current_process())
pool.shutdown(True)
注意:如果在进程池中要使用进程锁,则需要基于Manager中的Lock和RLock来实现。
import time
import multiprocessing
from concurrent.futures.process import ProcessPoolExecutor
def task(lock):
print("开始")
with lock:
# 假设文件中保存的内容就是一个值:10
with open('f1.txt', mode='r', encoding='utf-8') as f:
current_num = int(f.read())
print("排队抢票了")
time.sleep(1)
current_num -= 1
with open('f1.txt', mode='w', encoding='utf-8') as f:
f.write(str(current_num))
if __name__ == '__main__':
pool = ProcessPoolExecutor()
# lock_object = multiprocessing.RLock() # 不能使用
manager = multiprocessing.Manager()
lock_object = manager.RLock() # Lock
for i in range(10):
pool.submit(task, lock_object)
10 协程
计算机中提供了:线程、进程 用于实现并发编程(真实存在)。
协程(Coroutine),是程序员通过代码搞出来的一个东西(非真实存在)。
协程也可以被称为微线程,是一种用户态内的上下文切换技术。
简而言之,其实就是通过一个线程实现代码块相互切换执行(来回跳着执行)。
协程、线程、进程的区别?
线程,是计算机中可以被cpu调度的最小单元。
进程,是计算机资源分配的最小单元(进程为线程提供资源)。
一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。
由于CPython中GIL的存在:
- 线程,适用于IO密集型操作。
- 进程,适用于计算密集型操作。
协程,协程也可以被称为微线程,是一种用户态内的上下文切换技术,在开发中结合遇到IO自动切换,就可以通过一个线程实现并发操作。
可以实现在线程中遇到IO操作时,自动在代码块中相互切换执行
所以,在处理IO操作时,协程比线程更加节省开销(协程的开发难度大一些)。
11 总结
案例:多线程socket服务端
-
服务端
import socket import threading def task(conn): while True: client_data = conn.recv(1024) data = client_data.decode('utf-8') print("收到客户端发来的消息:", data) if data.upper() == "Q": break conn.sendall("收到收到".encode('utf-8')) conn.close() def run(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 8001)) sock.listen(5) while True: # 等待客户端来连接(主线程) conn, addr = sock.accept() # 创建子线程 t = threading.Thread(target=task, args=(conn,)) t.start() sock.close() if __name__ == '__main__': run()
-
客户端
import socket # 1. 向指定IP发送连接请求 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('127.0.0.1', 8001)) while True: txt = input(">>>") client.sendall(txt.encode('utf-8')) if txt.upper() == 'Q': break reply = client.recv(1024) print(reply.decode("utf-8")) # 关闭连接,关闭连接时会向服务端发送空数据。 client.close()
11.2 并发和并行
-
串行,多个任务排队按照先后顺序逐一去执行。
-
并发,假设有多个任务,只有一个CPU,那么在同一时刻只能处理一个任务,为了避免串行,可以让将任务切换运行(每个任务运行一点,然后再切换),达到并发效果(看似都在同时运行)。
并发在Python代码中体现:协程、多线程(由于CPython的GIL锁限制,多个线程无法被CPU调度)。
-
并行,假设有多个任务,有多个CPU,那么同一时刻每个CPU都是执行一个任务,任务就可以真正的同时运行。
并行在Python代码中的体现:多进程。
11.3 单例模式
在python开发和源码中关于单例模式有两种最常见的编写方式,分别是:
-
基于
__new__
方法实现import threading import time class Singleton: instance = None lock = threading.RLock() def __init__(self): self.name = "武沛齐" def __new__(cls, *args, **kwargs): if cls.instance: return cls.instance with cls.lock: if cls.instance: return cls.instance cls.instance = object.__new__(cls) return cls.instance obj1 = Singleton() obj2 = Singleton() print(obj1 is obj2) # True
-
基于模块导入方式
# utils.py class Singleton: def __init__(self): self.name = "武沛齐" ... single = Singleton()
from xx import single print(single) from xx import single print(single)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App