day033线程介绍,锁,递归锁,信号量,及与进程的效率对比

 
 

本节内容:

1.线程的创建
2.线程join
3.线程的其他方法
4.线程和进程的效率对比
5.查看子线程与主线程是否在同一个进程
6.线程之间是数据共享的
7.验证多线程共享数据资源造成数据不安全
8.加锁解决共享数据不安全的问题
9.死锁现象
10.递归锁,解决死锁现象
11.守护线程
12.信号量(也是一把锁,可以同时执行多个线程)
13.事件

一、背景知识

1、进程

之前我们已经了解了操作系统中进程的概念,程序并不能单独运行,
只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。
 
程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。
 
在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。
这是这样的设计,大大提高了CPU的利用率。
进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

2、有了进程,为什么还要线程

线程介绍及vs进程
#什么是线程:#指的是一条流水线的工作过程,关键的一句话:一个进程内最少自带一个线程,其实进程根本不能执行,进程不是执行单位,是资源的单位,分配资源的单位#线程才是执行单位#进程:做手机屏幕的工作过程,刚才讲的#我们的py文件在执行的时候,如果你站在资源单位的角度来看,我们称为一个主进程,如果站在代码执行的角度来看,它叫做主线程,
只是一种形象的说法,其实整个代码的执行过程成为线程,也就是干这个活儿的本身称为线程,
但是我们后面学习的时候,我们就称为线程去执行某个任务,其实那某个任务的执行过程称为一个线程,一条流水线的执行过程为线程
 
#进程vs线程#1 同一个进程内的多个线程是共享该进程的资源的,不同进程内的线程资源肯定是隔离的#2 创建线程的开销比创建进程的开销要小的多
 
 
#并发三个任务:1启动三个进程:因为每个进程中有一个线程,但是我一个进程中开启三个线程就够了#同一个程序中的三个任务需要执行,你是用三个进程好 ,还是三个线程好?#例子:
# pycharm 三个任务:键盘输入 屏幕输出 自动保存到硬盘
#如果三个任务是同步的话,你键盘输入的时候,屏幕看不到
#咱们的pycharm是不是一边输入你边看啊,就是将串行变为了三个并发的任务
#解决方案:三个进程或者三个线程,哪个方案可行。
如果是三个进程,进程的资源是不是隔离的并且开销大,最致命的就是资源隔离,但是用户输入的数据还要给另外一个进程发送过去,进程之间能直接给数据吗?
你是不是copy一份给他或者通信啊,但是数据是同一份,我们有必要搞多个进程吗,线程是不是共享资源的,
我们是不是可以使用多线程来搞,你线程1输入的数据,线程2能不能看到,你以后的场景还是应用多线程多,
而且起线程我们说是不是很快啊,占用资源也小,还能共享同一个进程的资源,不需要将数据来回的copy!
PythonCopy

3、进程的两个缺陷

1.进程在一个时间内,只能做一件事,如果想做两件事或多件事,进程就无能为力了
2.进程在执行的时候如果遇到阻塞,例如等待输入,整个进程就会挂起,
即使后面有些工作不依赖于输入的数据,也将无法执行
举个现实的例子也许你就清楚了:
如果把我们上课的过程看成一个进程的话,那么我们要做的是耳朵听老师讲课,手上还要记笔记,脑子还要思考问题,这样才能高效的完成听课的任务。
而如果只提供进程这个机制的话,上面这三件事将不能同时执行,同一时间只能做一件事,
听的时候就不能记笔记,也不能用脑子思考,这是其一;如果老师在黑板上写演算过程,我们开始记笔记,
而老师突然有一步推不下去了,阻塞住了,他在那边思考着,
而我们呢,也不能干其他事,即使你想趁此时思考一下刚才没听懂的一个问题都不行,这是其二。
 
现在你应该明白了进程的缺陷了,而解决的办法很简单,
我们完全可以让听、写、思三个独立的过程,并行起来,这样很明显可以提高听课的效率。
而实际的操作系统中,也同样引入了这种类似的机制——线程。

4、线程的出现

60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,
一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;
二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
线程及相关解释
因此在80年代,出现了能独立运行的基本单位——线程(Threads)。
 
注意:进程是资源分配的最小单位,线程是CPU调度的最小单位.
    每一个进程中至少有一个线程。 进程里的线程共享改进程中的资源,每个线程有自己的id号,不同于端口号,
    
    在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程
 
    线程顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程是一个进程
 
    车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一个流水线
 
    流水线的工作需要电源,电源就相当于cpu
 
    所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
 
 
 多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,
相当于一个车间内有多条流水线,都共用一个车间的资源。
 
 例如,北京地铁与上海地铁是不同的进程,而北京地铁里的13号线是一个线程,
北京地铁所有的线路共享北京地铁所有的资源,比如所有的乘客可以被所有线路拉。
PythonCopy

二、线程与进程的关系

1、线程与进程的区别可以归纳为以下4点:

1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
 
2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——
需要进程同步和互斥手段的辅助,以保证数据的一致性。(就类似进程中的锁的作用)
 
3)调度和切换:线程上下文切换比进程上下文切换要快得多。线程的开启速度非常快,没有进程那么多的开销,开辟内存地址,回收等等,
 
4)在多线程操作系统中(现在咱们用的系统基本都是多线程的操作系统),
进程不是一个可执行的实体,真正去执行程序的不是进程,是线程,
你可以理解进程就是一个线程的容器。
PythonCopy

三、线程的特点

先简单了解一下线程有哪些特点,里面的堆栈啊主存区啊什么的后面会讲,大家先大概了解一下就好啦。
在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。

1、线程具有以下属性。

1)轻型实体
线程中的实体基本上不拥有系统资源,只是有一些必不可少的、能保证独立运行的资源。
线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。
TCB包括以下信息:
(1)线程状态。
(2)当线程不运行时,被保存的现场资源。
(3)一组执行堆栈。
(4)存放每个线程的局部变量主存区。
(5)访问同一个进程中的主存和其它资源。
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
 
 
2)独立调度和分派的基本单位。
在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。
由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
 
3)共享进程资源。
线程在同一进程中的各个线程,都可以共享该进程所拥有的资源,
这首先表现在:所有线程都具有相同的进程id,这意味着,线程可以访问该进程的每一个内存资源;
此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。
由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
 
4)可并发执行。
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;
同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
PythonCopy

四、threading模块,线程的模块

multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,因而不再详细介绍(官方链接)

1、(多)线程的创建

1.我们先简单应用一下threading模块来看看并发效果:

多线程简单实现
import time
from threading import Thread
#多线程并发,是不是看着和多进程很类似def func(n):
time.sleep(1)
print(n)
 
#并发效果,1秒打印出了所有的数字for i in range(10):
t = Thread(target=func,args=(i,))
t.start()
PythonCopy

2.线程的两种创建方法

两种创建方法
from threading import Thread
 
# 第一种, 调用Thread模块,
 
def func(n):
print("你好呀")
print(n)
 
 
if __name__ == '__main__':
t = Thread(target=func, args=(2,))
t.start()
print("主线程结束") # 实际上是没有主线程之分的,共享进程资源
 
 
# 第二种 继承类
 
class MyThread(Thread):
def __init__(self, ni):
super().__init__() # 记得不要覆盖父类的__init__方法,
self.ni = ni
 
 
def run(self):
print(self.ni)
print("你好啊")
 
if __name__ == '__main__':
t = MyThread(11)
t.start()
PythonCopy

2、线程的join方法,等同于进程的join方法

在join的地方等待子线程执行完成后,再往下执行主线程的代码
import time
from threading import Thread
 
def func():
time.sleep(1) # 模拟代码执行时间,显示join的效果
print('我是子线程')
 
if __name__ == '__main__':
 
t = Thread(target=func,) # 创建一个线程
t.start() # 发出信号,告诉操作系统,可以运行这个线程了,
 
print('开始等待子线程了')
t.join() # 等待子线程完成后,再往下执行
print('主线程结束')
PythonCopy

3、线程的其他方法

1.其他方法的解释

方法解释
Thread实例对象的方法
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。
 
threading模块提供的一些方法:
# threading.currentThread(): 返回当前的线程变量。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果
 
其他方法
PythonCopy

2.具体代码示例

代码示例
import time
from threading import Thread
from threading import current_thread
import threading
 
 
def func():
time.sleep(3)
 
# current_thread().ident
print('我是子线程,名字是',current_thread().getName()) # 返回线程名
print('我是子线程,id是',current_thread().ident) # 子线程的id
 
if __name__ == '__main__':
 
for i in range(10):
t = Thread(target=func,)
# t = Thread(target=func, name="线程名%s" % i) # 也可直接在这里输入线程名
t.setName("线程名%s" % i) # 设置线程名
t.start()
 
# print(t.isAlive()) # 正常来说这里是True;有可能是False,取决于速度,也许线程还没创建,这里就判断了,
print(threading.enumerate()) # 返回正在运行的线程的list,正在运行指线程启动后、结束前,
print(threading.activeCount()) # 返回正在运行的线程数量(一般还需包含一个主线程),与len(threading.enumerate())有相同的结果
 
# print(threading.current_thread())#主线程对象
# print(threading.current_thread().getName()) #主线程名称
# print(threading.current_thread().ident) #主线程ID
# print(threading.get_ident()) #主线程ID
#
print('主线程结束')
PythonCopy

4、线程和进程的效率对比

1.查看线程的pid

首先来看看pid(进程id)
from threading import Thread
from multiprocessing import Process
import os
 
def work():
print('hello',os.getpid())
print("子线程的pid",os.getpid()) # 查看子进程的pid
 
if __name__ == '__main__':
#part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
t1=Thread(target=work)
t2=Thread(target=work)
t1.start()
t2.start()
print('主线程/主进程pid',os.getpid())
 
#part2:开多个进程,每个进程都有不同的pid
p1=Process(target=work)
p2=Process(target=work)
p1.start()
p2.start()
print('主线程/主进程pid',os.getpid())
PythonCopy
那么哪些东西存在进程里,那些东西存在线程里呢?
 
进程:导入的模块、执行的python文件的文件所在位置、内置的函数、文件里面的这些代码、全局变量等等,
 
然后线程里面有自己的堆栈(类似于一个列表,后进先出)和寄存器,里面存着自己线程的变量,操作(add)等等,占用的空间很小。

2.线程与进程效率对比的具体代码示例

进程与线程开启效率比较,具体代码示例
import time
from threading import Thread
from multiprocessing import Process
 
def func():
# time.sleep(3)
print('xxxx')
 
if __name__ == '__main__':
 
t_list = []
t_s_t = time.time() # 所有线程的开始时间
for i in range(100): # 创建100个线程
t = Thread(target=func,) # 创建线程
t_list.append(t)
t.start()
[tt.join() for tt in t_list] # join等待所有线程执行
t_e_t = time.time() # 所有线程的结束时间
t_dif_t = t_e_t - t_s_t # 所有线程的执行时间
 
p_list = []
p_s_t = time.time()
for i in range(100):
p = Process(target=func,)
p_list.append(p)
p.start()
[pp.join() for pp in p_list] # 等待所有进程执行
p_e_t = time.time()
p_dif_t = p_e_t - p_s_t # 所有进程的执行时间
print('多线程的时间>>>',t_dif_t) # 时间对比,体现效率
print('多进程的时间>>>',p_dif_t)
 
print('主线程结束')
PythonCopy

5、线程之间是数据共享的

import time
from threading import Thread
 
num = 100
 
def func():
# time.sleep(3)
global num # 这里可以拿到本进程中的全局变量,子线程可以对他进行改变,体现了线程共享同进程的数据
num -= 1
 
if __name__ == '__main__':
 
# for i in range()
 
t = Thread(target=func,)
t.start()
t.join() # 等待线程执行完,才能看到效果
print('主线程的num',num) # 99 这里输出,是子线程改变后的全局变量
PythonCopy

6、验证多线程共享数据资源造成数据不安全

import time
from threading import Thread
 
 
num = 100
 
def func():
global num
tep = num
time.sleep(0.001) # 模拟延迟,这就会有多个子线程,拿到同一个数值,而不是按顺序修改,说明了线程间会导致数据不安全
tep = tep - 1 # 需要通过加锁来,确保安全,加锁牺牲了效率,保证了安全
num = tep
# num -= 1
 
 
if __name__ == '__main__':
t_list = []
for i in range(100):
t = Thread(target=func,) # 创建一个子线程
t_list.append(t)
t.start()
 
[tt.join() for tt in t_list] # 等待所有的子线程结束
print("主线程的num", num)
PythonCopy

7、加锁解决共享数据不安全的问题

import time
from threading import Thread,Lock
 
num = 100def func(tl):
# time.sleep(3)
# print('xxxxx')
time.sleep(1)
global num # 这里global,一开始所有的子线程拿到的都是100,随着加锁里面的代码执行,这个num一直在改变
print(num)
tl.acquire() # 加锁保证只有一个子线程进入,然后执行,这里所有的子线程会变成串行,牺牲了效率,保证了安全
tep = num
time.sleep(0.001)
tep = tep - 1
num = tep
tl.release()
# num -= 1
 
if __name__ == '__main__':
tl = Lock()
t_list = []
for i in range(10):
t = Thread(target=func,args=(tl,)) # 异步提交100个子线程
t_list.append(t)
t.start()
[tt.join() for tt in t_list] # 这样join等待所有的子线程
# t.join()
print('主线程的num',num)
PythonCopy

8、死锁现象

双方互相等待对方释放对方手里拿到的那个锁
import time
from threading import Thread,Lock,RLock
 
def func1(lock_A,lock_B):
lock_A.acquire()
time.sleep(0.5)
print('alex拿到了A锁')
lock_B.acquire() # 子线程t2此时的lock_B锁还没释放,t1阻塞在这里,等待其释放
print('alex拿到了B锁')
lock_B.release()
lock_A.release()
 
def func2(lock_A,lock_B):
lock_B.acquire()
print('taibai拿到了B锁')
lock_A.acquire() # 子线程t1此时的lock_A锁还没释放,t2阻塞在这里,等待其释放
print('taibai 拿到了A锁')
lock_A.release()
lock_B.release()
 
if __name__ == '__main__':
lock_A = Lock()
lock_B = Lock()
t1 = Thread(target=func1,args=(lock_A,lock_B))
t2 = Thread(target=func2,args=(lock_A,lock_B))
t1.start()
t2.start()
PythonCopy

9、递归锁,解决死锁现象

RLock 同样是互斥的.里面存着个计数器,拿到一个锁,加1,释放一个锁,减1,
同一时间,只有一个线程进入了锁里面,外面的线程,
只有等这个线程释放了所有的锁,才可以进行争抢这个锁的,进行执行
注意:
锁必须这样创建,
lock_A = lock_B = RLock()
必须要同时指向同一个递归锁,才能解决问题
import time
from threading import Thread, Lock, RLock
 
 
 
def func1(lock_A, lock_B):
lock_A.acquire()
time.sleep(0.5)
print("alex拿到了A锁")
lock_B.acquire()
print("alex拿到了B锁")
lock_B.release()
lock_A.release()
 
# 同一时间,只有一个子线程抢到了递归锁,其他子线程就都抢不到了,须等到这个子线程执行完毕,全部释放了,才能重新抢锁def func2(lock_A, lock_B):
lock_B.acquire()
print("taibai拿到了B锁")
lock_A.acquire()
print("taibai拿到了A锁")
lock_A.release()
lock_B.release()
 
 
if __name__ == '__main__':
# lock_A = RLock() # 这样不行
# lock_B = RLock() #
lock_A = lock_B = RLock() # 必须同时指向一个递归锁,才能解决死锁
# lock_A = lock_B = Lock() # 这样也不行
t1 = Thread(target=func1, args= (lock_A,lock_B))
t2 = Thread(target=func2, args=(lock_A,lock_B))
 
t1.start()
t2.start()
PythonCopy

10、守护线程

守护线程:主线程等着进程中所有非守护线程的结束,才算结束
 
守护进程:主进程代码结束,守护进程跟着结束
import time
from threading import Thread
from multiprocessing import Process
 
 
def func1():
time.sleep(3)
print("任务1结束")
 
def func2():
time.sleep(2)
print("任务2结束")
 
 
if __name__ == '__main__':
t1 = Thread(target=func1,)
t2 = Thread(target=func2,)
# t1.daemon = True # 线程1不会执行,只有线程2执行,因为守护线程的代码执行时间比其他的子线程的代码执行时间长,
t2.setDaemon(True) # t2设置为守护线程,但是t2的执行时间,比其他子线程的时间短,所以会打印结果
t1.start()
t2.start()
 
print("主线程结束") # 主线程代码虽然结束,但是还在等待非守护子线程的结束才结束
# # 守护线程会跟着主线程的结束而结束,不是跟着主线程的代码结束而结束,
 
# # 对比来看进程# p1 = Process(target=func1,)# p2 = Process(target=func2,)# p1.daemon = True # 守护进程跟着主进程的代码结束,而结束,# p1.start()# p2.start()## print("主进程结束")
PythonCopy

fe:在这里我们简单总结一下:

进程是最小的内存分配单位
 
线程是操作系统调度的最小党委
 
线程被CPU执行了
 
进程内至少含有一个线程
 
进程中可以开启多个线程 
 
开启一个线程所需要的时间要远小于开启一个进程
 
多个线程内部有自己的数据栈,数据不共享
 
全局变量在多个线程之间是共享的
PythonCopy

11、信号量(也是一把锁,可以同时执行多个线程)

实际上也是一把锁,就是同一时间可以进入多个子线程,
可以自己设定,同一时间可以执行多少个子线程,默认为cup_count()的数量(cup的核数)
import time
import random
from threading import Thread,Semaphore
 
def func1(i,s):
s.acquire()
# time.sleep(1)
print('客官%s里边请~~'%i) # 一开始可以同时进入四个子线程,因为一开始4个信号量都为空,
# 然后就是根据每个子线程的执行时间的不同,释放一个,进入一个,或者释放多个,进入多个
time.sleep(random.randint(1, 3)) # 模拟每个子进程的执行时间的不同
s.release()
 
 
if __name__ == '__main__':
s = Semaphore(4) # 信号量
for i in range(10):
t = Thread(target=func1,args=(i,s))
t.start()
PythonCopy

12、事件(同进程的事件)

from threading import Thread,Event
 
e = Event() #默认是False,
 
# print(e.isSet())print('开始等啦')
e.set() #将事件对象的状态改为Trueprint(e.isSet())# e.clear() #将e的状态改为False
 
e.wait() #如果e的状态为False,就在这里阻塞print('大哥,还没完事儿,你行')
 
原文地址:https://www.cnblogs.com/yipianshuying/p/10045329.html