阅读Android源码的时候,发现有线程相关的东西,有点陌生了。所以重新温习一下POSIX标准的线程的几种使用方式,本文不涉及深层次原理性的解读,纯粹提供集中线程使用代码,以供熟悉API使用。
进程与线程
做应用开发的时候,对进程和线程的理解非常有限,只是知道进程是应用运行的基本单位,而线程是CPU任务调用的基本单位,一个进程可以包含多个线程等。随着工作逐渐接触内核之后发现,应用层面的认识在内核中有不同的根本性的解读。
进程:如果简而言之的话(或许在面试的时候可以这么回答),进程就是一个正在运行的程序实体。进一步的解释可能要从内核角度看了,在内核看来,进程意味着一个实体,这些实体在初始化时占有或者共享一定的系统资源,在其生命周期中会对占有的资源进程维护,对共享的资源进行调度,在生命周期结束后,内核会回收其所占有的资源。这些资源包括初始化时会占有的内存、调度时共享的CPU、网络等。``
进程的内存分布主要如下:
- 文本段:存放代码数据
- 数据段:存放静态数据
- 堆:可扩展内存,用于存放new出来的数据
- 栈:记录局部变量、函数调用链等数据
进程的创建有两种方式:
fork()
实现:被fork出的进程被称为fork函数调用进程的子进程,调用进程被称为父进程。为了区别进程,每个进程都有自己的进程id即PID,同时进程有一个字段PPID,用于表示当前进程的父进程是什么。内核通过复制父进程的方式创建子进程。子进程会继承父进程除文本段外的所有数据,共享父进程文本段数据。子进程创建以后,对自身数据段、堆栈的数据修改将不会影响到父进程。其实通常的做法是,子进程创建拥有了自己的虚拟地址空间,但其中的数据段、堆栈对映的物理地址和父进程是相同的,一旦子进程对数据有修改,才会为子进程重新分配物理空间。
execve()
实现:另外可以通过执行程序的方式,创建一个新进程,通过execve()
族函数实现,具体函数和平台有关。这种方式创建的进程不再从创建进程处继承数据,而是加载新的文本段。
不管是什么方式创建的进程,创建新进程的进程都被称为父进程,新进程被称为子进程,那么最开始的那个祖宗进程是谁?由谁创建?答案是init
进程,这是个极其特殊的进程,Linux kernel在启动的最后,会启动用户空间的第一个进程,也就是init
,它的进程id始终为1。随后init
会fork出其它用户空间进程,于是,init
进程和它的子孙进程一起组成了一个树状结构,这也是进程树概念的由来。
线程:可以看做是一种特殊形式的进程,特殊之处在于,除了有自己单独的栈空间用于存放局部变量和函数调用外,共享进程的所有数据(文本段、数据段、堆)。所以对于当线程对共享数据访问时需要足够注意。线程共享变量之间的控制,主要通过Mutex
(互斥量)、pthread_cond_t
条件变量实现,后面会有详细解读。还有什么呢,对了,进程默认会有一个主线程,嗯。
有了对进程、线程的原理性认知之后,应用开发时需要刻意记住的规则,变得有迹可循起来。
线程的创建和运行
POSIX中的线程,需要引入pthread的头文件:#include <pthread.h>
线程的一般使用步骤是:
-
声明线程函数:作为线程被CPU调度时需要执行的函数,其函数原型如下
void *downloadfile(void *filename) { // 需要执行的代码 pthread_exit((void *)data); // 可选项:当线程退出时,返回给调用者数据时使用 }
pthread_exit((void *)data)
:一个运行中的函数可以通过调用该函数退出线程。函数参数data需要被转为一个(void *)
类型,这是线程退出的返回值。
-
声明线程对象:线程对象类型为
pthread_t
,是一个线程区别于其它线程的标志。该对象会在创建线程阶段被初始化。 -
声明线程属性:线程属性类型为:
pthread_attr_t
,线程属性的声明、初始化、赋值一般都在创建线程前,前后同一段时间执行。TODO:默认是什么?NULLpthread_attr_t thread_attr; // 线程属性声明 pthread_attr_init(&thread_attr); // 线程属性初始化 pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_JOINABLE); // 线程属性赋值
属性声明和初始化函数没什么好说的,需要注意的是
pthread_attr_setdetachstate
函数的第二个参数。通常有两个可选参数:PTHREAD_CREATE_JOINABLE
:该属性的线程被终止时,会保留虚拟内存、堆栈等资源,由父线程释放,释放前父线程可以获取子线程的返回值。主线程将会等待子线程执行完毕,才会最终退出。PTHREAD_CREATE_DETACHED
:该属性的线程终止后,资源由系统自动回收,父线程将无法获得子线程的返回值。
-
创建线程:创建线程,由
pthread_create
函数实现,线程创建成功后,得到CPU时间后就开始执行线程函数了。原型如下:int pthread_create(pthread_t *th, const pthread_attr_t *attr, void *(* func)(void *), void *arg);
四个参数的含义分别如下:
- th:前面申明的线程对象指针,用于给该对象赋值。
- attr:线程属性指针,用于规定新创建线程的相关行为。
- void *(* func)(void *):函数指针,线程执行时,执行该指针指向的函数。
- arg:线程函数调用对应的参数。
-
销毁线程属性:不用的内存就销毁,养成好习惯
int pthread_attr_destroy(pthread_attr_t *attr);
-
等待线程结束:主线程执行(默认线程)到这里,会等待新创建的子线程退出,退出时,可以通过第二个参数,将子线程返回值返回给调用者。
int pthread_join(pthread_t t, void **res);
-
主线程结束:可以直接通过调用该函数退出线程,参数用于将子线程返回值返回,不需要返回值可以为
NULL
void pthread_exit(void *res);
综上,一个线程最基本的使用伪代码如下:
#include <pthread.h>
// 1. 声明线程函数
void *fun_run(void* param) {
long progress;
pthread_exit((void *) progress);
}
pthread_t t // 2. 申明线程对象
pthread_attr_t attr; // 3. 声明并初始化线程属性
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
// 4.创建线程
pthread_create(t, attr, fun_run, param);
pthread_attr_destroy(attr); // 5. 回收线程属性内存
int downloadtime;
pthread_join(t, (void**)&downloadtime); // 等待线程执行完毕,并获取返回值
pthread_exit(nullptr); // 退出主线程 非必须
以下是一段使用线程的完整代码,可直接复制编译运行:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
#define NUM_OF_TASKS 5
void *downloadfile(void *filename) { // 线程函数,执行下载任务
printf("I am downloading the file %s!\n", (char *)filename);
// sleep(10);
_sleep(10);
long downloadtime = rand() % 100; // 随机一个下载时间
printf("I finish downloading the file within %d minutes!\n", downloadtime);
pthread_exit((void *)downloadtime); // 线程退出,并返回返回下载所需时间
}
int main(int argc, char *argv[]) {
char files[NUM_OF_TASKS][20]={"file1.avi","file2.rmvb","file3.mp4","file4.wmv","file5.flv"};
pthread_t threads[NUM_OF_TASKS]; // 声明5个线程
int rc;
int t;
int downloadtime;
pthread_attr_t thread_attr; // 线程属性声明
pthread_attr_init(&thread_attr); // 线程属性初始化
pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_JOINABLE); // 线程属性赋值为:需要等待线程结束
for(t = 0; t < NUM_OF_TASKS; t++) {
printf("creating thread %d, please help me to download %s\n", t, files[t]);
rc = pthread_create(&threads[t], &thread_attr, downloadfile, (void *)files[t]); // 创建之前申明的5个线程
if (rc){
printf("ERROR; return code from pthread_create() is %d\n", rc);
exit(-1);
}
}
pthread_attr_destroy(&thread_attr); // 销毁线程属性
for(t = 0; t < NUM_OF_TASKS; t++){
pthread_join(threads[t],(void**)&downloadtime); //
printf("Thread %d downloads the file %s in %d minutes.\n",t,files[t],downloadtime);
}
pthread_exit(NULL);
}
上述代码执行结果为:
creating thread 0, please help me to download file1.avi
creating thread 1, please help me to download file2.rmvb
I am downloading the file file1.avi!
creating thread 2, please help me to download file3.mp4
I am downloading the file file2.rmvb!
creating thread 3, please help me to download file4.wmv
I am downloading the file file3.mp4!
creating thread 4, please help me to download file5.flv
I am downloading the file file4.wmv!
I am downloading the file file5.flv!
I finish downloading the file within 41 minutes!
I finish downloading the file within 41 minutes!
I finish downloading the file within 41 minutes!
I finish downloading the file within 41 minutes!
I finish downloading the file within 41 minutes!
Thread 0 downloads the file file1.avi in 41 minutes.
Thread 1 downloads the file file2.rmvb in 41 minutes.
Thread 2 downloads the file file3.mp4 in 41 minutes.
Thread 3 downloads the file file4.wmv in 41 minutes.
Thread 4 downloads the file file5.flv in 41 minutes.
线程数据
线程数据被分为三类,1、线程栈中的本地数据。2、线程私有数据。3、进程共享全局数据,分别看看它们的特点于应用。
线程栈本地数据
本地数据比较容易理解,每个线程都有一块叫做线程栈的内存空间。在线程中,临时申明的变量、函数之间的调用关系等都会被存入栈空间中。这个栈所占空间的大小有一个系统默认值(8192即8M),可以通过ulimt -a/s
查看:
homer:/ # ulimit -s
8192
homer:/ # ulimit -a
-t: time(cpu-seconds) unlimited
-f: file(blocks) unlimited
-c: coredump(blocks) 0
-d: data(KiB) unlimited
-s: stack(KiB) 8192
-l: lockedmem(KiB) 65536
-n: nofiles(descriptors) 32768
-p: processes 7816
-i: sigpending 7816
-q: msgqueue(bytes) 819200
-e: maxnice 40
-r: maxrtprio 0
-m: resident-set(KiB) unlimited
-v: address-space(KiB) unlimited
-x: filelocks unlimited
一个进程中,每个线程栈中间都有一小块隔离区,当线程调用超过了自己的栈空间范围,触碰到了隔离区,则会引起断错了。如果线程栈空间不够用,可以通过线程属性,修改线程的栈空间大小:
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
线程私有数据
一个进程中的线程,除了自己的栈空间外,其它空间都是线程之间共享的。如果这个时候需要一个线程的全局变量(基于在不同的函数之间传递数据所需)怎么办?这就要用到线程私有数据类型了(Thread Specific Data)。
顾名思义,就是线程自己独享的数据,它有一个特点是:一次创建,可供多个线程使用。区别在于,它的值由各自线程维护和使用。相关函数调用如下:
-
创建变量:
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*))
key:将被创建的私有数据类型变量
void (*destructor)(void*):清理函数,在线程释放该线程存储时调用。如果该函数为NULL,将会调用默认的清理函数。
-
各线程中为变量赋值:
int pthread_setspecific(pthread_key_t key, const void *value)
key:被赋值的变量
value:变量值
-
各线程中从变量取值:
void *pthread_getspecific(pthread_key_t key)
返回变量之前取的值
一个完整(可以直接复制编译通过)的私有数据demo如下:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
using namespace std;
pthread_key_t key;// 全局变量
struct person {
int age;
string name;
};
void *run1(void *arg) {
struct person homer;
_sleep(3);
homer.age = 10;
homer.name = "homer";
pthread_setspecific(key, &homer); // 为全局线程私有变量赋值
printf("run1 data ptr is --> 0x%p\n", &(homer));
printf("run1 data ptr frome pthread_getspecific(key) is --> 0x%p\n", (person *)pthread_getspecific(key));
printf("run1 data value is %s:%d\n",
((person *)pthread_getspecific(key))->name, ((person *)pthread_getspecific(key))->age);
}
void *run2(void *arg) {
int temp = 20;
_sleep(2);
pthread_setspecific(key, &temp); // 好吧,原来这个函数这么简单
printf("run2 data ptr is --> 0x%p\n", &temp);
printf("run2 data ptr frome pthread_getspecific(key) is --> 0x%p\n", (int *)pthread_getspecific(key));
printf("run2 data value is %d\n", *((int *)pthread_getspecific(key)));
}
int main(int argc, char *argv[]) {
pthread_t t1, t2;
pthread_key_create(&key, NULL); // 这里是构建一个pthread_key_t类型,确实是相当于一个key
pthread_create(&t1, NULL, run1, NULL);
pthread_create(&t2, NULL, run2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_key_delete(key);
return (0);
}
执行后的结果如下:
run2 data ptr is --> 0x02A5FF04
run2 data ptr frome pthread_getspecific(key) is --> 0x02A5FF04
run2 data value is 20
run1 data ptr is --> 0x0281FEEC
run1 data ptr frome pthread_getspecific(key) is --> 0x0281FEEC
run1 data value is homer:10
进程全局数据
进程中的全局变量,顾名思义,就是整个进程共享的。进程的子线程都可以访问,所以就需要一个机制,防止线程之间对该共享变量肆意访问造成数据的不确定性。这就要引入数据保护机制,即锁的概念了。
进程共享变量锁
共享数据的保护,使用的是Mutex,全称为Mutual Exclusion,很多地方都把它叫做互斥锁。但是对它的解释我有点不太能理顺,我有一版自己的理解。这里就说说我的理解,不喜欢的可以评论区讨论。
我的理解是,在锁机制中,还有一个钥匙的概念(可以抽象为一种访问权限),钥匙并不实际存在对应的一个对象类,但是却是打开互斥锁的关键。一个互斥锁对象对应一把钥匙。在共享变量访问前,相关的代码段通过pthread_mutex_lock
函数,申请了一把对应的钥匙,并把该代码段用锁锁起来,此时锁是关闭状态,只有拥有钥匙的代码段才能够打开,其它代码段再调用pthread_mutex_lock
函数就无法获取钥匙开锁,于是只能等待。拥有钥匙的代码段执行完成之后,通过pthread_mutex_unlock
函数,将锁打开,然后将钥匙归还,以便其他代码段获取钥匙。
互斥锁相关的类和函数顺序一般如下:
- 声明互斥锁变量:互斥锁变量通常是全局变量,类型为
pthread_mutex_t
,申明伪代码:pthread_mutex_t lock;
- 初始化锁变量:
pthread_mutex_init(&lock, NULL);
第二个参数为互斥锁属性,暂时没见到应用场景,不予讨论。 - 对操作共享变量的代码段上锁:
pthread_mutex_lock(&lock);
上锁后,代码段获得了唯一的钥匙,其它线程再通过该函数上锁时将因无法获取钥匙而等待,达到保护共享变量数据的目的。 - 操作完成,开锁,释放钥匙:
pthread_mutex_unlock(&lock);
- 释放互斥锁空间:
pthread_mutex_destroy(&lock);
下面是一段典型的应用代码:模拟异常交易,jerry给tom在不同的地方同时打钱,在多线程中极易发生在某一时刻,两者账户资金总量只合与初始值不等的情况。锁保护后,即可保证所有时刻,资金总量保持不变。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
int money_of_tom = 100;
int money_of_jerry = 100;
pthread_mutex_t lock;
void *transfer(void *notused) {
pthread_t tid = pthread_self();
printf("Thread %u is transfering money!\n", (unsigned int)tid);
pthread_mutex_lock(&lock); // 获取钥匙,开锁。没获取到则一直等待
_sleep(rand() % 10);
money_of_tom += 10;
_sleep(rand() % 10);
money_of_jerry -= 10;
pthread_mutex_unlock(&lock); // 释放锁钥匙,让其它地方可以获取钥匙
printf("Thread %u finish transfering money!\n", (unsigned int)tid);
pthread_exit((void *)0);
}
int main(int argc, char *argv[]) {
pthread_t t1;
pthread_t t2;
int t;
pthread_mutex_init(&lock, NULL); // 初始化锁
pthread_create(&t1, NULL, transfer, NULL); // 创建线程,模拟异地交易
pthread_create(&t2, NULL, transfer, NULL); // 创建线程,模拟异地交易
for(t = 0; t < 10; t++){ // 不断查询两个人钱的总数,也需要获取钥匙
pthread_mutex_lock(&lock);
printf("money_of_tom + money_of_jerry = %d\n", money_of_tom + money_of_jerry);
pthread_mutex_unlock(&lock);
}
pthread_mutex_destroy(&lock); // 释放互斥锁
pthread_exit(NULL);
}
对应的打印如下:
money_of_tom + money_of_jerry = 200
money_of_tom + money_of_jerry = 200
money_of_tom + money_of_jerry = 200
money_of_tom + money_of_jerry = 200
money_of_tom + money_of_jerry = 200
money_of_tom + money_of_jerry = 200
money_of_tom + money_of_jerry = 200
money_of_tom + money_of_jerry = 200
money_of_tom + money_of_jerry = 200
money_of_tom + money_of_jerry = 200
Thread 2 is transfering money!
Thread 3 is transfering money!
Thread 2 finish transfering money!
Thread 3 finish transfering money!
上面的代码,如果将锁相关的行注释掉,可能会产生不一样的结果。
共享变量锁与条件变量
上一节关于互斥锁的应用有个问题,那就是如果一个线程在调用pthread_mutex_lock
函数时,获取不到钥匙,它就会一直阻塞在哪里,直到其它线程释放钥匙。另外一种获取钥匙的函数是pthread_mutex_trylock
:该函数的特点是,调用线程回去尝试获取钥匙,如果获取不到则直接返回。
有一个场景是,当一个线程运行需要达到某种条件才能运行时,即使获得钥匙,因为没有满足条件就只能释放钥匙。只要条件不满足,就会一直去重复获取锁、检查条件不满足、释放锁的循环,这会消耗大量的本应分配给其它线程的CPU资源。条件变量的引入,可以解决这个问题。
条件变量需要被锁保护,当线程获得钥匙后,会检查条件是否满足,如果不满足就会释放钥匙,线程还是阻塞在这里,区别时线程会进入休眠状态,等待条件满足后被唤醒执行。休眠的线程让有条件执行的线程可以获得CPU资源,这种类似通知的模型大大解约了资源。
通知其实是靠条件变量来实现的。先来介绍一下条件变量的相关结构体和函数:
typedef void *pthread_cond_t;
int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *a);
int pthread_cond_destroy(pthread_cond_t *cv);
int pthread_cond_signal(pthread_cond_t *cv);
int pthread_cond_broadcast(pthread_cond_t *cv);
int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *external_mutex);
int pthread_cond_timedwait(pthread_cond_t *cv, pthread_mutex_t *external_mutex, const struct timespec *t);
pthread_cond_t
是一个由各平台自己实现,POSIX规定,需要使用pthread_cond_init()
函数初始化。重点关注一下pthread_cond_wait()
函数,通过它可以解释大多数条件变量使用的问题。
该函数有两个参数,第一个参数就是条件变量指针,第二个参数是一个互斥量类型指针pthread_mutex_t *
。
重点需要关注的是pthread_cond_wait
函数,该函数通常和pthread_mutex_lock
一起使用:
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
这两个函数的含义如下:
- 首先获得锁钥匙
- 判断条件是否满足,如果不满足则释放钥匙,线程阻塞并进入休眠状态等待被唤醒。
- 条件变量唤醒线程时,会从
pthread_cond_wait
函数往下执,接着判断条件是否满足。回到第二部的流程。
所以,通常pthread_cond_wait
函数都是被放在一个条件循环语句中执行的。
pthread_cond_signal()
和pthread_cond_broadcast()
都能唤醒被pthread_cond_wait()
阻塞的线程。区别在于前者唤醒一个线程,而后者唤醒所有相同条件变量的线程。pthread_cond_destroy()
用于释放条件变量。pthread_cond_timedwait()
和pthread_cond_wait()
类似,只是多了一个超时时间。
下面介绍一个具体的例子(来源刘超的操作系统系列课程):三个线程代表三个员工,一个老板。老板给员工派发任务,任务是有限的,员工需要抢活干。建议先看主函数部分。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_OF_TASKS 3
#define MAX_TASK_QUEUE 11
char tasklist[MAX_TASK_QUEUE]="ABCDEFGHIJ";
int head = 0;
int tail = 0;
int quit = 0;
pthread_mutex_t g_task_lock;
pthread_cond_t g_task_cv; // 申明信号变量
void *coder(void *notused) {
pthread_t tid = pthread_self();
while(!quit){ // 只要员工没被开,就一直要抢任务做
pthread_mutex_lock(&g_task_lock); // 共享变量访问,抢钥匙
while(tail == head){ // 看一下有没有任务
if(quit){
pthread_mutex_unlock(&g_task_lock);
pthread_exit((void *)0);
}
printf("No task now! Thread %u is waiting!\n", (unsigned int)tid);
pthread_cond_wait(&g_task_cv, &g_task_lock); // 没有任务,线程阻塞
printf("Have task now! Thread %u is grabing the task !\n", (unsigned int)tid);
}
char task = tasklist[head++]; // 有任务,当前线程抢到了任务
pthread_mutex_unlock(&g_task_lock); // 解锁,完成任务
printf("Thread %u has a task %c now!\n", (unsigned int)tid, task);
_sleep(5);
printf("Thread %u finish the task %c!\n", (unsigned int)tid, task);
}
pthread_exit((void *)0);
}
int main(int argc, char *argv[]) {
pthread_t threads[NUM_OF_TASKS]; // 三个员工
int t;
pthread_mutex_init(&g_task_lock, NULL); // 初始化互斥量
pthread_cond_init(&g_task_cv, NULL); // 初始化条件变量
for(t = 0; t < NUM_OF_TASKS; t++) { // 初始化三个员工
pthread_create(&threads[t], NULL, coder, NULL); // 员工开始工作了,接着看线程函数部分
}
_sleep(5); // 等待,老板招完人并不是马上派活
for(t = 1; t <= 4; t++){
pthread_mutex_lock(&g_task_lock); //共享数据访问,上个锁
tail += t; // 派活
printf("I am Boss, I assigned %d tasks, I notify all coders!\n", t);
pthread_cond_broadcast(&g_task_cv); // 叫三个员工全部起来抢活干,但是因为当前线程还没把锁打开,员工还在等锁打开
pthread_mutex_unlock(&g_task_lock); // 老板把锁打开,将钥匙交出去让员工抢。
_sleep(20);
}
// 活没了,解雇所有员工(让所有线程退出)后,老板再退出
pthread_mutex_lock(&g_task_lock);
quit = 1;
pthread_cond_broadcast(&g_task_cv); // 通知员工离开
pthread_mutex_unlock(&g_task_lock);
// 释放所有资源
pthread_mutex_destroy(&g_task_lock);
pthread_cond_destroy(&g_task_cv);
pthread_exit(NULL);
}
相关辅助理解的注释已经放在代码中了,相关打印如下:
No task now! Thread 2 is waiting!
No task now! Thread 3 is waiting!
No task now! Thread 4 is waiting!
I am Boss, I assigned 1 tasks, I notify all coders!
Have task now! Thread 2 is grabing the task !
Thread 2 has a task A now!
Have task now! Thread 3 is grabing the task !
No task now! Thread 3 is waiting!
Have task now! Thread 4 is grabing the task !
No task now! Thread 4 is waiting!
Thread 2 finish the task A!
No task now! Thread 2 is waiting!
I am Boss, I assigned 2 tasks, I notify all coders!
Have task now! Thread 2 is grabing the task !
Thread 2 has a task B now!
Have task now! Thread 3 is grabing the task !
Thread 3 has a task C now!
Have task now! Thread 4 is grabing the task !
No task now! Thread 4 is waiting!
Thread 3 finish the task C!
No task now! Thread 3 is waiting!
Thread 2 finish the task B!
No task now! Thread 2 is waiting!
I am Boss, I assigned 3 tasks, I notify all coders!
Have task now! Thread 4 is grabing the task !
Thread 4 has a task D now!
Have task now! Thread 3 is grabing the task !
Thread 3 has a task E now!
Have task now! Thread 2 is grabing the task !
Thread 2 has a task F now!
Thread 4 finish the task D!
No task now! Thread 4 is waiting!
Thread 2 finish the task F!
No task now! Thread 2 is waiting!
Thread 3 finish the task E!
No task now! Thread 3 is waiting!
I am Boss, I assigned 4 tasks, I notify all coders!
Have task now! Thread 4 is grabing the task !
Thread 4 has a task G now!
Have task now! Thread 3 is grabing the task !
Thread 3 has a task H now!
Have task now! Thread 2 is grabing the task !
Thread 2 has a task I now!
Thread 2 finish the task I!
Thread 2 has a task J now!
Thread 3 finish the task H!
No task now! Thread 3 is waiting!
Thread 4 finish the task G!
No task now! Thread 4 is waiting!
Thread 2 finish the task J!
Have task now! Thread 4 is grabing the task !
以上,就是线程相关的所有操作了。