python----线程、进程、协程的区别及多线程详解
文章目录
- 一、线程、进程、协程区别
- 二、创建线程
- 1、函数创建
- 2、类创建
- 三、线程锁
- 1、Lock
- 2、死锁
- 2.1加锁之后处理业务逻辑,在释放锁之前抛出异常,这时的锁没有正常释放,当前的线程因为异常终止了,就会产生死锁。
- 2.2开启两个或两个以上的线程,不同的线程得到了不同的锁,都在等待对方释放锁,但是都在阻塞,所以产生了死锁
- 2.3在同一线程里,多次取获得锁,第一次获取锁后,还未释放,再次获得锁
- 3、RLock
- 四、线程通信
- 1、condition
- 2、semaphore
一、线程、进程、协程区别
1. 进程是资源分配的独立单位,是应用程序的载体, 进程之间是相互独立(资源不共享),一个进程由多个线程构成2.线程是资源分配的最小单位,线程共享进程的资源3.协程是用户态执行的轻量级编程模型,由单一线程内部发出控制信号进行调度,协程允许不同入口点在不同的位置暂停或开始程序,协程本质就是一个小线程多线程适合于 I/O 密集型任务,如网络请求、文件读写等,可以提高并发性和响应性。
多进程适用于 CPU 密集型任务,如大量计算、图像处理等,可以利用多核处理器加速运算。
二、创建线程
在Python中创建线程主要依靠内置的threading模块。线程类Thread的常用方法如下表:
序号 | 方法 | 含义 |
---|---|---|
1 | start() | 创建一个Thread子线程实例并执行该实例的run()方法 |
2 | run() | 子线程需要执行的目标任务 |
3 | join() | 主进程阻塞等待子线程直到子线程结束才继续执行,可以设置等待超时时间timeout |
4 | is_alive() | 判断子线程是否终止 |
5 | daemon() | 设置子线程是否随主进程退出而退出,默认是False |
threading.current_thread():获取到当前线程。
获取线程后可以得到两个比较重要的属性:name和ident,分别是线程名称和id。
创建线程可以使用两种方法:使用函数或类创建。
1、函数创建
import threading
import timedef myThread(i):start = time.time()my_thread_name = threading.current_thread().name # 当前线程的名字time.sleep(i)my_thread_id = threading.current_thread().ident # 当前线程idprint('当前线程为:{},线程ID:{},所在进程为:{}'.format(my_thread_name, my_thread_id, os.getpid()))print('%s线程运行时间结束,耗时%s...' % (my_thread_name, time.time() - start))# 创建三个线程
def fun():t1 = time.time()thread = []for i in range(1, 4):t = threading.Thread(target=myThread,name='线程%s' % i, args=(i,))t.start()thread.append(t)for i in thread:i.join() # 阻塞主线程if __name__ == '__main__':fun()
threading.Thread(group=None,target=None,name='',args=(),kwargs={},daemon=False)
group:预留参数,不需要传递
target:目标代码(要执行的内容)
name:线程名称:字符串
args:是target的参数,可迭代对象
kwargs:是target的参数,是一个字典
daemon:设置线程为守护线程
2、类创建
class myThread(threading.Thread):def __init__(self,name=None):super().__init__()self.name = namedef run(self):start = time.time()my_thread_name = threading.current_thread().name # 当前线程的名字time.sleep(3)my_thread_id = threading.current_thread().ident # 当前线程idprint('当前线程为:{},线程ID:{},所在进程为:{}'.format(my_thread_name, my_thread_id, os.getpid()))print('%s线程运行时间结束,耗时%s...' % (my_thread_name, time.time() - start))# 创建三个线程
def fun():t1 = time.time()thread = []for i in range(1, 4):t = myThread(name='线程%s' % i)t.start()thread.append(t)for i in thread:i.join() # 阻塞主线程,直到子线程执行完毕再运行主线程if __name__ == '__main__':fun()
三、线程锁
多线程一个很大的问题是数据不安全,因为线程之间的数据是共享的。多线程可以通过线程锁来进行数据同步,可用于保护共享资源同时被多个线程读写引起冲突导致错误
import threading
x = 0
# lock = threading.RLock()
def increment():global xname = threading.current_thread().namefor _ in range(1000000):x += 1print(name, x)
threads = []
for _ in range(3):t = threading.Thread(target=increment)threads.append(t)t.start()
for t in threads:t.join()
print("Final value of x:", x)#Thread-2 1435674
#Thread-1 1423093
#Thread-3 1727936
#;Final value of x: 1727936
可以看出开启三个线程在0的基础上每次加1000000得到的结果应该是3000000.但不是这样的
1、Lock
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。互斥锁为资源设置一个状态:锁定和非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
import threading
x = 0
lock = threading.Lock()
def increment():global xname = threading.current_thread().namelock.acquire()try:for _ in range(1000000):x += 1print(name, x)finally:lock.release()
threads = []
for _ in range(3):t = threading.Thread(target=increment)threads.append(t)t.start()for t in threads:t.join()
print("Final value of x:", x)#Thread-1 1000000
#Thread-2 2000000
#Thread-3 3000000
#Final value of x: 3000000
使用了锁之后,代码运行速度明显降低,这是因为线程由原来的并发执行变成了串行,不过数据安全性得到保证。
还可以使用with lock这种上下文格式,自动管理上锁和释放锁。
import threading
x = 0
lock = threading.Lock()
def increment():global xname = threading.current_thread().namewith lock:for _ in range(1000000):x += 1print(name, x)
threads = []
for _ in range(3):t = threading.Thread(target=increment)threads.append(t)t.start()
for t in threads:t.join()
print("Final value of x:", x)#Thread-1 1000000
#Thread-2 2000000
#Thread-3 3000000
#Final value of x: 3000000
一般来说加锁以后还要有一些功能实现,在释放之前还有可能抛异常,一旦抛出异常,锁是无法释放,但是当前线程可能因为这个异常被终止了,这就产生了死锁。死锁解决办法:
1、使用 try..except..finally 语句处理异常、保证锁的释放2、with 语句上下文管理,锁对象支持上下文管理。只要实现了__enter__和__exit__魔术方法的对象都支持上下文管理。另一种是
锁的应用场景:独占锁: 锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候。共享锁: 如果共享资源是不可变的值时,所有线程每一次读取它都是同一样的值,这样的情况就不需要锁。使用锁的注意事项:少用锁,必要时用锁。使用了锁,多线程访问被锁的资源时,就变成了串行,要么排队执行,要么争抢执行。
加锁时间越短越好,不需要就立即释放锁。
一定要避免死锁。
不使用锁时,有了效率,但是结果是错的。使用了锁,变成了串行,效率地下,但是结果是对的。
2、死锁
2.1加锁之后处理业务逻辑,在释放锁之前抛出异常,这时的锁没有正常释放,当前的线程因为异常终止了,就会产生死锁。
解决方案:
1、使用 try..except..finally 语句处理异常、保证锁的释放2、with 语句上下文管理,锁对象支持上下文管理。只要实现了__enter__和__exit__魔术方法的对象都支持上下文管理。
2.2开启两个或两个以上的线程,不同的线程得到了不同的锁,都在等待对方释放锁,但是都在阻塞,所以产生了死锁
解决方案:
1、不同线程中获得锁的顺序一致,不能乱2、使用递归锁:RLock。
2.3在同一线程里,多次取获得锁,第一次获取锁后,还未释放,再次获得锁
解决方案:
1、使用递归锁:RLock。
3、RLock
递归锁也被称为“锁中锁”,指一个线程可以多次申请同一把锁,但是不会造成死锁,这就可以用来解决上面的死锁问题。
import threading
x = 0
lock = threading.RLock()
def increment():global xname = threading.current_thread().namelock.acquire()lock.acquire()try:for _ in range(1000000):x += 1print(name, x)finally:lock.release()lock.release()
threads = []
for _ in range(3):t = threading.Thread(target=increment)threads.append(t)t.start()
for t in threads:t.join()
print("Final value of x:", x)#Thread-1 1000000
#Thread-2 2000000
#Thread-3 3000000
#Final value of x: 3000000
RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
四、线程通信
1、condition
Condition可以认为是一把比Lock和RLOK更加高级的锁,其在内部维护一个琐对象(默认是RLock),可以在创建Condigtion对象的时候把琐对象作为参数传入。Condition也提供了acquire, release方法,其含义与琐的acquire, release方法一致,其实它只是简单的调用内部琐对象的对应的方法而已。
运行原理:可以认为Condition对象维护了一个锁(Lock/RLock)和一个waiting池。线程通过acquire获得Condition对象,当调用wait方法时,线程会释放Condition内部的锁并进入blocked状态,同时在waiting池中记录这个线程。当调用notify方法时,Condition对象会从waiting池中挑选一个线程,通知其调用acquire方法尝试取到锁。Condition对象的构造函数可以接受一个Lock/RLock对象作为参数,如果没有指定,则Condition对象会在内部自行创建一个RLock。除了notify方法外,Condition对象还提供了notifyAll方法,可以通知waiting池中的所有线程尝试acquire内部锁。由于上述机制,处于waiting状态的线程只能通过notify方法唤醒,所以notifyAll的作用在于防止有的线程永远处于沉默状态。
import threading
import time
from queue import Queueclass Producer(threading.Thread):# 生产者函数def run(self):global countwhile True:if con.acquire():# 当count 小于等于1000 的时候进行生产if count > 1000:con.wait()else:count = count + 100msg = self.name + ' produce 100, count=' + str(count)print(msg)# 完成生成后唤醒waiting状态的线程,# 从waiting池中挑选一个线程,通知其调用acquire方法尝试取到锁con.notify()con.release()time.sleep(1)class Consumer(threading.Thread):# 消费者函数def run(self):global countwhile True:# 当count 大于等于100的时候进行消费if con.acquire():if count < 100:con.wait()else:count = count - 5msg = self.name + ' consume 5, count=' + str(count)print(msg)con.notify()# 完成生成后唤醒waiting状态的线程,# 从waiting池中挑选一个线程,通知其调用acquire方法尝试取到锁con.release()time.sleep(1)count = 0
con = threading.Condition()def test():for i in range(2):p = Producer()p.start()for i in range(5):c = Consumer()c.start()
if __name__ == '__main__':test()#Thread-1 produce 100, count=100
#Thread-2 produce 100, count=200
#Thread-3 consume 5, count=195
#Thread-4 consume 5, count=190
#Thread-5 consume 5, count=185
#Thread-6 consume 5, count=180
#Thread-7 consume 5, count=175
#Thread-2 produce 100, count=275
#Thread-3 consume 5, count=270
2、semaphore
semaphore是python中的一个内置的计数器,内部使用了Condition对象,多线程同时运行,能提高程序的运行效率,但是并非线程越多越好,而 semaphore 信号量可以通过内置计数器来控制同时运行线程的数量,启动线程(消耗信号量)内置计数器会自动减一,线程结束(释放信号量)内置计数器会自动加一;内置计数器为零,启动线程会阻塞,直到有本线程结束或者其他线程结束为止;
创建多个线程,同一时间运行三个线程
import threading
# 导入时间模块
import time# 添加一个计数器,最大并发线程数量5(最多同时运行5个线程)
semaphore = threading.Semaphore(2)def foo():semaphore.acquire() #计数器获得锁time.sleep(2) #程序休眠2秒print("当前时间:",time.ctime()) # 打印当前系统时间semaphore.release() #计数器释放锁if __name__ == "__main__":thread_list= list()for i in range(6):t=threading.Thread(target=foo,args=()) #创建线程thread_list.append(t)t.start() #启动线程for t in thread_list:t.join()print("程序结束!")#当前时间: Tue Jul 30 11:18:38 2024
#当前时间: Tue Jul 30 11:18:38 2024
#当前时间: Tue Jul 30 11:18:40 2024
#当前时间: Tue Jul 30 11:18:40 2024
#当前时间: Tue Jul 30 11:18:42 2024
#当前时间: Tue Jul 30 11:18:42 2024
#程序结束!
因为Semaphore使用了Condition,线程之间仍然有锁保证线程数据安全