这是崔庆才老师的《52讲轻松搞定网络爬虫》课程相关笔记。
以下的代码段运行前请先执行此代码
import threading
import time
Thread类直接创建子线程
通过构造函数,再传递此函数给 threading.Thread(target,args) 实现多线程,其中target是目标函数,args是目标函数的参数列表。
## 这里是构造函数
def target(second):
print(f'Threading {threading.current_thread().name} is running') ### 线程的名字我们通过 threading.current_thread().name 来获取出来,
### 主线程的话,其值就是 MainThread,子线程的话,其值就是 Thread-*。
print(f'Threading {threading.current_thread().name} is sleep {second}s')
time.sleep(second)
print(f'Threading {threading.current_thread().name} is ended')
## 主线程开始
print(f'Threading {threading.current_thread().name} is running')
for i in [1,5]:
t = threading.Thread(target=target,args=[i])
t.start()
t.join() ### 这里的作用是子线程运行结束之前主线程不能提前运行结束,
### 主进程等待子进程运行结束后自己才可以运行结束。
### 主进程等待子进程结束才结束的行为叫阻塞。
print(f'Threading {threading.current_thread().name} is ended')
Threading MainThread is running
Threading Thread-6 is running
Threading Thread-6 is sleep 1s
Threading Thread-6 is ended
Threading Thread-7 is running
Threading Thread-7 is sleep 5s
Threading Thread-7 is ended
Threading MainThread is ended
守护进程
一个线程被设置为守护线程,这意味着,如果主线程结束了而该守护线程还没有运行完,那么它将会被强制结束。
设置守护进程的方法
- 方法一:可以通过 setDaemon(True) 方法来将某个线程设置为守护线程。
- 方法二:通过 threading.Thread (target,args,daemon=True) 来实现将某个线程设置为守护进程。
## 这里是构造函数
def target(second):
print(f'Threading {threading.current_thread().name} is running')
print(f'Threading {threading.current_thread().name} sleep {second}s')
time.sleep(second)
print(f'Threading {threading.current_thread().name} is ended')
# ## 方法一:
# print(f'Threading {threading.current_thread().name} is running')
# t1 = threading.Thread(target=target, args=[2])
# t1.start()
# t2 = threading.Thread(target=target, args=[5])
# t2.setDaemon(True) ### 将t2设置为守护进程,它会随着主进程结束而结束。
# t2.start()
# print(f'Threading {threading.current_thread().name} is ended')
## 方法二:
print(f'Threading {threading.current_thread().name} is running')
t1 = threading.Thread(target=target, args=[2])
t1.start()
t2 = threading.Thread(target=target, args=[5],daemon=True) ### 将t2设置为守护进程,它会随着主进程结束而结束。
t2.start()
print(f'Threading {threading.current_thread().name} is ended')
Threading MainThread is running
Threading Thread-15 is running
Threading Thread-15 sleep 2s
Threading Thread-16 is running
Threading Thread-16 sleep 5s
Threading MainThread is ended
Threading Thread-15 is ended
Threading Thread-16 is ended
继承 Thread类创建子线程
MyThread是一个继承 threading 的类,线程需要实现的方法写在run方法里
## 这是个继承了Thread的类
class MyThread(threading.Thread):
def __init__(self, second):
threading.Thread.__init__(self)
self.second = second
def run(self):
print(f'Threading {threading.current_thread().name} is running')
print(f'Threading {threading.current_thread().name} sleep {self.second}s')
time.sleep(self.second)
print(f'Threading {threading.current_thread().name} is ended')
## 主线程开始
print(f'Threading {threading.current_thread().name} is running')
threads = [] #### 用于存放MyThread的一个list
for i in [1, 5]:
thread = MyThread(i)
threads.append(thread) #### list什么都可以存,包括实例化类对象
thread.start()
for thread in threads:
thread.join()
print(f'Threading {threading.current_thread().name} is ended')
Threading MainThread is running
Threading Thread-6 is running
Threading Thread-6 sleep 1s
Threading Thread-7 is running
Threading Thread-7 sleep 5s
Threading Thread-6 is ended
Threading Thread-7 is ended
Threading MainThread is ended
互斥锁
线程之间是可以共享资源的,但是有些共享资源不能够同时多个线程访问的,比如,打印机,打印机一时间只能有一个线程占着。**怎么样利用多个线程轮流对一个全局变量进行加法处理呢?**这个时候需要设置互斥锁才会没问题的,下面分别列举了没设置互斥锁和设置了互斥锁的结果。
### 比如在一个进程中,有一个全局变量 count 用来计数,现在我们声明多个线程,
### 每个线程运行时都给 count 加 1,让我们来看看效果如何。
### 这里设置了1000尺循环,按常理说最终结果是1000。
# count = 0
# class MyThread(threading.Thread):
# def __init__(self):
# threading.Thread.__init__(self)
# def run(self):
# global count
# temp = count + 1
# time.sleep(0.001)
# count = temp
# threads = []
# for _ in range(1000):
# thread = MyThread()
# thread.start()
# threads.append(thread)
# for thread in threads:
# thread.join()
# print(f'Final count: {count}')
### 以上运行结果不可能是1000的,因为多个线程几乎是同时执行的,
### 一个时间点的 count 会被多个线程拿来加,但线程返回 count 的值时候返回的值是同一个值,
### 也就是说加了多次相等于只加一次,所以结果肯定偏小
### 此时互斥锁就派上用场了
### 互斥锁能够严格限制资源同一时间只能一个线程访问。
### 当那个线程访问完了,才能够被另一个线程访问。
count = 0
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self): #### 在 run方法里面,获取 count 前先加锁,修改完 count 之后再释放锁,
#### 这样多个线程就不会同时获取和修改 count 的值了
global count
lock.acquire() #### 关锁,公共资源禁止其他线程访问。
temp = count + 1
time.sleep(0.001)
count = temp
lock.release() #### 开锁,刚完事后,公共资源允许其他人访问。
lock = threading.Lock() #### 这里声明了一个 lock 对象,其实就是 threading.Lock 的一个实例
threads = []
for _ in range(1000):
thread = MyThread()
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
print(f'Final count: {count}')
Python 多线程的问题
由于 Python中 GIL的限制,导致不论是在单核还是多核条件下,在同一时刻只能运行一个线程,导致 Python多线程无法发挥多核并行的优势。GIL全称为 GlobalInterpreter Lock,中文翻译为全局解释器锁,其最初设计是出于数据安全而考虑的。
在 Python多线程下,每个线程的执行方式如下:
- 获取 GIL 执行对应线程的代码
- 释放 GIL
可见,某个线程想要执行,必须先拿到 GIL,我们可以把 GIL看作是通行证,并且在一个 Python进程中,GIL只有一个。拿不到通行证的线程,就不允许执行。这样就会导致,即使是多核条件下,一个 Python 进程下的多个线程,同一时刻也只能执行一个线程。
不过对于爬虫这种 IO 密集型任务来说,这个问题影响并不大。而对于计算密集型任务来说,由于 GIL的存在,多线程总体的运行效率相比可能反而比单线程更低。