python基础——多线程

在引入多线程之前首先看一下多任务:
所谓多任务,简单的说就是操作系统同时执行多个任务
真正的多任务是只能在多核CPU上才可以实现的,单核CPU只是切换速度比较快,根本不是真正的多任务

并行和并发概念

并行:当任务数小于CPU核数,每个任务占用一个CPU核,所有任务真正的一起执行
并发:当任务数大于CPU核数,操作系统通过调度算法,在各个任务之间进行切换,实现多任务"一起"执行,实际上只是切换的速度比较快,看起来一起执行而已

多线程

python底层已经封装好自己的模块threading,可以更加方便的使用,并且主线程会等待所有的子线程结束以后再结束
首先看一下多线程的使用

import threading 
import time


def func1():
	for i in range(3):
		print("开发")
		# 由于操作系统速度非常快,如果没有sleep是看不出效果的
		time.sleep(1)

def func2():
	for i in range(3):
		print("测试")
		time.sleep(1)

t1 = threading.Thread(target=func1)
t2 = threading.Thread(target=func2)

# 只有在调用start()方法之后才会真正的创建线程,并启动线程
t1.start()
t2.start()

输出:
开发
测试
开发
测试
开发
测试
一般来说多线程是比单线程快

单线程:

import threading 
import time

def func1():
	print("开发")
	time.sleep(1)

start_time = time.time()
for i in range(5):
	func1()
end_time = time.time()
print(end_time-start_time)

输出:
开发
开发
开发
开发
开发
5.0102128982543945

多线程:

import threading 
import time

def func1():
	print("开发")
	time.sleep(1)

start_time = time.time()
for i in range(5):
	t = threading.Thread(target=func1)
	t.start()
t.join()  # 主线程等待所有的子线程结束以后才会接着向下执行代码,即使调用join()方法,主线程也会等待子线程结束,只是end_time的时间就不是所有线程执行完以后的时间 
end_time = time.time()
print(end_time-start_time)

输出:
开发
开发
开发
开发
开发
1.005455493927002

以上比较可以得出:单线程用了5秒而开了5个线程用了1秒,所以多线程比单线程快。。。
在cpython解释器中时存在GIL的,当一个进程中同时存在多个线程时,并不能真正实现多任务,尤其是在线程中存在I/O操作时GIL锁会解开而继续执行下一个线程,即使如此,在程序中存在I/O操作时多线程还是比单线程快。。。

注意:多线程的执行顺序是不确定的,
import threading 
import time


def func1(num):
	for i in range(num):
		print("开发")
		time.sleep(1)

def func2(num):
	for i in range(num):
		print("测试")
		time.sleep(1)

t1 = threading.Thread(target=func1, args=(3,))
t2 = threading.Thread(target=func2, args=(3,))
t1.start()
t2.start()

输出:
开发
测试
测试
开发
测试
开发
从代码和执⾏结果我们可以看出,多线程程序的执⾏顺序是不确定的。当执 ⾏到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进⼊就 绪(Runnable)状态,等待调度。⽽线程调度将⾃⾏选择⼀个线程执⾏
多线程之间共享全局变量
import threading 
import time


def func1(num):
	num.append(4)
	print("func1 num {}".format(num))

def func2(num):
	time.sleep(1)
	print("func2 num {}".format(num))

num = [1,2,3]
t1 = threading.Thread(target=func1, args=(num, ))
t1.start()

t2 = threading.Thread(target=func2, args=(num, ))
t2.start()

输出:
func1 num [1, 2, 3, 4]
func2 num [1, 2, 3, 4]

总结:
1、在一个进程内所有的线程共享全局变量,有利于数据共享
2、线程是对全局变量随意遂改可能造成多线程之间对全局变量 的混乱(即线程⾮安全)

多线程共享全局变量可能出现的问题(经典的数字相加问题)
import threading 

sum_num = 0

def func1(num):
	global sum_num  # 使用全局变量
	for i in range(num):
		sum_num += 1
	print(sum_num)

def func2(num):
	global sum_num
	for i in range(num):
		sum_num += 1
	print(sum_num)

t1 = threading.Thread(target=func1, args=(1000000,))
t2 = threading.Thread(target=func2, args=(1000000,))

t1.start()
t2.start()

输出:
1031410
1183429

正常的结果应该是第一次加完以后是1000000,第二次加完以后是2000000,但是两次结果都没有符合预期的结果值,由于是多线程操作,可能的原因如下:
1、在sum_num=0时,t1取得sum_num=0。此时系统把t1调度为”sleeping”状态, 把t2转换为”running”状态,t2也获得g_num=0
2、然后t2对得到的值进⾏加1并赋给sum_num,使得sum_num=1
3、然后系统⼜把t2调度为”sleeping”,把t1转为”running”。线程t1⼜把它之 前得到的0加1后赋值给sum_num。
4、这样导致虽然t1和t2都对sum_num加1,但结果仍然是sum_num=1

结论:多个线程同时对一个全局变量进行操作会出现资源竞争问题,导致结果偏差

解决多线程资源竞争可以使用线程同步的方法,即互斥锁
互斥锁的概念:某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他 线程不能更改;直到该线程释放资源,将资源的状态变成“⾮锁定”,其他的 线程才能再次锁定该资源。互斥锁保证了每次只有⼀个线程进⾏写⼊操作, 从⽽保证了多线程情况下数据的正确性。

import threading


sum_num = 0
mutex = threading.Lock()  # 创建一把锁
def func1(num):
	global sum_num  # 使用全局变量
	for i in range(num):
		mutex.acquire()  # 上锁
		sum_num += 1
		mutex.release()  # 解锁
	print(sum_num)

def func2(num):
	global sum_num
	for i in range(num):
		mutex.acquire()
		sum_num += 1
		mutex.release()
	print(sum_num)

t1 = threading.Thread(target=func1, args=(1000000,))
t2 = threading.Thread(target=func2, args=(1000000,))

t1.start()
t2.start()

输出:
1937509
2000000
注意:

1、如果这个锁之前是没有上锁的,那么acquire不会堵塞 。
2、如果在调⽤acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么 此时acquire会堵塞,直到这个锁被解锁为⽌。
3、加锁的位置不同,t1的输出结果可能有变化,但t2的结果是一定的,因为总共就加了2000000次,所以最终的结果肯定是2000000,如果把锁加在for循环的上边,即整个的相加都被锁定的话,那么t1是1000000,t2是2000000。
4、加锁以后,锁定的部分是单线程执行的,这样数据保证是安全的,但是效率会下降,这也是加锁的一个缺点。

死锁问题

在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时 等待对⽅的资源,就会造成死锁

import threading 
import time


class T:
	def func1(self):
		mutex1.acquire()  # mutex1 上锁
		print("func1 mutex1 abc acquire")
		mutex2.acquire()  # mutex2 上锁
		print("func1 mutex2 acquire")
		mutex2.release()  # mutex2 解锁
		mutex1.release()  # mutex1 解锁

	def func2(self):
		mutex2.acquire()
		print("func2 mutex2 acquire")
		time.sleep(1)
		mutex1.acquire()
		print("func2 mutex1 acquire")
		mutex1.release()
		mutex2.release()

	def run(self):
		self.func1()
		self.func2()

mutex1 = threading.Lock()
mutex2 = threading.Lock()
for i in range(3):
	t = threading.Thread(target=T().run)
	t.start()  # 创建3个线程

输出:
func1 mutex1 abc acquire
func2 mutex2 acquire
func2 mutex2 acquire
func1 mutex1 abc acquire

从以上结果可以看出,假如现在需要创建3个线程A、B、C,第一次假如A上锁mutex1,那么B和C线程只有等待A释放,此时A处于睡眠状态,可以轻松获取mutex2,当线程A执行完func1时,会把mutex1释放,在A执行func2时获取mutex2,假如是B抢到锁mutex1,这时A占用mutex2,需要获取mutex1才能向下执行,而B占用mutex1, 需要获取mutex2才能向下执行,此时就会形成一个死锁。。。
解决死锁的办法就是使用递归锁(RLock),只需要把mutex1 = threading.Lock()和mutex2= threading.Lock()
改为mutex1=mutex2=threading.RLock()即可解决问题

发布了36 篇原创文章 · 获赞 2 · 访问量 936

猜你喜欢

转载自blog.csdn.net/zzrs_xssh/article/details/104341038