python多线程为什么不适合计算密集型操作?
一、先说是不是
不使用多线程的冒泡排序
下面的代码我们创建了五个list,并使用遍历的方法对其每一个进行冒泡排序。
import time
ARRAY_NUM = 5
ARRAY_SIZE = 5000
def bubble_sort(arr):
for i in arr:
for j in range(0, len(arr) - i - 1):
if arr[j] > arr[j + 1]:
tmp = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = tmp
if __name__ == '__main__':
arrs = []
ths = []
for c in range(ARRAY_NUM):
arr = []
for d in range(ARRAY_SIZE):
arr.append(ARRAY_SIZE - d)
arrs.append(arr)
time_start = time.time()
for r in range(ARRAY_NUM):
bubble_sort(arrs[r])
time_end = time.time()
print(time_end - time_start)
所需时间:
/usr/bin/python3.8 /home/loubw/PycharmProjects/tst/time_test.py
6.275976657867432
使用多线程的冒泡排序
import time
import threading
ARRAY_NUM = 5
ARRAY_SIZE = 5000
def bubble_sort(arr):
for i in arr:
for j in range(0, len(arr) - i - 1):
if arr[j] > arr[j + 1]:
tmp = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = tmp
if __name__ == '__main__':
arrs = []
ths = []
for c in range(ARRAY_NUM):
arr = []
for d in range(ARRAY_SIZE):
arr.append(ARRAY_SIZE - d)
arrs.append(arr)
time_start = time.time()
for r in range(ARRAY_NUM):
ths.append(threading.Thread(target=bubble_sort, kwargs={
"arr": arrs[r]
}))
ths[r].start()
for r in range(ARRAY_NUM):
ths[r].join()
time_end = time.time()
print(time_end-time_start)
所需时间:
/usr/bin/python3.8 /home/loubw/PycharmProjects/tst/main.py
6.432690858840942
结论
python的多线程确实不适合计算密集型的(即使用cpu频繁)任务。
二、再问为什么
一、全局解释器锁
简单地说,Cpython解释器的内存管理是线程不安全的,为了避免竞争带来的错误,Cpython使用GIL来保证同一时刻只能有一个线程在执行,这就使得Cpython并不能发挥多核cpu可以并行线程的优势。
二、为什么这个多线程的例子时间达到了预期效果?
二①、不使用多线程的sleep
import time
import threading
ARRAY_NUM = 5
ARRAY_SIZE = 50000
def sleep_3s():
time.sleep(3)
if __name__ == '__main__':
time_start = time.time()
for r in range(ARRAY_NUM):
sleep_3s()
time_end = time.time()
print(time_end-time_start)
所需时间:
/usr/bin/python3.8 /home/loubw/PycharmProjects/tst/time_test_sleep.py
15.009067058563232
二②、使用多线程的sleep
import time
import threading
ARRAY_NUM = 5
ARRAY_SIZE = 50000
def sleep_3s():
time.sleep(3)
if __name__ == '__main__':
ths = []
time_start = time.time()
for r in range(ARRAY_NUM):
ths.append(threading.Thread(target=sleep_3s))
ths[r].start()
for r in range(ARRAY_NUM):
ths[r].join()
time_end = time.time()
print(time_end-time_start)
所需时间:
/usr/bin/python3.8 /home/loubw/PycharmProjects/tst/time_test_sleep_mthread.py
3.003570079803467
二③、结论
在进行IO密集型(线程处于阻塞情况比较多)的任务时,可以有效利用cpu,接下来我们将简介什么是
线程的阻塞状以及为什么我们在这里仅仅用了sleep充当计算机IO就可以下结论说进行IO任务多线程可以有效利用cpu。
三、多线程在C语言中的表现
三①、C语言中不使用多线程的冒泡排序
#include "stdio.h"
#include "pthread.h"
#include "stdlib.h"
#include "sys/time.h"
#define ARRAY_SIZE 50000
#define ARRAY_NUM 5
typedef struct {
int* arr;
int size;
} num_array;
void *BubbleSort(void *data)
{
num_array *p = (num_array *)data;
int *arr = p->arr;
int size = p->size;
int i, j, tmp;
for (i = 0; i < size - 1; i++) {
for (j = 0; j < size - i - 1; j++) {
if (arr[j] > arr[j+1]) {
tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
return NULL;
}
double get_us(struct timeval t){
return (double )(t.tv_sec*1000000 + t.tv_usec);
}
int main(){
int arr[ARRAY_NUM][ARRAY_SIZE];
num_array *a_s[ARRAY_NUM];
for (int j=0;j<ARRAY_NUM;j++){
for(int i=0;i<ARRAY_SIZE;i++){
arr[j][i] = ARRAY_SIZE-i;
}
a_s[j] = malloc(sizeof(num_array)) ;
a_s[j]->size = ARRAY_SIZE;
a_s[j]->arr = arr[j];
}
clock_t begin = clock();
struct timeval start_time,end_time;
gettimeofday(&start_time,NULL);
for(int i=0;i<ARRAY_NUM;i++){
BubbleSort((void *)a_s[i]);
}
gettimeofday(&end_time,NULL);
clock_t end = clock();
double time_consumption = (double)(end - begin) / CLOCKS_PER_SEC;
printf("Not Use MThread Time use %f ms\n", (get_us(end_time)- get_us(start_time))/1000);
printf("Not Use MThread cpu time use %f s\n", time_consumption);
}
所需时间:
loubw@loubw-ThinkCentre-M920t-N000:~/CLionProjects/untitled/torp$ ./a.out
Not Use MThread Time use 19217.993000 ms
Not Use MThread cpu time use 19.214332 s
三②、C语言中使用多线程的冒泡排序
#include "stdio.h"
#include "pthread.h"
#include "stdlib.h"
#include "sys/time.h"
#define ARRAY_SIZE 50000
#define ARRAY_NUM 5
typedef struct {
int* arr;
int size;
} num_array;
pthread_t ths[ARRAY_NUM];
void *BubbleSort(void *data)
{
num_array *p = (num_array *)data;
int *arr = p->arr;
int size = p->size;
int i, j, tmp;
for (i = 0; i < size - 1; i++) {
for (j = 0; j < size - i - 1; j++) {
if (arr[j] > arr[j+1]) {
tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
return NULL;
}
double get_us(struct timeval t){
return (double )(t.tv_sec*1000000 + t.tv_usec);
}
int main(){
int arr[ARRAY_NUM][ARRAY_SIZE];
num_array *a_s[ARRAY_NUM];
for (int j=0;j<ARRAY_NUM;j++){
for(int i=0;i<ARRAY_SIZE;i++){
arr[j][i] = ARRAY_SIZE-i;
}
a_s[j] = malloc(sizeof(num_array)) ;
a_s[j]->size = ARRAY_SIZE;
a_s[j]->arr = arr[j];
}
clock_t begin = clock();
struct timeval start_time,end_time;
gettimeofday(&start_time,NULL);
for(int i=0;i<ARRAY_NUM;i++){
pthread_create(&(ths[i]), NULL, BubbleSort, (void *)a_s[i]);
}
for(int i=0;i<ARRAY_NUM;i++){
pthread_join(ths[i], NULL);
}
gettimeofday(&end_time,NULL);
clock_t end = clock();
double time_consumption = (double)(end - begin) / CLOCKS_PER_SEC;
printf("Use MThread Time use %f ms\n", (get_us(end_time)- get_us(start_time))/1000);
printf("Use MThread cpu time use %f s\n", time_consumption);
}
所需时间:
loubw@loubw-ThinkCentre-M920t-N000:~/CLionProjects/untitled/torp$ ./a.out
Use MThread Time use 3936.645000 ms
Use MThread cpu time use 19.521638 s
三③、结论
- 多线程在C语言发挥了自己的作用,原本需要19.2s的程序在多线程的运行下减少到了3.9s。
- 注意输出的时间中的第二个是使用的cpu时间,而不是我们所说的时间。
- 在实践的过程中,我对比了python和c进行五个50000个数字的数组的冒泡排序,C需要19s,而python居然需要600s
我检查完程序认为自己没写错,没办法就把python的50000改成了5000。如果您的测试中python不需要这么多时间或者发现了我的程序错误,评论通知我,感谢。
线程的五种状态
- 新建状态
创建一个线程。 - 就绪状态
线程完成创建,等待进入cpu执行。 - 运行状态
得到cpu的执行权,进入cpu执行,在cpu的一个时间片执行完毕后,线程或进入就绪状态或进入阻塞状态。 - 阻塞状态
阻塞状态下线程让出cpu,等阻塞状态结束后进入就绪状态等待cpu调度。
阻塞大概有以下几种情况- 主线程调用join等待子线程结束时,主线程会进入阻塞状态。
- 等待IO时,比如c的read()(等待用户输入)。
- sleep()也会导致现线程进入阻塞(C、python)。
这就是为什么上面的sleep获得了理想的执行时间,而IO操作也是同理。
- 死亡状态
线程执行完毕。
三、什么是线程?
大概理解一下,要知道线程的具体执行原理还需要继续看书。
一、是什么
线程本质上就是函数的执行。每个进程总会有一个线程,对于C语言程序来说就是main函数为主线程。
二、与进程的区别
每个进程都有自己的地址空间和(通常)一个线程,在C程序编译为可执行文件后,执行此文件,此时在内存中这个进程具有它自己的
- 文本段:存放CPU需要执行的机器指令,通常是所有线程共享但只读的
- BSS段:存放未初始化的全局和静态变量
- 数据段:存放初始化过的全局和静态变量
- 堆区:用于动态内存分配,线程共享
- 栈区:存放函数的参数、返回、局部变量以及该函数使用的寄存器信息等,线程不共享
但进程虽然管理了程序运行的所有资源,但把main函数中的一条条指令放到cpu执行的却是它中的主线程。
也就是说,线程把需要执行的代码放到了寄存器,CPU调度执行。
三、线程的共享资源
文本段、数据段、BSS段、堆区
四、线程的独占资源
栈区、函数运行需要的寄存器、程序计数器(是一个寄存器,通过恢复程序计数器就知道接下来要执行哪个指令)、栈指针