几个问题
1、python多线程下,局部变量、全局变量都有安全风险吗?
一个线程的局部变量只有线程自己能看见,不会影响其他线程。而全局变量是所有线程共享的,修改时要注意线程安全问题。
2、如果全局变量不直接写在文件,而是写在文件的类里,还有风险么?
在多线程安全问题上,类内的方法和类外的函数并没有本质的区别。线程安全问题主要出现在多个线程同时访问和修改同一份数据时,可能会导致数据的不一致或者其他预期之外的结果。
- 【线程安全】:如果一个 "类内"的方法 或 "类外"的函数 访问和修改的都是局部变量,那么它们在多线程环境下都是线程安全的。因为每个线程都有自己的执行环境,局部变量存储在这个执行环境中,因此各个线程之间的局部变量是互不干扰的。
- 【线程风险】:如果一个 "类内"的方法 访问和修改的是实例变量,那么这个方法在多线程环境下可能是不安全的,因为实例变量是在堆内存中存储的,是可以被多个线程共享的。
案例:
def processing_data(task_id, collection_name):
'''
写在类外面,多线程下,每个线程独立拥有该方法,所以是线程安全。
:param task_id:
:param collection_name:
:return:
'''
print(task_id + collection_name)
if collection_name == "a":
time.sleep(10)
print(task_id + collection_name)
class LabelService:
def create_thread(self, task_id, collection_name_list):
"""
:param task_id: 任务id
:param collection_name_list: 集合名称列表
"""
# 多线程处理数据
for collection_name in collection_name_list:
# 创建线程
thread_processing_data = threading.Thread(
# target=self.processing_data1, # todo 方法内的变量是实例变量,调用同一实例的同一方法,方法内的变量会被多线程篡改,不安全!
target=LabelService().processing_data1, # todo 方法内的变量是实例变量,调用不同实例的同一方法,不同实例间相互隔离,线程安全~
# target=self.processing_data2, # todo 方法内的变量是局部变量,多个线程同时调用同一实例的同一方法,该方法内的局部变量也是相互隔离的,线程安全~
# target=self.processing_data3, # todo 函数内的变量是局部变量,多个线程同时调用同一类外函数,该函数内的局部变量也是相互隔离的,线程安全~
args=(task_id, collection_name))
thread_processing_data.name = task_id + "data_push_" + collection_name
# 启动线程
thread_processing_data.start()
print("创建线程:[" + thread_processing_data.name + "]" + str(thread_processing_data.is_alive()))
def processing_data2(self, task_id, collection_name):
print(task_id + collection_name)
if collection_name == "a":
time.sleep(10)
print(task_id + collection_name)
def processing_data1(self, task_id, collection_name):
self.task_id = task_id
self.collection_name = collection_name
print(self.task_id + self.collection_name)
if collection_name == "a":
time.sleep(10)
print(self.task_id + self.collection_name)
LabelService().create_thread("qqqqqqqqqqqqqq", ["a", "b"])
3、锁和threadlocal哪个好?
锁和threadLocal的适用场景不同。
作用对象:全局变量
适用场景:
- 锁:某个变量需要被多个线程共享他的最新值,就要用锁来限制多线程间的动作。
- threadLocal:某个变量需要在每个线程里各自维护一份不同的数据。
(1)举例:
比如一个班级(某web项目)里有30个学生(并发执行30个线程),那"班级名"在全班都是公开的,"家庭住址"是需要每个学生时刻记住的(比如:回家、填报个人信息等动作中都要用到)。所以"班级名"、"家庭住址"都是全局变量,只不过前者value 是整个web共享 ,后者value 是每个线程各自维护一份、并且在各自存活过程中一直存在 。
如果班级名改了,那班里30人的班级名都跟着变;
如果某个学生的"家庭住址"改了,不会影响其他学生,也不用通知其他学生。
所以:
- "班级名"的修改需要加锁,这样就不会部分同学知道班级改名了,部分同学不知道,当别人问你们是哪个班时,就不会出现多个答案冲突的情况。
- "家庭住址"可以放到threadLocal中,只用修改threadLocal中的该变量即可。
(2)案例
① ThreadLocal
【反例】全局变量:多线程下不安全
import time, threading
balance = 0
def change_it(n):
global balance
balance = balance + n
print(threading.current_thread().name, " +++++:", str(balance))
balance = balance - n
print(threading.current_thread().name, "-----:", str(balance))
def run_thread(n):
for i in range(100):
change_it(n)
t1 = threading.Thread(target = run_thread , args = (5,))
t2 = threading.Thread(target = run_thread ,args = (8,))
t1.start()
t2.start()
'''
thread.join()方法的作用是阻塞"执行xx.join()"这行代码的线程:在这里就是主线程(如果实在t2线程执行这行代码,就是t2要等待t1执行完),使主线程在该方法被调用的子线程结束之前等待。
t1.join()使主线程等待t1线程结束,t2.join()使主线程等待t2线程结束。
如果你不写thread.join(),那么主线程和子线程将会并发执行,也就是说,主线程不会等待子线程结束就会继续执行。这种情况下,主线程可能在子线程之前就结束,此时子线程可能还没有执行完。这种情况下,由于主线程(执行`print(balance))已经结束,程序也就结束了,因此你可能看不到子线程的执行结果。
'''
t1.join()
t2.join()
print(balance)
【正例】全局变量:使用threadLocal
import threading
import time
def set_telephone(telephone):
local.telephone = telephone
print(threading.current_thread().name + " 放入的手机是", local.telephone + "\n")
time.sleep(1)
get_telephone()
def get_telephone():
print(threading.current_thread().name + " 取出的手机是", local.telephone + "\n")
if __name__ == '__main__':
local = threading.local()
for i in range(3):
thread = threading.Thread(target=set_telephone, name='学生' + str(i), args=('手机' + str(i),))
thread.start()
② 锁
python多线程中的锁你知道几种?
对死锁的理解
死锁核心思想
【反例】死锁
死锁的条件:
(1)同时存在两把锁
(2)锁的获取是异步的
(3)一个线程 抢到一把锁之后还要去抢第二把锁
(4)一个线程抢到一把锁,另一个线程抢到另一把锁
###### 死锁现象
import time
from threading import Thread, Lock
noodle_lock = Lock() # 面锁
fork_lock = Lock() # 叉子锁
def eat1(name):
noodle_lock.acquire()
print('%s拿到面条了' % name)
fork_lock.acquire()
print('%s拿到叉子了' % name)
print('%s吃面' % name)
time.sleep(3)
fork_lock.release()
print('%s放下叉子' % name)
noodle_lock.release()
print('%s放下面' % name)
def eat2(name):
fork_lock.acquire()
print('%s拿到叉子了' % name)
noodle_lock.acquire()
print('%s拿到面条了' % name)
print('%s吃面' % name)
time.sleep(3)
noodle_lock.release()
print('%s放下面' % name)
fork_lock.release()
print('%s放下叉子' % name)
if __name__ == '__main__':
name_list1 = ['zhangsan', 'lisi']
name_list2 = ['wangwu', 'zhaoliu']
for name in name_list1:
Thread(target=eat1, args=(name,)).start()
for name in name_list2:
Thread(target=eat2, args=(name,)).start()
本例中,可能会出现:线程 lisi 先调用eat1()拿到了noodle_lock, 同时 线程wangwu 调用eat2()拿到了fork_lock。
随后,在 线程 lisi 希望拿到 fork_lock才会释放noodle_lock,线程wangwu 希望拿到 noodle_lock才会释放fork_lock。大家互不相让,形成死锁。
【正例】可重入锁(递归锁)
改造 :使用 可重入锁(递归锁)解决死锁问题。
可重入锁,是一种特殊的线程锁,允许同一个线程在没有释放自己已经获得的锁的情况下,再次获得自己已经获得的那个锁。
全局只有一把锁 ,而且是可重入锁(递归锁)!
这时如果一个线程获取了锁,另一个线程就被阻塞,直到对方释放锁。
import time
from threading import Thread, Lock, RLock
fork_lock = noodle_lock = RLock()
def eat1(name):
noodle_lock.acquire()
print('%s拿到面条了' % name)
fork_lock.acquire()
print('%s拿到叉子了' % name)
print('%s吃面' % name)
time.sleep(3)
fork_lock.release()
print('%s放下叉子' % name)
noodle_lock.release()
print('%s放下面' % name)
def eat2(name):
fork_lock.acquire()
print('%s拿到叉子了' % name)
noodle_lock.acquire()
print('%s拿到面条了' % name)
print('%s吃面' % name)
time.sleep(3)
noodle_lock.release()
print('%s放下面' % name)
fork_lock.release()
print('%s放下叉子' % name)
if __name__ == '__main__':
name_list1 = ['AA', 'BB', 'CC']
name_list2 = ['大锤', '茉莉', "云朵"]
for name in name_list1:
Thread(target=eat1, args=(name, )).start()
for name in name_list2:
Thread(target=eat2, args=(name, )).start()