- 线程介绍
- 代码实现线程
- threading 模块
- threading.Thread
- 1. 创建线程
- 2. 线程实现TCP服务端的并发
- 3. 线程 join 方法
- 4. 线程之间共享
- 5. 线程对象属性和方法
- 守护线程
- GIL全局解释器锁
1. 在前面了解了进程的概念,简单来说进程就是在内存中申请了一块内存空间,其实还有一个线程的概念,
线程包含在进程之中,是代码真正的执行者。也就是说进程其实是一个资源单位,而线程是执行单位。
2. 线程是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以存在多个
线程,每条线程并行执行不同的任务。可以想象成进程是一个车间,线程就是车间里的流水线。
为什么还要划分线程?
因为开设线程的消耗远远小于进程
开进程的流程:
1.申请内存空间
2.拷贝代码
而开线程,无需申请内存空间,无需拷贝代码
线程还可以分为两类:用户级线程(User-Level Thread)和内核级线程(Kernel-Level Thread),后者又
称为内核支持的线程或轻量级进程。在多线程 *** 作系统中,各个系统的实现方式并不相同,在有的系统中实现
了用户级线程,有的系统中实现了内核级线程。
用户级线程:内核的切换由用户态程序自己控制内核切换,不需要内核干涉,少了进出内核态的消耗,但不能很
好的利用多核Cpu
内核级线程:切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户
态;可以很好的利用smp,即利用多核cpu。类似于 windows线程。
代码实现线程
线程的 *** 作代码和进程类似,需要使用的是 threading 模块该模块相当于 multiprocessing 的作用,
threading 模块 threading.Thread*** 作线程需要使用的是 threading 模块下的 Thread ,使用的方法和 Process 类似。
1. 创建线程创建线程的方式也是有俩种,通过指定函数或者继承类的方式。
和创建进程不同的是,创建线程代码不需要在 __main__ 方法下。因为新线程不需要复制代码。
指定函数方式
代码示例一
from threading import Thread
def run(username):
print(f'{username} is running')
if __name__ == '__main__': # 不需要在 __main__ 下写,可以保持习惯写下。
t = Thread(target=run, args=('XWenXiang', ))
t.start()
print('主线程')
输出结果
XWenXiang is running
主线程
1. 同样是指定函数传入参数的形式。生成线程对象后调用 start()方法启动。
2. 但是此时发现主线程中打印的话在最后面执行了,其实是因为创建线程的开销极小,几乎是一瞬间就可以
创建,也就是执行的速度很快。
代码示例二
from threading import Thread
import time
def run(username):
time.sleep(3)
print(f'{username} is running')
if __name__ == '__main__': # 不需要在 __main__ 下写,可以保持习惯写下。
t = Thread(target=run, args=('XWenXiang',))
t.start()
print('主线程')
1. 我们可以使用 time 模块增加子线程的运行时间,这样主线程的代码会被优先执行。
'''
虽然主线程的代码先执行完了,但是并不会完全结束,因为主线程结束也就标志着整个进程的结束,要确
保子线程运行过程中所需的各项资源
'''
继承类方法
代码示例
from threading import Thread
class MyThread(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f'{self.name} is running')
if __name__ == '__main__': # 可以不写。
t = MyThread('XWenXiang')
t.start()
输出结果
XWenXiang is running
1. 同样的继承 Thread类,并定义 run() 方法,该方法也会自动被 target 指定执行。并通过__init__
来传参。
2. 线程实现TCP服务端的并发
我们可以用多线程简单模拟一下并发 *** 作
服务端代码
from threading import Thread
import socket
# 如果是进程的方式这些代码要放在 __main__ 下
s = socket.socket()
s.bind(('127.0.0.1', 8888))
s.listen(5)
def run(sock):
while True:
res = sock.recv(1024)
print(res.decode('utf8'))
sock.send('我是服务端'.encode('utf8'))
while True:
sock, addr = s.accept()
t = Thread(target=run, args=(sock,))
t.start()
客户端代码
import socket
c = socket.socket()
c.connect(('127.0.0.1', 8888))
while True:
info = input('>>> ').strip()
c.send(info.encode('utf8'))
res = c.recv(1024)
print(res.decode('utf8'))
3. 线程 join 方法
前面只不过是因为子线程比较快才会先执行,其实主线程和子线程是异步的,可以通过 time 模块的time.sleep 方法来更好的看到。那么将异步改成同步也是使用 join 方法,它可以让主线程等待子线程结束。
代码示例
from threading import Thread
import time
def run(username):
time.sleep(3)
print(f'{username} is running')
if __name__ == '__main__': # 不需要在 __main__ 下写,可以保持习惯写下。
t = Thread(target=run, args=('XWenXiang',))
t.start()
t.join()
print('主线程')
输出结果
XWenXiang is running
主线程
1. 此时主线程会等待子线程结束后在执行其他代码,所以子线程的代码先打印出来。
4. 线程之间共享
进程与进程之间默认隔离,而线程与线程之间数据是共享的,因为创建了新线程也是在同一个进程里面。
代码示例
from threading import Thread
username = 'XWenXiang'
def run():
global username
username = 'XXX'
print(f'{username} is running')
t = Thread(target=run)
t.start()
t.join()
print('主线程')
print(username)
输出结果
XXX is running
主线程
XXX
1. 此时的变量已经被修改了,证明了进程之间数据是共享的。
5. 线程对象属性和方法
验证线程是否处于一个进程
代码示例
from threading import Thread
import os
def run():
print(os.getpid()) # 获取子线程所在的进程号
t = Thread(target=run)
t.start()
t.join()
print(os.getpid()) # 获取主进程所在的进程号
1. 通过 os 模块分别在子线程和主线程中获取进程号,发现他们是一样的,验证了他们确实是在同一个进程
统计进程下活跃的线程数
使用的是模块 threading 里的方法 active_count,需要导入
代码示例
from threading import Thread, active_count
import time
def run(username):
time.sleep(2)
print(f'{username} is running')
t = Thread(target=run, args=('XWenXiang',))
t.start()
print(active_count())
print('主线程')
输出结果
2
主线程
XWenXiang is running
1. 由于线程速度比较快,使用time延迟几秒。
2. 导入方法执行,此时统计的数量是 2 个,因为主线程也被包含在内了。
获取线程的名字
获取线程的名字可以使用方法 current_thread() ,或者用类的方式取出 self.name
代码示例(current_thread()方法)
from threading import Thread, current_thread
import time
def run():
time.sleep(1)
print(current_thread().name)
t = Thread(target=run)
t.start()
print('主线程', current_thread().name)
输出结果
主线程 MainThread
Thread-1 (run)
1. 主线程和子线程的名称是不一样的
代码示例(self.name)
from threading import Thread
class MyThread(Thread):
def run(self):
print(self.name)
if __name__ == '__main__': # 可以不写。
t = MyThread()
t.start()
输出结果
Thread-1
判断线程是否存活
使用的方法是 is_alive()
代码示例
from threading import Thread
import time
def run(name):
print(f'{name} is running')
time.sleep(2)
print(f'{name} is over')
t = Thread(target=run, args=('XWenXiang',))
t.start()
print(t.is_alive()) # 判断线程是否存活
print('主线程')
输出结果
XWenXiang is running
True
主线程
XWenXiang is over
守护线程
和守护进程相似,守护线程就是会随着主线程的结束而结束
代码示例
from threading import Thread
import time
def run(name):
print(f'{name} is running')
time.sleep(3)
print(f'{name} is over')
t = Thread(target=run, args=('XWenXiang',))
t.daemon = True
t.start()
print('主线程')
输出结果
XWenXiang is running
主线程
1. 将子线程设成守护线程后,不管子线程里的代码需要运行多少时间,都会随着主进程的结束而结束,且此
时主线程结束会将子线程进行回收。所以示例中子线程只执行了一部分。
1. 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束
2. 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
也就是说,主线程在有非守护线程存在的情况下,设置守护线程作用不大
代码示例
from threading import Thread
import time
def run(name):
print(f'{name} is running')
time.sleep(1)
print(f'{name} is over')
def run1(name):
print(f'{name} is running')
time.sleep(2)
print(f'{name} is over')
t = Thread(target=run, args=('XWenXiang',))
t1 = Thread(target=run1, args=('XXX',))
t.daemon = True
t.start()
t1.start()
print('主线程')
输出结果
XWenXiang is running
XXX is running
主线程
XWenXiang is over
XXX is over
1. 此时除了守护线程外还有一个线程,当主线程代码执行完不会马上结束,会等待子线程结束并回收。和不
设置守护线程效果一样。
GIL全局解释器锁
GIL全局解释器锁在官方文档中是这么描述的:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL exists,
other features have grown to depend on the guarantees that it enforces.)
理解:
首先要知道 python 解释器的类别有很多,例如 Cpython、Jpython、Ppython
GIL只存在于CPython解释器中,GIL是一把互斥锁,用于阻止同一个进程下的多个线程同时执行,
也就是说同一时间只执行一个线程,不能利用多核优势。这是因为CPython解释器中的垃圾回收机制不是
线程安全的,垃圾回收机制代码在进程中也是以线程存在。
我们可以反推一下,如果可以多个线程同时进行,有可能会出现在变量名都还没有赋值上就被垃圾
回收机制给清除了,也就是说会产生垃圾回收机制与正常线程之间数据错乱。所以我们需要一把锁进行
限制。
GIL是一把全局解释器锁,因为执行代码都需要用到解释器,加上锁之后线程就需要通过抢锁来得到
解释器资源,等到运行结束后放锁让下一个线程抢锁。所以一次只能运行一个线程。
所有的解释型语言都无法做到同一个进程下多个线程利用多核优势
如果你想让你的应用更好地利用多核心计算机的计算资源,
推荐你使用 multiprocessing 或 concurrent.futures.ProcessPoolExecutor。
但是,如果你想要同时运行多个 I/O 密集型任务,则多线程仍然是一个合适的模型(使用多道技术)。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)