线程
线程的介绍
在Python中,想要实现多任务除了使用进程,还可以使用线程來完成,线程是实现多任务的另外一种方式。
线程的概念
线程是进程中执行代码的一个分支,每个执行分支(线程)要想工作执行代码需要cpu进行调度,也就是说线程是cpu调度的基本单位,每个进程至少都有一个线程,而这个线程就是我们通常说的主线程。
线程的作用
多线程可以完成多任务
线程实现多任务
# 导包
import threading
import time
def sing():
for i in range(5):
print('正在唱歌中....')
time.sleep(0.1) # 让进程手动失去CPU资源 让进程休眠0.1秒,今日阻塞态,失去cpu
def dance():
for i in range(5):
print('正在跳舞中.....')
time.sleep(0.1)
if __name__ == '__main__':
# 创建线程对象
thread_sing = threading.Thread(target=sing)
thread_dance = threading.Thread(target=dance)
# 启动线程
thread_sing.start()
thread_dance.start()
线程传参
# 导包
import threading
import time
def sing(singer, song):
for i in range(5):
print(f'{singer}正在唱歌{song}中....')
time.sleep(0.1) # 让进程手动失去CPU资源 让进程休眠0.1秒,今日阻塞态,失去cpu
def dance(dancer, name):
for i in range(5):
print(f'{dancer}正在跳{name}舞')
time.sleep(0.1)
if __name__ == '__main__':
# 创建线程对象
thread_sing = threading.Thread(target=sing, args=('张学友', '野猫之恋'))
thread_dance = threading.Thread(target=dance, kwargs={'dancer': '梁咏琪', 'name': '胆小鬼'})
# 启动线程
thread_sing.start()
thread_dance.start()
线程的执行是随机的
# 导包
import threading
import time
def func():
time.sleep(1)
print('当前线程为:', threading.current_thread().name)
if __name__ == '__main__':
# 创建10个线程
for i in range(10):
sub = threading.Thread(target=func)
sub.start()
主进程会等待子进程结束再结束
# 导包
import threading
import time
def func():
for i in range(5):
print('子线程代码结束')
time.sleep(0.5)
if __name__ == '__main__':
# 想让子线程随着主线程的结束而结束,可以将子线程设置为 daemon 线程
sub = threading.Thread(target=func, daemon=True)
# 第二种方法
# sub.daemon = True
# 第三种方法
# sub.SetDaemon(True)
sub.start()
time.sleep(1)
print('主线程代码结束')
线程共享全局变量问题
# 导包
import threading
import time
g_list = []
def add_data():
for i in range(5):
g_list.append(i)
print('添加数据', i)
time.sleep(0.1)
# 代码出了 for 循环,代表数据添加完成
print('add_data', g_list)
# 定义任务函数,读取全局变量的值
def read():
print('read', g_list)
if __name__ == '__main__':
# 想让子线程随着主线程的结束而结束,可以将子线程设置为 daemon 线程
thread_add = threading.Thread(target=add_data)
thread_read = threading.Thread(target=read)
thread_add.start()
thread_add.join()
thread_read.start()
# 线程共享全局变量出现的问题

线程阻塞解决资源竞争问题
# 导包
import threading
import time
g_num = 0
def task():
# 修改全局变量的值需要使用 global 声明
global g_num
for i in range(1000000):
g_num += 1
print(threading.current_thread().name, g_num)
if __name__ == '__main__':
thread_1 = threading.Thread(target=task)
thread_2 = threading.Thread(target=task)
thread_1.start()
# 解决方案:让线程一先执行结束后在执行
thread_1.join()
thread_2.start()
互斥锁
互斥锁的概念
互斥锁:对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
注意:
- 互斥锁是多个
线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁。
- 互斥锁是多个
互斥锁的使用
threading模块中定义了Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。
```python
导包
import threading import time
加锁
mutex = threading.Lock() g_num = 0
def task():
# 修改全局变量的值需要使用 global 声明
global g_num
for i in range(1000000):
# 上锁
mutex.acquire()
g_num += 1
# 解锁
mutex.release()
print(threading.current_thread().name, g_num)
if name == 'main': thread_1 = threading.Thread(target=task) thread_2 = threading.Thread(target=task) thread_1.start()
# 解决方案:让线程一先执行结束后在执行
# thread_1.join() # 线程等待
# 第二种 互斥锁
thread_2.start()
# 导包
import threading import time
mutex = threading.Lock() g_num = 0
def task():
# 修改全局变量的值需要使用 global 声明
global g_num
# 上锁
mutex.acquire()
for i in range(1000000):
g_num += 1
# 解锁
mutex.release()
print(threading.current_thread().name, g_num)
if name == 'main': thread_1 = threading.Thread(target=task) thread_2 = threading.Thread(target=task) thread_1.start()
# 解决方案:让线程一先执行结束后在执行
# thread_1.join() # 线程等待
# 第二种 互斥锁
thread_2.start()
这两种锁都可以
3. 死锁
死锁:一直等待对方释放锁的情景就是死锁
会造成应用程序的停止响应,不能再处理其它任务了。
```python
# 导包
import threading
import time
mutex = threading.Lock()
g_list = [1, 2, 3, 4, 5]
def func(index):
mutex.acquire()
if index >= len(g_list):
print(threading.current_thread().name, '下标越界')
mutex.release() # 在锁的时候释放锁
return
print(threading.current_thread().name, g_list[index])
mutex.release()
if __name__ == '__main__':
for i in range(10):
sub = threading.Thread(target=func, args=(i,))
sub.start()
区别对比
- 进程之间不共享全局变量
- 线程之间共享全局变量,但是要注意资源竟争的问题,解决办法:互斥锁或者线程同步
- 创建进程的资源开销要比创建线程的资源开销要大
- 进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位
- 线程不能够独立执行,必须依存在进程中
- 多进程开发比单进程多线程开发稳定性要强
优缺点对比
- 进程的优缺点:
- 优点:可以用多核
- 缺点:资源开销大
- 线程优缺点:
- 优点:资源开销小
- 缺点:不能使用多核
GIL锁
GIL 全局解释器锁
GIL 保证同一时间,只有一个线程使用 CPU。
一个进程有一个 GIL锁。
GIL 不是 Python 的特性,只是 cPython 解释器的概念。历史遗留问题。
GIL锁什么时候释放?
- 在当前线程执行超时后会自动释放
- 在当前线程执行阻塞操作时会自动释放(input, io/输入输出)
- 当前执行完成时
G儿L的弊端
- GIL对计算密集型的程序会产生影响。因为计算密集型的程序,需要占用系统资源。
- GIL的存在,相当于始终在进行单线程运算,这样自然就慢了。
- IO密集型影响不大的原因在于,IO,input/output,这两个词就表明程序的瓶颈在于输入输出所耗费的时间,线程大部分时间在等待,所以它们是多个一起等(多线程)还是单个等(单线程)无所谓的。
解决方案:
要提升多线程执行效率,解决方案:
- 更换解释器
- 改为多进程替换多线程
- 子线程使用C语言实现(绕过GIL锁)
必须要知道的是:
- CPU 密集型不太适合多线程
- I/O 密集型适合多线程 (Gil锁会释放)爬虫程序