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个内核同时运行,是真正的多线程