1 线程的基础理论
如图所示,一个进程可以包含多个线程。
- 同一个程序中的所有线程均会执行相同的程序,且共享进程的内存段,包括数据段、堆区;
- 进程的栈区对线程是不共享的,每个线程都拥有属于自己的栈区,用于存放函数的参数值、局部变量的值、返回地址等。
同一个进程中的多个线程可以并发执行。在多处理器环境下,多个线程可以同时并行。如果一个线程因等待I/O操作而遭阻塞,那么其他线程依然可以继续运行。
- 同一个进程创建的线程之间进行通信相对于进程之间来说,要方便、快速地多。原因在于线程之间是共享进程的数据段的,因此,线程间通信是通过操作共享的数据段实现的;
- 进程则不同,每个进程所操作的地址空间是独立的,因此进程之间如果需要进行数据的传递则需要引入进程间的通信机制来实现。
虽然线程间的通信不用考虑相对复杂的通信机制,但也有其比较棘手的问题有待解决。
- 如果多个线程并发(同时)访问共享的进程数据段,而不按照一定的规则或顺序,则会造成共享资源的不确定,这种情况称为竞态。例如,线程1对全局变量a进行了赋值,而线程2则需要读取全局变量a的值,如果此时刚要进行读取,线程3却对变量a进行了重新赋值,那么,此时读取的结果将出现差异。
- 因此针对多线程编程通信的问题,Linux中提供了很多同步互斥机制,从而保证在某一个线程在操作共享资源时,不会被其他线程打扰。即一个时刻,只能有一个线程在对共享资源进行访问。
2 线程操作
创建线程
挂起线程
终止线程
其它操作
2.1 创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
功能:创建线程;线程调用pthread_create函数创建新线程后,当前线程会从pthread_create函数返回并继续向下执行,新线程执行函数指针start_routine所指的函数。
参数说明:
- thread:一个传入传出参数,待创建线程的id指针;
- attr:设置待创建线程的属性,通常传入NULL;
- start_routine:一个函数指针,指向一个参数为void *,返回值也为void *的函数,该函数为待创建线程的执行函数;
- arg:传给线程执行函数的参数。
返回值说明:
- 成功:返回0;
- 不成功:返回errno。
特别说明:
- 进程id的类型pid_t是一个正整数,在整个系统都是唯一的;
- 线程id的类型pthread_t并非是一个正整数,只在当前进程中保证唯一;
- 当前进程调用pthread_create后获得的thread为新线程id;
- 线程id不能简单地使用printf函数打印,而应使用pthread_self函数来获取。
【案例1】使用pthread_create函数创建线程,并使原线程与新线程分别打印自己的线程id。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *tfn(void *arg) {
printf("tfn--pid=%d,tid=%lu\n", getpid(), pthread_self());
return (void*)0;
}//of tfn
int main() {
pthread_t tempTid;
printf("main--pid=%d, tid=%lu\n", getpid(), pthread_self());
int tempRet = pthread_create(&tempTid, NULL, tfn, NULL);
if (tempRet != 0){
fprintf(stderr, "pthread_create error:%s\n", strerror(tempRet));
exit(1);
}//of if
sleep(1);
return 0;
}//of main
因为pthread库不是Linux系统默认的库,需要在编译的时候添加选项-lpthread:
gcc pthread_cre.c -o pthread_cre -lpthread
执行结果如下:
进程和线程的联系:
- 当一个进程创建一个线程时,原有的进程就会变成线程,两个线程共用一段地址空间;
- 对内核而言,线程和进程没有区别,CPU会为每个线程与进程分配时间片,通过进程控制块来调度不同的线程和进程。
进程和线程的区别:
- 进程拥有独立的地址空间,当使用fork函数创建新进程时,若其中一个进程要对fork之前的数据进行修改,进程会根据“写时复制”原则,先复制一份该数据到子进程的地址空间,再修改数据。即便是全局变量,在进程间也不是共享的。
- 线程间共享地址空间,一个线程对全局取的数据进行了修改,其它线程访问到的也是修改后的数据。
【案例2】创建新线程,在新线程中修改原线程中定义在全局区的变量,并在原线程中打印该数据。
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
int globVar = 100;
void *tfn(void *arg) {
globVar = 200;
printf("thread\n");
return NULL;
}//of tfn
int main(void) {
printf("At first var = %d\n", globVar);
pthread_t tempTid;
pthread_create(&tempTid, NULL, tfn, NULL);
sleep(1);
printf("after pthread_create, var = %d\n", globVar);
return 0;
}//of main
执行结果如下:
2.2 线程退出
#include <pthread.h>
void pthread_exit(void *retval);
为什么要用pthread_exit:
- return:用于退出函数,使函数返回函数调用处;
- exit:用于退出进程,若在线程中调用该函数,那么该线程所处的进程也会退出。
功能:退出线程。
参数说明:
- retval:表示线程的退出状态,通常设置为NULL。
返回值说明:
- 无。
【案例 3】:在一个进程中创建4个新线程,分别用pthread_exit, return, exit使其中一个线程退出,观察其它线程的执行状况。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void *tfn(void *paraArg) {
long int i;
i = (long int)paraArg; //强转
if (i == 2) {
pthread_exit(NULL); //return, exit(0)
}//of if
sleep(i); //通过i来区别每个线程
printf("I'm %dth thread, Thread_ID = %lu\n", i + 1, pthread_self());
return NULL;
}//of tfn
int main(int paraArgc, char *paraArgv[]) {
long int tempNum = 5, i;
pthread_t tempTid;
if (paraArgc == 2) {
tempNum = atoi(paraArgv[1]);
}//of if
for (i = 0; i < tempNum; i++) {
//将i转换为指针,在tfn中再强转回整型
pthread_create(&tempTid, NULL, tfn, (void *)i);
}//of for i
sleep(tempNum);
printf("I am main, I'm a thread!\n"
"main_thread_ID = %lu\n", pthread_self());
return 0;
}//of main
执行结果如下:
2.3 线程中止
#include <pthread.h>
int pthread_cancel(pthread_t thread);
功能:
- 向指定线程发送CANCEL信号,使一个线程强行杀死另外一个线程,类似于终止进程函数kill;
- 与进程不同的是,调用该函数杀死线程时,需要等待线程到达某个取消点,线程才会成功被终止;
- 取消点通常伴随阻塞出现,用户也可以在程序中通过调用pthread_testcancel函数创造取消点;
- pthread_exit使线程主动退出,pthread_cancel通过信号使线程被动退出;
- 注意:由于线程机制出现之前信号机制已经出现,信号机制在创建时并未考虑线程,线程与信号机制的兼容性略有不足,因此多线程编程时尽量避免使用信号,以免出现难以调试的错误。
参数说明:
- thread:线程id。
返回值说明:
- 成功:0;
- 不成功:返回errno。
【案例 4】使用pthread_cancel使原线程终止指定线程。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void *tfn(void *paraArg) {
while(1) {
printf("child thread ...\n");
pthread_testcancel();
}//of while
}//of tfn
int main(void)
pthread_t tempTid;
void *tempTret = NULL;
pthread_create(&tempTid, NULL, tfn, NULL);
sleep(1);
pthread_cancel(tempTid);
pthread_join(tempTid, &tempTret);
printf("child thread exit code = %ld\n", (long int)tempTret);
return 0;
}//of main
执行结果如下:
2.4 线程挂起
若将案例3主函数的sleep行删除,程序的执行结果如下:
I am main, I'm a thread!
main_thread_ID = 140186242565888
- 问题:其它线程并没有执行,为什么?
- 原因分析:
– 线程与进程不同,若作为程序入口的原线程退出,系统内部会调用exit函数,导致同一进程中的所有线程都退出。 - 解决方案:
– 使用sleep函数使原线程阻塞,保证新创建的线程顺利执行;
– 使用线程挂起函数。
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能:
- 挂起线程,等待指定线程thread结束;
- 类似于wait,waitpid将进程挂起,以等待某个子进程结束;
- 该函数中指定的线程必须与调用该函数的线程处于同一个进程中,且多个线程不能同时挂起等待同一个进程,否则pthread_join将会返回错误。
参数说明:
- thread:表示被等待的线程id;
- retval:用于接收thread线程执行函数的返回值指针,该指针的值与thread线程的终止方式有关:
– 通过return返回:retval存放的是thread线程函数的返回值;
– 其它线程通过系统调用pthread_cancel异常终止,retval存放的是常量PTHREAD_CANCELED;
– 自调用pthread_exit终止,retval存放的是pthread_exit的参数ret_val;
– 若不关心它的终止状态,retval设置为NULL。
返回值说明:
- 成功:0;
- 不成功:返回errno。
【案例 5】使用pthread_exit退出线程,为线程设置退出状态并将线程的退出状态输出。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
typedef struct {
int a;
int b;
} exit_t;
void *tfn(void *paraArg){
exit_t *tempRet;
tempRet = malloc(sizeof(exit_t));
tempRet->a = 100;
tempRet->b = 300;
pthread_exit((void *)tempRet); //线程终止
return NULL; //线程返回
}//of tfn
int main(void){
pthread_t tempTid;
exit_t *tempRetval;
pthread_creat(&tempTid, NULL, tfn, NULL);
//调用pthread_join可以获取线程的退出状态
pthread_join(tempTid, (void **)&tempRetval);
printf("a = %d, b = %d\n", tempRetval->a, tempRetval->b);
return 0;
}//of main
分析:tfn函数既调用了pthread_exit函数,又设置了关键字return;若如下代码打印不为空,则说明线程通过pthread_exit函数退出。
printf("a = %d, b = %d\n", tempRetval->a, tempRetval->b);
执行结果如下:
【案例 6】使用pthread_join回收多个新线程,并使用pthread_exit获取每个线程的退出状态。
分析:进程中可以使用waitpid函数结合循环结构使原进程等待多个进程退出,线程中pthread_join同样可以与循环结构结合,等待多个线程退出。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
long int globVar = 100;
void *tfn(void *paraArg){
long int i;
i = (long int)paraArg;
sleep(i);
if(i == 1) {
globVar = 333;
printf("var = %d\n", globVar);
pthread_exit((void *)globVar);
} else if(i == 3){
globVar = 777;
printf("I'm %dth pthread, pthread_id = %lu, var = %d\n", i + 1, pthread_self(), globVar);
pthread_exit((void *)globVar);
} else {
printf("I'm %dth pthread, pthread_id = %lu, var = %d\n", i + 1, pthread_self(), globVar);
pthread_exit((void *)globVar);
}//of if
return NULL;
}//of tfn
int main(void){
pthread_t tempTid[5];
long int i;
int *tempRet[5];
for(i = 0; i < 5; i ++){ //创建新线程
pthread_create(&tempTid[i], NULL, tfn, (void *)i);
}//of for i
for(i = 0; i < 5; i ++){ //回收新线程
pthread_join(tempTid[i], (void **)&tempRet[i]);
printf("---%d's ret = %d\n", i, (long int)tempRet[i]);
}//of for i
printf("I'm main pthread tid = %lu\t var = %d\n", pthread_self(), globVar);
pthread_exit(NULL);
}//of main
分析:原线程的退出之所以会导致其它线程退出,是因为原线程执行完毕后,main函数会隐式调用exit函数,而pthread_exit函数可以只使调用该函数的线程退出。若在原线程调用return之前调用pthread_exit,同样可以保证其它线程的正常运行。
执行结果如下:
2.5 线程分离
- 在线程终止后,其它线程调用pthread_join函数获取该线程的终止状态前,该线程会一直保持终止状态,这种状态类似进程中的僵尸态;
- 为避免处于终止状态的线程占用内存,线程机制中提供了pthread_detach函数,可在线程被创建后设置线程分离,被分离的线程在执行结束后将会自动释放,不再等待其它线程回收。
#include <pthread.h>
int pthread_detach(pthread_t thread);
功能:
- 将线程从主控线程分离,这样当线程结束后,它的退出状态不需要由其它线程来获取,而是由该线程自身自动释放;
- pthread_join不能终止已处于detach状态的线程,若对于分离态的线程调用pthread_join,函数会调用失败并返回EINVAL。
参数说明:
- thread:待分离的线程id。
返回值说明:
- 成功:0;
- 不成功:返回errno。
【案例 7】使用pthread_detach分离新线程,使新线程自动回收。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
void *tfn(void *paraArg) {
int tempNum = 5;
while (n--) {
printf("pthread tfn n = %d\n", tempNum);
sleep(1);
}//of while
return (void *)7;
}//of tfn
int main(void) {
pthread_t tempTid;
void *tempRet;
pthread_create(&tempTid, NULL, tfn, NULL);
pthread_detach(tempTid); //分离新线程
int tempRetvar = pthread_join(tempTid, (void **)&tempRet);
if (tempRetvar != 0) {
fprintf(stderr, "pthread_join error %s\n", strerror(tempRetvar));
} else {
printf("pthread exit with %ld\n", (long int)tempRet);
}//of if
return 0;
}//of main
分析:线程分离后调用pthread_join会失败,因此会执行如下代码:
fprintf(stderr, "pthread_join error %s\n", strerror(tempRetvar));
执行结果如下:
知识扩展:
- linux线程执行 pthread有两种状态:joinable和unjoinable,默认是joinable;
– 通过pthread_attr_getdetachstate获取线程的状态;
– 方法一:通过如下代码来设置为状态joinable 还是 unjoinable
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&thr, &attr, &thread_start, NULL);
– 方法二:在线程中调用 pthread_detach, 如:pthread_detach(pthread_self()),将状态改为unjoinable,确保资源的释放:
void threadFunc( void *ptr ){
pthread_detach(pthread_self());
pthread_exit(0) ;
}//of threadFunc
pthread_t tid;
int status = pthread_create(&tid, NULL, threadFunc, NULL);
– 方法三:外部主线程主动调用 pthread_detach(tid):
pthread_t tid;
int status = pthread_create(&tid, NULL, ThreadFunc, NULL);
thread_detach(tid);
- 如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符(总计8K多);只有调用pthread_join之后这些资源才会被释放;
- 若是unjoinable状态的线程,堆栈和线程描述符这些资源在线程函数退出时或pthread_exit时自动会被释放。