threading包比thread提供的功能更全面,所以这里使用threading为例
不过本文不想过多讨论基础操作, 我比较好奇的是Python的GIL和线程安全问题(Java写多了)
import threading
def say(name):
for i in range(5):
print("from thread "+str(name));
t1 = threading.Thread(target=say,args=("1"));
t2 = threading.Thread(target=say,args=("2"));
t1.start();
t2.start();
上面这个例子会交替打印 message1 和message2
我们稍微做一下改变,让这两个线程一直循环下去好观察cpu的占用率
import threading
def say(name):
while(True):
print("from thread "+str(name));
t1 = threading.Thread(target=say,args=("1"));
t2 = threading.Thread(target=say,args=("2"));
t1.start();
t2.start();
因为window7本身是内核级进程(也是大多数Linux现在的默认设置),所以可以很清楚的观察到两个线程被平均分配到了4个内核中(我的环境是i7 4内核)
程序启动以后,4个核心的占用率直接飙升

即使使用一个线程进行while(true)循环你也会看到4个内核占用率同时提高,因为操作系统使用的线程实际上是一种轻量级进程
我们来测试一下使用python多线程执行密集型运算
import threading;
import sys;
import math;
import time;
from decimal import *;
def bellard(n):
pi=Decimal(0)
k=0
while k < n:
pi+=(Decimal(-1)**k/(1024**k))*( Decimal(256)/(10*k+1)+Decimal(1)/(10*k+9)-Decimal(64)/(10*k+3)-Decimal(32)/(4*k+1)-Decimal(4)/(10*k+5)-Decimal(4)/(10*k+7)-Decimal(1)/(4*k+3))
k+=1
pi=pi*1/(2**6)
return pi
def say(name):
start = time.clock();
for i in range(100):
p = bellard(1000);
# print("running..."+str(p));
end = time.clock();
print ("read: %f s" % (end - start));
t1 = threading.Thread(target=say,args=("1"));
t2 = threading.Thread(target=say,args=("2"));
t1.start();
t2.start();
双线程运行pi计算上面的代码会给出
read: 51.493309 s read: 51.516034 s |
改成单线程之后会得到如下结果
read: 24.826255 s |
你会惊喜的发现时间缩短一倍, 那我们猜测如果3个线程的话会得到3倍左右的运算时间
read: 75.466136 s read: 75.925743 s read: 76.911878 s |
Python使用一种叫做GIL ( 全局解释器锁 ) 的机制来调度线程
在主轮询中(这里鄙视一下某些书的翻译,把轮询翻译成循环) 同时只能有一个线程执行
有点像单核cpu执行多线程的模式,然后而又有点区别, java在单核cpu多线程时也需要考虑线程安全,因为有些变量是多线程之间共享的
Python这种一刀切的方式,干脆相当于在所有方法前都加上了 synchronized 同步
这里还有一个问题要强调一下, java 中 volatile 并不能保证变量是线程安全的
volatile 只是告诉虚拟机在每次使用这个变量时,去堆内存中重新读一下, 如果主内存变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化
这里我们用java的多线程模型做对比
java没有GIL, 是真实的多线程,也就是说一个线程在4内核上需要10秒钟的运算,两个线程在4内核上也需要10秒,因为java可以真正同时使用两个内核进行运算(两个内核各运行10秒)
这里需要注意以下几点
1. 不要在循环中使用System.out.print输出任何字符,因为会造成IO消耗,等待时间大部分都是IO造成的,测试不准确
2. 不要使用惯用的sleep方法来测试,sleep方法只是计时器,不能造成cpu密集型运算
public class Test {
static Runnable r = new Runnable(){
@Override
public void run() {
long startTime=System.currentTimeMillis();
for(int i=0;i<10000000;i++){
cut(20L);
}
System.out.println("running...");
long endTime=System.currentTimeMillis();
System.out.println("程序运行时间: "+(endTime-startTime)+"ms");
}
};
public static void main(String args[]) throws InterruptedException{
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
static void cut(Long n){
double y=1.0;
for(Long i=0L;i<=n;i++){
double π=3*Math.pow(2, i)*y;
// System.out.println("第"+i+"次切割,为正"+(6+6*i)+"边形,圆周率π≈"+π);
y=Math.sqrt(2-Math.sqrt(4-y*y));
}
}
}
上面的程序在调用两个线程时会给出下面的结果
running… 程序运行时间: 14933ms running… 程序运行时间: 14937ms |
在调用三个线程时会给出
running… 程序运行时间: 15639ms running… 程序运行时间: 15654ms running… 程序运行时间: 15689ms |
调用四个线程
running… 程序运行时间: 16069ms running… 程序运行时间: 16099ms running… 程序运行时间: 16154ms running… 程序运行时间: 16197ms |
这个时候我的4内核cpu应该已经处于饱和状态了,最后我们尝试调用8个线程
running… 程序运行时间: 31569ms running… 程序运行时间: 32132ms running… 程序运行时间: 32156ms running… 程序运行时间: 32166ms running… 程序运行时间: 32132ms running… 程序运行时间: 32208ms running… 程序运行时间: 32370ms running… 程序运行时间: 32321ms |
正如所料,消耗了差不多4线程的两倍时间,因为我的cpu已经饱和了
java的jvm实际上只有一个进程,但是由于使用了内核级线程(轻量级进程),操作系统会把运算工作分发给4个内核同时运行,是真正的多线程