1. 按值调用和按引用调用基础
- 按值调用:函数接收的是参数值的副本,对副本的修改不会影响原始变量。
- 按引用调用:函数接收的是参数的内存地址,对参数的修改会直接影响原始变量。
2. 多线程环境下问题
线程安全问题
- 按值调用:一般情况下,由于操作的是副本,不会直接影响共享数据,线程安全问题相对较少。但如果副本数据较大,复制过程可能消耗较多资源。
- 按引用调用:直接操作共享数据,容易出现多个线程同时修改同一数据的情况,导致数据不一致,如经典的银行转账问题。
资源竞争问题
- 按值调用:如果涉及资源(如文件、数据库连接)的副本创建,可能导致资源竞争,比如过多副本占用过多资源。
- 按引用调用:当多个线程通过引用访问和修改共享资源时,竞争更为直接,如多个线程同时写入同一文件。
3. 解决手段
锁机制
- 互斥锁(Mutex):用于保护共享资源,同一时间只允许一个线程访问。在按引用调用时,对共享数据操作前后加锁。
import threading
lock = threading.Lock()
shared_variable = 0
def increment():
global shared_variable
lock.acquire()
shared_variable += 1
lock.release()
- 读写锁(RLock):允许多个线程同时读共享资源,但写操作时需要独占。适用于读多写少的场景。
import threading
rlock = threading.RLock()
data = []
def read_data():
rlock.acquire()
try:
# 读取数据操作
pass
finally:
rlock.release()
def write_data():
rlock.acquire()
try:
# 写入数据操作
pass
finally:
rlock.release()
信号量
- Semaphore:控制同时访问资源的线程数量。比如限制同时访问文件的线程数。
import threading
semaphore = threading.Semaphore(3) # 允许最多3个线程同时访问
def access_file():
semaphore.acquire()
try:
# 文件访问操作
pass
finally:
semaphore.release()
4. 选择策略及场景
按值调用优先场景
- 数据独立性要求高:如并行计算场景,每个线程独立处理数据,不需要共享数据的修改反馈。
import concurrent.futures
def square(x):
return x * x
with concurrent.futures.ThreadPoolExecutor() as executor:
results = list(executor.map(square, [1, 2, 3, 4, 5]))
- 数据量小且操作简单:复制开销小,且能避免共享数据带来的复杂性。
按引用调用优先场景
- 共享数据频繁更新:如实时统计系统,多个线程需要不断更新共享的统计数据。
import threading
counter = 0
lock = threading.Lock()
def update_counter():
global counter
lock.acquire()
counter += 1
lock.release()
- 资源共享且需要协调:如多线程访问数据库连接池,通过引用共享连接资源并合理分配。
5. 性能分析
- 按值调用:在数据复制开销小且线程安全需求不高时性能较好。但大数据量复制会带来额外开销。
- 按引用调用:避免数据复制开销,但锁机制可能导致线程等待,在高并发写操作时性能下降。合理使用锁和信号量,优化锁粒度和使用场景,可平衡性能。如采用读写锁优化读多写少场景,减少线程等待时间。