并发编程
1 开启线程
- 开启线程的代码不需要放入main的代码块中,因为开启线程不需要再次申请内存空间以及像开启进程那样从上往下执行代码;
- 创建线程的开销小,代码执行到start语句基本上能立即创建。
from threading import Thread
import time
def task(name):
print('{name} is running.'.format(name=name))
time.sleep(1)
print('{name} is over.'.format(name=name))
if __name__ == '__main__':
t = Thread(target=task, args=('Task1',))
t.start() # 在当前主进程下创建一个线程
from threading import Thread
class CustomThread(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print('{name} is running.'.format(name=self.name))
if __name__ == '__main__':
t = CustomThread('Task1')
t.start()
2 TCP服务端实现并发
服务端的必备条件
- 具有固定的ip地址和端口号;
- 不间断提供服务;
- 能够支持并发。
server.py
from threading import Thread
import socket
server = socket.socket() # 默认是TCP协议
server.bind(('127.0.0.1', 8080))
server.listen(5)
def talk(conn):
# 通信循环
while True:
try:
data = conn.recv(1024)
if len(data) == 0:
break
print(data.decode('utf-8'))
print(str(data, encoding='utf-8'))
conn.send(data.upper())
except ConnectionResetError as e:
print(e)
break
conn.close()
# 连接循环
while True:
conn, addr = server.accept()
t = Thread(target=talk, args=(conn,))
t.start()
client.py
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
client.send('Hello, world.'.encode('utf-8'))
data = client.recv(1024)
print(data.decode('utf-8'))
3 线程对象的方法join
from threading import Thread
import time
def task(name):
print('{name} is running.'.format(name=name))
time.sleep(1)
print('{name} is over.'.format(name=name))
if __name__ == '__main__':
t = Thread(target=task, args=('Task1',))
t.start()
t.join()
# 等待子线程结束后,主线程再继续执行。
print('Over.')
4 线程间数据共享
同一个进程下的线程数据共享。
from threading import Thread
import time
num = 10
def task():
global num
num = 1
if __name__ == '__main__':
t = Thread(target=task)
t.start()
t.join()
print(num) # 1
5 线程对象属性和方法
- current_thread().name
获取线程名 - active_count()
统计当前活跃的线程数量
from threading import Thread, active_count, current_thread
import os
import time
def task(sec):
print(os.getpid())
print(current_thread().name)
time.sleep(sec)
print('Running.')
if __name__ == '__main__':
t1 = Thread(target=task, args=(1, ))
t1.start() # Thread-1
t2 = Thread(target=task, args=(2, ))
t2.start() # Thread-2
print(os.getpid())
print(current_thread().name) # MainThread
t2.join()
print(active_count()) # 1
6 守护线程
主线程运行完毕后其守护线程立即结束。
主线程运行完毕后不会立即结束,会等待其它非守护线程运行结束后才会结束,因为主线程结束了就意味着其下的所有进程也结束了。
from threading import Thread
import time
def task1():
print('Task1 is running.')
time.sleep(1)
print('Task1 is over.')
def task2():
print('Task2 is running.')
time.sleep(3)
print('Task2 is over.')
if __name__ == '__main__':
t1 = Thread(target=task1)
t2 = Thread(target=task2)
t1.daemon = True
t1.start()
t2.start()
print('Main')
7 线程互斥锁
多个进程/线程操作同一个数据,为了保证数据的安全,需要进行加锁处理。
加锁处理会将并发变成串行,降低了数据的运行效率,但能保证数据的准确。
from threading import Thread, Lock
import time
num = 10
mutex = Lock()
def task():
global num
# mutex.acquire()
# 使用上下文管理with自动调用mutex.acquire()和mutex.release()
with mutex:
temp = num
time.sleep(1)
num = temp - 1
# mutex.release()
if __name__ == '__main__':
t_list = []
for i in range(10):
t = Thread(target=task)
t.start()
t_list.append(t)
for each_t in t_list:
each_t.join()
print(num)
8 GIL全局解释器锁
全局解释器锁(GIL,Global Interpreter Lock)
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不是Python语言的特点,而是CPython解释器的特点,用来保证解释器级别的数据安全;
- GIL本质是一把互斥锁,用来阻止同一个进程下的多个线程同时执行;
- 原因:CPython解释器的内存管理不是线程安全的。
这里内存管理是指垃圾回收机制,垃圾回收机制运行 于垃圾回收线程中。
假设没有GIL,一个线程在创建变量过程中在为变量名和变量值建立绑定关系前,垃圾回收线程对变量进行了扫描操作,发现新建的这个变量名身上的引用计数为0,就会将这个变量视作垃圾进行回收,这种操作是不合理的; - 影响:GIL的存在使得同一个进程下的多个线程无法利用多核的优势;
- 解释型语言的通病:同一个进程的多个线程无法利用多核优势。
结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,因此无法利用多核的优势。
9 多进程与多线程比较
单核与多核
IO密集型与计算密集型
- 对于计算密集型
对于单核,同一时间只能有一个任务在运行。
多进程:需消耗额外资源;
多线程:节省系统开销(推荐)。
对于多核
多进程:任务总耗时是单个任务耗时中最长的耗时多一点;(推荐)
多线程:任务总耗时是所有任务耗时之和多一点。 - 对于IO密集型
由于可能存在大量阻塞,单核与多核差别不大。
多进程:需消耗额外资源;
多线程:节省系统开销(推荐)。