线程的操作

线程理论

线程是需要依赖进程的,事实上,进程运行时并不会工作,而是线程在工作,进程只是在内存中申请了一块空间而已,所以进程是一个资源单位,而线程是执行单位;就相当于进程创建工厂,而线程是工厂的打工人。

开设线程的消耗远远小于进程,一个进程里至少有一个线程,也可以开设多个线程,创建线程无需申请内存空间,创建消耗资源小,并且一个进程内的多个线程数据是共享的。

创建线程的两种方式

创建线程与创建进程的方法几乎一致,并且创建线程无需在__main__下面编写,但是为了统一,还是习惯在子代码中写。

方式一:Thread()

from threading import Thread
import time

def task(name):
    print(f'{name} is running')
    time.sleep(3)
    print(f'{name} is over')

if __name__ == '__main__':
    t = Thread(target=task, args=('jason', ))
    t.start()  # 创建线程的开销极小 几乎是一瞬间就可以创建
    print('主线程')

"""
执行结果:
jason is running
主线程
jason is over
"""

从执行结果来看,代码会先输出线程的内容,可见线程创建的迅速,如果是创建进程,一般情况下都是先输出主进程的内容。

方式二:重写Thread类的run方法

from threading import Thread
import time

class MyThread(Thread):
    def __init__(self, username):
        super().__init__()
        self.username = username
    def run(self):
        print(f'{self.username} jason is running')
        time.sleep(3)
        print(f'{self.username} is over')

t = MyThread('jason')
t.start()
print('主线程')

"""
执行结果:
jason jason is running
主线程
jason is over
"""

线程实现TCP服务端的并发

线程实现TCP服务端的并发的方法与进程也几乎一致,唯一的区别就是线程节省资源。

服务端(Server)

import socket
from threading import Thread

def task(sock):
    while True:
        msg = sock.recv(1024)
        print(msg.decode('utf8'))
        sock.send(b'from server')

if __name__ == '__main__':
    server = socket.socket()
    server.bind(('127.0.0.1', 8080))
    server.listen(5)
    while True:
        client, addr = server.accept()
        p = Thread(target=task, args=(client, ))
        p.start()

客户端(Client)

import socket

client = socket.socket()
client.connect(('127.0.0.1', 8080))

while True:
    s = input('发送给服务端的消息:')
    send_msg = 'from client: %s' % s
    client.send(send_msg.encode('utf8'))
    msg = client.recv(1024)
    print(msg.decode('utf8'))

线程join方法

join方法作用:让主线程代码等待子线程代码运行完毕之后再往下执行。

from threading import Thread
import time

def task():
    print('子线程 is running')
    time.sleep(3)
    print('子线程 is over')

t = Thread(target=task)
t.start()
t.join()
print('主线程')

"""
执行结果:
子线程 is running
子线程 is over
主线程
"""

PS:在没有用join()方法时,主线程为什么要等着子线程结束才会结束整个进程?因为主线程结束也就标志着整个进程的结束,要确保子线程运行过程中所需的各项资源。

image

线程数据共享

在同一个进程内的多个线程数据是可以实现共享的。

from threading import Thread

money = 10000
def task():
    global money
    money = 1

t = Thread(target=task)
t.start()
t.join()  # 确保线程执行了修改money的值
print(money)  # 输出:1

线程对象属性和方法

os.getpid():查看当前所在进程。

active_count():获取当前存活线程,主线程也算。

current_thread().name:获取当前线程名称

from threading import Thread, current_thread, active_count
import time
import os

def task():
    time.sleep(1)
    print('子线程名称:', current_thread().name)
    print('子线程所在进程号:', os.getpid())

if __name__ == '__main__':
    t1 = Thread(target=task)
    t2 = Thread(target=task)
    t1.start()
    t2.start()
    print('当前存活线程数:', active_count())  # 获取当前存活线程,主线程也算
    print('主线程名称:', current_thread().name)
    print('主线程所在进程号:', os.getpid())

执行结果:

当前存活线程数: 3
主线程名称: MainThread
主线程所在进程号: 15048
子线程名称: Thread-1
子线程名称: Thread-2
子线程所在进程号: 15048
子线程所在进程号: 15048

守护线程

线程与进程一样,给对象的daemon属性值设为True即可。

from threading import Thread
import time

def task():
    print('子线程 is running')
    time.sleep(3)
    print('子线程 is over')

if __name__ == '__main__':
    t1 = Thread(target=task)
    t1.daemon = True
    t1.start()
    print('主线程')

执行结果:

子线程 is running
主线程

但是在有多个线程的情况下,如果有一个子线程还在执行,那么这个守护线程就跟没有设置一样,因为主线程要等待所有非守护线程结束才可以结束。

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.)

解释:

GIL只存在于CPython(用C语言开发的)解释器中,不是python的特征。

GIL是加在CPython解释器上面的互斥锁,用于阻止同一个进程下的多个线程同时执行,原因是因为CPython解释器中的垃圾回收机制不是线程安全的,垃圾回收机制也算是一个线程,如果线程同时执行,那么可能会出现在一个线程中刚刚创建一个变量,垃圾回收机制线程就给回收了。

所以同一个进程下的多个线程要想执行必须先抢GIL锁,同一个进程下多个线程肯定不能同时运行。

拓展

如果多个任务都是IO密集型的,那么多线程更有优势,因为创建线程消耗的资源更少,并且因为有多个IO操作,线程会经常阻塞,一阻塞就会释放锁。

如果多个任务都是计算密集型,因为GIL的存在,多线程确实没有优势,但是可以用多进程。

image

GIL与普通互斥锁区别

GIL本质上其实也是一个互斥锁,GIL是对线程进行上锁,线程在执行IO操作的时候会释放锁;普通互斥锁会一锁到底,不会提前释放锁;

image

验证GIL的存在

from threading import Thread

money = 100

def task():
    global money
    money -= 1

if __name__ == '__main__':
    t_list = []  # 用于存储线程对象
    # 创建一百个线程
    for i in range(100):
        t = Thread(target=task)
        t_list.append(t)
        t.start()
    # 把线程全部join一遍,确保主线程最后执行
    for t in t_list:
        t.join()
    print(money)  # 输出:0

输出的money为0,说明其中一个线程执行时,其他线程是没有在执行的;这就是GIL的作用,在同一时刻只有一个线程在执行。

我对上述代码先进行如下修改:

from threading import Thread
import time

money = 100

def task():
    global money
    temp = money
    # 模拟IO操作
    time.sleep(0.1)
    money = temp - 1

if __name__ == '__main__':
    t_list = []  # 用于存储线程对象
    # 创建一百个线程
    for i in range(100):
        t = Thread(target=task)
        t_list.append(t)
        t.start()
    # 把线程全部join一遍,确保主线程最后执行
    for t in t_list:
        t.join()
    print(money)  # 输出:99

执行结果输出为99,说明执行中的线程在执行IO操作的时候,其它的线程就会开始去抢着执行,让所有线程里的temp都等于money后,也就是100,所以最后的money = temp - 1都变成了money = 100 - 1,最后的结果当然是99,这种的解决办法就是只有在加一个锁。

验证不同数据加不同锁

from threading import Thread, Lock
import time

money = 100
mutex = Lock()

def task():
    global money
    # 上锁
    mutex.acquire()
    temp = money
    # 模拟IO操作
    time.sleep(0.1)
    money = temp - 1
    # 解锁
    mutex.release()

if __name__ == '__main__':
    t_list = []  # 用于存储线程对象
    # 创建一百个线程
    for i in range(100):
        t = Thread(target=task)
        t_list.append(t)
        t.start()
    # 把线程全部join一遍,确保主线程最后执行
    for t in t_list:
        t.join()
    print(money)  # 输出:0

这种方法虽然解决了数据安全问题,但相应的也降低了效率。

多线程作用验证

如果多个任务都是IO密集型的,那么多线程更有优势,因为创建线程消耗的资源更少,并且因为有多个IO操作,线程会经常阻塞,一阻塞就会释放锁。

验证:

使用线程

from threading import Thread
import time

def work():
    # 模拟IO操作
    time.sleep(5)

if __name__ == '__main__':
    start_time = time.time()
    t_list = []
    # 创建500个执行有IO操作的线程
    for i in range(500):
        t = Thread(target=work)
        t_list.append(t)
        t.start()
    # 让线程全部执行完毕才能执行主线程
    for t in t_list:
        t.join()
    print('运行时间:', time.time() - start_time)
    # 输出:运行时间: 5.065059185028076

使用进程

from multiprocessing import Process
import time

def work():
    # 模拟IO操作
    time.sleep(5)

if __name__ == '__main__':
    start_time = time.time()
    # 存储进程对象
    p_list = []
    # 创建500个执行有IO操作的进程
    for i in range(500):
        p = Process(target=work)
        p_list.append(p)
        p.start()
    # 让线程全部执行完毕才能向下执行
    for p in p_list:
        p.join()
    print('运行时间:', time.time() - start_time)
    # 输出:运行时间: 14.466909170150757

当有多个IO密集型的任务时,线程的优势是很明显的,运行时间远小于进程。

但是如果是有多个计算密集型任务时,进程的优势更大。

验证

使用线程

from threading import Thread
import time

def work():
    res = 1
    # 疯狂进行计算
    for i in range(1000000):
        res += i

if __name__ == '__main__':
    start_time = time.time()
    t_list = []
    # 创建500个线程
    for i in range(50):
        t = Thread(target=work)
        t_list.append(t)
        t.start()
    # 让线程全部执行完毕才能向下执行
    for t in t_list:
        t.join()
    print('运行时间:', time.time() - start_time)
    # 输出:运行时间: 2.6860923767089844

使用进程

from multiprocessing import Process
import time

def work():
    res = 1
    # 疯狂进行计算
    for i in range(1000000):
        res += i

if __name__ == '__main__':
    start_time = time.time()
    p_list = []
    # 创建500个线程
    for i in range(50):
        p = Process(target=work)
        p_list.append(p)
        p.start()
    # 让线程全部执行完毕才能向下执行
    for p in p_list:
        p.join()
    print('运行时间:', time.time() - start_time)
    # 输出:运行时间: 1.5109596252441406
posted @ 2022-04-20 17:42  Yume_Minami  阅读(53)  评论(0编辑  收藏  举报