1. 前言
在进行python多线程编程时,我们经常会遇到需要共享变量的场景。共享变量是指多个线程可以同时访问和修改的变量。但是,多线程同时访问和修改同一个变量会导致数据不一致的问题,为此我们需要使用一些方法保证多线程操作共享变量的正确性和效率。
2. 共享变量的使用方法
共享变量的使用方法有多种,比较常见的有使用锁、使用信号量和使用互斥量。
2.1 使用锁
锁是一种用于保护共享资源不被并发访问和修改的机制。在python中使用锁可以使用threading模块中的Lock类。使用方法如下:
from threading import Thread, Lock
# 定义一个共享变量
count = 0
# 定义一个锁对象
lock = Lock()
def modify_count():
global count
for i in range(100000):
# 先获取锁
lock.acquire()
count += 1
# 释放锁
lock.release()
t1 = Thread(target=modify_count)
t2 = Thread(target=modify_count)
t1.start()
t2.start()
t1.join()
t2.join()
print(count)
以上代码中,我们首先定义了一个共享变量count,然后定义了一个Lock对象lock。在修改count之前先获取lock的锁,修改count之后再释放锁。这样就保证了多线程同时访问修改count时的正确性。
2.2 使用信号量
信号量是一种常用的系统同步工具,它通过控制对资源的访问量来实现对共享资源的保护。在python中使用信号量可以使用threading模块中的Semaphore类。使用方法如下:
from threading import Thread, Semaphore
# 定义一个共享变量
count = 0
# 定义一个信号量对象,初始值为1
sem = Semaphore(1)
def modify_count():
global count
for i in range(100000):
# 获取信号量
sem.acquire()
count += 1
# 释放信号量
sem.release()
t1 = Thread(target=modify_count)
t2 = Thread(target=modify_count)
t1.start()
t2.start()
t1.join()
t2.join()
print(count)
以上代码中,我们首先定义了一个共享变量count,然后定义了一个Semaphore对象sem,初始值为1。在修改count之前先获取sem的信号量,修改count之后再释放信号量。这样就保证了多线程同时访问修改count时的正确性。
2.3 使用互斥量
互斥量是一种特殊的锁,可以用来保护共享资源不被并发访问和修改。在python中使用互斥量可以使用threading模块中的RLock类。与Lock类不同的是,RLock可以多次acquire()锁,但必须多次释放,这使得同一个线程在使用共享资源时不会死锁。使用方法如下:
from threading import Thread, RLock
# 定义一个共享变量
count = 0
# 定义一个互斥锁对象
mutex = RLock()
def modify_count():
global count
for i in range(100000):
# 先获取锁
mutex.acquire()
count += 1
# 释放锁
mutex.release()
t1 = Thread(target=modify_count)
t2 = Thread(target=modify_count)
t1.start()
t2.start()
t1.join()
t2.join()
print(count)
以上代码中,我们首先定义了一个共享变量count,然后定义了一个RLock对象mutex。在修改count之前先获取mutex的锁,修改count之后再释放锁。这样就保证了多线程同时访问修改count时的正确性。
3. 共享变量的效率问题
共享变量的使用虽然能够保证多线程操作共享变量的正确性,但是由于多线程之间需要获取锁或信号量,会造成一定的性能损失。
3.1 共享变量的效率测试
我们可以通过对比使用共享变量和不使用共享变量的效率来测试共享变量的性能影响。以下代码演示了对一个数组进行累加操作的效率比较:
import time
from threading import Thread, Lock
# 定义一个全局变量
nums = [i for i in range(10000000)]
def accumulate_no_lock(nums):
total = 0
for num in nums:
total += num
return total
def accumulate_with_lock(nums):
total = 0
lock = Lock()
for num in nums:
lock.acquire()
total += num
lock.release()
return total
start = time.time()
total_no_lock = accumulate_no_lock(nums)
end = time.time()
print(f"不使用锁的累加结果:{total_no_lock},用时:{end-start:.4f}")
start = time.time()
total_with_lock = accumulate_with_lock(nums)
end = time.time()
print(f"使用锁的累加结果:{total_with_lock},用时:{end-start:.4f}")
运行以上代码,我们会发现使用锁的累加方法用时比不使用锁的累加方法长得多,因为在累加时需要获取锁的、释放锁的时间会占用一定的时间。
3.2 锁的粒度问题
锁的粒度指的是多个线程使用同一个锁时的操作范围,锁的粒度越小,多个线程间的并发性就越高,但是锁的数量也就越多,也就更容易导致死锁和性能问题。
以下代码演示了一个在锁的粒度为行级别和单元格级别下,对一个表格进行累加操作的性能比较:
import time
from threading import Thread, Lock
table = []
for i in range(10):
row = [j for j in range(i*10, i*10+10)]
table.append(row)
def accumulate_total_row_lock():
lock = Lock()
total = 0
for i in range(10):
lock.acquire()
total += sum(table[i])
lock.release()
return total
def accumulate_total_cell_lock():
total = 0
for i in range(10):
lock = Lock()
for j in range(10):
lock.acquire()
total += table[i][j]
lock.release()
return total
start = time.time()
total_row_lock = accumulate_total_row_lock()
end = time.time()
print(f"行级别锁下的累加结果:{total_row_lock},用时:{end-start:.4f}")
start = time.time()
total_cell_lock = accumulate_total_cell_lock()
end = time.time()
print(f"单元格级别锁下的累加结果:{total_cell_lock},用时:{end-start:.4f}")
运行以上代码,我们会发现行级别锁下的累加方法用时比单元格级别锁下的累加方法更短,这是因为锁的粒度更小,允许较多的线程并发操作。
4. 总结
共享变量在多线程编程中非常重要,但是使用不当会导致数据不一致和性能问题。我们可以使用锁、信号量和互斥量等机制来保证共享变量的正确性。同时,考虑锁的粒度问题,选择合适的锁粒度可以提高并发效率。
另外,一般情况下可以指定temperature为合适的值,可以对训练中我们输出的模型performance进行优化。我们可以根据自己的需求进行设定,建议在每一轮训练中对temperature进行调整。例如:
temperature=0.6