【多任务编程】线程
前言
本篇文章将介绍多任务编程中的线程,多线程的使用,线程执行带有参数的任务,线程注意点,互斥锁,死锁。
一、线程
1.线程的介绍
在Python中,想要实现多任务除了使用进程,还可以使用线程来完成,线程是实现多任务的另外一种方式。
线程是进程中执行代码的一个分支,每个执行分支(线程)要想工作执行代码需要cpu进行调度 ,也就是说线程是cpu调度的基本单位,每个进程至少都有一个线程,而这个线程就是我们通常说的主线程。
2.线程的作用
二、多线程的使用
#导入线程模块
import threading
Thread([group [, target [, name [, args [, kwargs]]]]])
group: 线程组,目前只能使用None
target: 执行的目标任务名
args: 以元组的方式给执行任务传参
kwargs: 以字典方式给执行任务传参
name: 线程名,一般不用设置
# 1。导入线程模块
import threading
import time
def sing():
# 获取当前线程
current_thread = threading.current_thread()
print('sing:',current_thread)
for i in range(3):
print('唱歌中...')
time.sleep(0.2)
def dance():
# 获取当前线程
current_thread = threading.current_thread()
print('sing:', current_thread)
for i in range(3):
print('跳舞中...')
time.sleep(0.2)
if __name__ == '__main__':
# 获取当前线程
current_thread = threading.current_thread()
print('main_thread:', current_thread)
# 2.创建子线程
sing_thread = threading.Thread(target=sing)
dance_thread = threading.Thread(target=dance)
# 3.启动子线程执行对应的任务
sing_thread.start()
dance_thread.start()
总结:
1.导入线程模块
import threading
2.创建子线程并指定执行的任务
sub_thread = threading.Thread(target=任务名)
3.启动线程执行任务
sub_thread.start()
三、线程执行带有参数的任务
前面我们使用线程执行的任务是没有参数的,假如我们使用线程执行的任务带有参数,如何给函数传参呢?
Thread类执行任务并给任务传参数有两种方式:
- args 表示以元组的方式给执行任务传参
- kwargs 表示以字典方式给执行任务传参
import threading
def show_info(name, age):
print('name: %s , age: %d' % (name, age))
if __name__ == '__main__':
# 创建子线程
# 以元组方式传参, 要保证元粗的顺序和函数的参数顺序一致
# sub_thread = threading.Thread(target=show_info, args=('李四', 18))
# # 启动子线程
# sub_thread.start()
# 以字典的方式传参, 要保证字典里面的key和函数的参数名保持一致
sub_thread = threading.Thread(target=show_info, kwargs={'name': '王五', 'age': 19})
sub_thread.start()
四、线程注意点
- 1.线程之间执行是无序的
import threading
import time
def task():
time.sleep(1)
# 获取当前线程
print(threading.current_thread())
if __name__ == '__main__':
# 循环创建大量线程,测试线程之间执行是否无序
for i in range(20):
# 每循环一次创建一个子线程
sub_thread = threading.Thread(target=task)
# 启动子线程执行对应的任务
sub_thread.start()
# 线程之间执行是无序的,具体哪个线程执行是由cpu调度决定的
运行结果:
说明:
线程之间执行是无序的,它是由cpu调度决定的 ,cpu调度哪个线程,哪个线程就先执行,没有调度的线程不能执行。
进程之间执行也是无序的,它是由操作系统调度决定的,操作系统调度哪个进程,哪个进程就先执行,没有调度的进程不能执行。
import multiprocessing
import time
def task():
time.sleep(1)
print(multiprocessing.current_process())
if __name__ == '__main__':
for i in range(20):
sub_process = multiprocessing.Process(target=task)
sub_process.start()
# 结论: 进程之间执行也是无序的,是由操作系统调度进程来决定的
运行结果:
- 2.主线程会等待所有的子线程执行结束再结束
import threading
import time
def task():
while True:
print('任务执行中...')
time.sleep(0.3)
if __name__ == '__main__':
# 创建子线程
# daemon=True 表示创建的子线程守护主线程,主线程退出子线程直接销毁
# sub_thread = threading.Thread(target=task,deamon=True)
sub_thread = threading.Thread(target=task)
# 把子线程设置成为主线程
sub_thread.setDaemon(True)
sub_thread.start()
# 主线程延时执行1s
time.sleep(1)
print('over')
# 结论: 主线程会等待子线程执行结束再结束
# 解决办法: 把子线程设置成为守护主线程即可
我们可以设置守护主线程
守护主线程:
守护主线程就是主线程退出子线程销毁不再执行
设置守护主线程有两种方式:
threading.Thread(target=show_info, daemon=True)
线程对象.setDaemon(True)
- 3.线程之间共享全局变量
import threading
import time
# 定义全局变量
g_list = list()
print(g_list)
# 添加数据的任务
def add_data():
for i in range(3):
# 每次循环把数据添加到全局变量
g_list.append(i)
print('add:', i)
time.sleep(0.3)
# 代码执行到此,说明添加数据完成
print('添加数据完成:',g_list)
# 读取数据的任务
def read_data():
print("read:", g_list)
if __name__ == '__main__':
# 创建添加数据的子线程
add_thread = threading.Thread(target=add_data)
# 创建读取数据的子进程
read_thread = threading.Thread(target=read_data)
# 启动线程执行对应的任务
add_thread.start()
# time.sleep(1)
# 让当前线程(主线程)等待添加数据的子线程执行完成以后代码在继续执行
add_thread.join()
read_thread.start()
# 结论: 线程之间共享全局变量
- 4.线程之间共享全局变量数据出现错误问题
需求:
定义两个函数,实现循环100万次,每循环一次给全局变量加1
创建两个子线程执行对应的两个函数,查看计算后的结果
import threading
# 全局变量
g_num = 0
# 循环100万次执行的任务
def task1():
global g_num
for i in range(1000000):
# 每循环一次给全局变量+1
g_num = g_num + 1
# 代码执行到此,说明数据计算完成
print('task1:', g_num)
# 循环100万次执行的任务
def task2():
global g_num
for i in range(1000000):
# 每循环一次给全局变量+1
g_num = g_num + 1
# 代码执行到此,说明数据计算完成
print('task2:', g_num)
if __name__ == '__main__':
first_thread = threading.Thread(target=task1)
second_thread = threading.Thread(target=task2)
first_thread.start()
# first_thread.join()
second_thread.start()
运行结果:
全局变量数据错误的解决办法:
线程同步: 保证同一时刻只能有一个线程去操作全局变量 同步: 就是协同步调,按预定的先后次序进行运行。
线程同步的方式:
1.线程等待(join)
2.互斥锁
小结:
线程执行执行是无序的
主线程默认会等待所有子线程执行结束再结束,设置守护主线程的目的是主线程退出子线程销毁。
线程之间共享全局变量,好处是可以对全局变量的数据进行共享。
线程之间共享全局变量可能会导致数据出现错误问题,可以使用线程同步方式来解决这个问题。线程等待(join)
五、互斥锁
互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
注意:
互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁。
threading模块中定义了Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。
互斥锁使用步骤:
#创建锁
mutex = threading.Lock()
#上锁
mutex.acquire()
…这里编写代码能保证同一时刻只能有一个线程去操作, 对共享数据进行锁定…
#释放锁
mutex.release()
注意点:
acquire和release方法之间的代码同一时刻只能有一个线程去操作
如果在调用acquire方法的时候 其他线程已经使用了这个互斥锁,那么此时acquire方法会堵塞,直到这个互斥锁释放后才能再次上锁。
import threading
# 全局变量
g_num = 0
# 创建互斥锁, Lock本质上是一个函数,通过调用函数可以创建一个互斥锁
lock = threading.Lock()
# 循环100万次执行的任务
def task1():
# 上锁
lock.acquire()
for i in range(1000000):
# 每循环一次给全局变量加1
global g_num # 表示要声明修改全局变量的内存地址
g_num = g_num + 1 # g_num += 1
# 代码执行到此,说明数据计算完成
print("task1:", g_num)
# 释放锁
lock.release()
# 循环100万次执行的任务
def task2():
# 上锁
lock.acquire()
for i in range(1000000):
# 每循环一次给全局变量加1
global g_num # 表示要声明修改全局变量的内存地址
g_num = g_num + 1 # g_num += 1
# 代码执行到此,说明数据计算完成
print("task2:", g_num)
# 释放锁
lock.release()
if __name__ == '__main__':
# 创建两个子线程
first_thread = threading.Thread(target=task1)
second_thread = threading.Thread(target=task2)
# 启动线程执行任务
first_thread.start()
second_thread.start()
# 互斥锁可以保证同一时刻只有一个线程去执行代码,能够保证全局变量的数据没有问题
# 线程等待和互斥锁都是把多任务改成单任务去执行,保证了数据的准确性,但是执行性能会下降
六、死锁
死锁: 一直等待对方释放锁的情景就是死锁
死锁的结果:
会造成应用程序的停止响应,不能再处理其它任务了。
# 死锁: 一直等待对方释放锁的情景叫做死锁
import threading
# 创建互斥锁
lock = threading.Lock()
# 需求: 多线程同时根据下标在列表中取值,要保证同一时刻只能用一个线程去取值
def get_value(index):
# 上锁
lock.acquire()
my_list = [1, 4, 4]
# 判断下标是否越界
if index >= len(my_list):
# 取值不成功,也需要释放互斥锁,不要影响后面的线程去取值
# 锁需要在合适的地方进行释放,防止死锁
lock.release()
return
# 根据下标取值
value = my_list[index]
print(value)
# 释放锁
lock.release()
if __name__ == '__main__':
# 创建大量线程,同时执行根据下标取值的任务
for i in range(10):
# 每循环一次创建一个子线程
sub_thread = threading.Thread(target=get_value, args=(i,))
# 启动线程执行任务
sub_thread.start()
使用互斥锁的时候需要注意死锁的问题,要在合适的地方注意释放锁。
死锁一旦产生就会造成应用程序的停止响应,应用程序无法再继续往下执行了。
总结
到这里这篇文章的内容就结束了,谢谢大家的观看,如果有好的建议可以留言喔,谢谢大家啦!