1 线程控制
1.1 线程的创建
1.通过pthread_create函数来创建线程
(1)函数原型:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
(2)参数解析:
①thread为线程id(是输出型参数,表示线程创建之后通过thread参数可以拿到线程的线程id);
attr表示线程的属性,默认可设为NULL;
start_routine为线程处理函数;
arg为线程处理函数的参数(表示对线程编号为thread的线程进行操作)。
②可以看到:线程处理函数的参数及返回值都是void*,因为C语言没有泛型编程,而线程处理的类型不一定相同,所以用void*接收任意类型,达到泛型编程的目的。
注意:在Linux里没有真正的线程,所以需要使用第三方的库,主要采用POSIX线程库,则使用线程相关函数必须包含头文件<pthread.h>
,并且编译时必须加上-lpthread,否则就会出现连接错误。
(3)返回值:
成功返回0.失败返回错误码,而且错误代码以返回值形式返回。(所有以pthread开头的相关函数都满足这个特性)
2.使用示例:
例1:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* thread_entry(void* arg)
{
printf("thread\n");
return NULL;
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,thread_entry,NULL);
sleep(1); //主线程必须要等1秒,否则如果主线程先被调度,就会导致主线程结束后,新线程还没有执行。
return 0;
}
3.通过pthread_self函数查看线程id
示例:查看线程的线程id(通过pthread_self( )函数,可以查看自己的线程id)
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* thread_entry(void* arg)
{
printf("child thread, id=%lu\n",pthread_self());
return NULL;
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,thread_entry,NULL);
sleep(1);
printf("main thread, child thread id:%lu\n",id);
return 0;
}
运行结果:
- 可以看到:通过pthread_create函数拿到的线程id与新线程里调用pthread_self函数得到的线程id的值相同
[root@localhost 线程创建]# gcc -o thread2 thread2.c -lpthread
[root@localhost 线程创建]# ./thread2
child thread, id=3078486896
main thread, child thread id:3078486896
3.当创建一个新线程后,会有两个执行流,新线程去执行相应的处理函数,主线程继续往下执行。
4.创建的新线程和主线程谁先执行并不确定,由调度器决定,(因为线程是轻量级进程,父子进程谁先执行并不确定)
1.2 线程等待
1.为什么线程需要等待
(1)需要知道线程退出信息;
(2)需要知道线程运行结果是否正确;
(3)回收线程的资源;
(4)如果不等待就会造成和僵尸进程类似的问题(注意没有僵尸线程);
2.线程等待函数(pthread_join)
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
//thread表示需要等待线程的线程id,retval为thread线程执行其线程处理函数的返回值(retval为输出型参数,为了拿到线程函数的返回值),因为线程处理函数的返回值为void*,所以需要用void**来接收返回值
3.只有当线程正常退出才能被等待
(1)线程正常退出:代码执行完结果正确;代码执行完结果不正确;
(2)因为如果线程异常退出,整个进程就会被终止掉。
4.通过pthread_join( )函数,调用者以阻塞方式等待。调用pthread_join函数的线程会被挂起等待,直到被等待的进程终止才继续执行。
注意:线程等待中没有非阻塞等待方式,而进程等待有阻塞和非阻塞形式。
5.使用示例:
#include<stdio.h>
#include<pthread.h>
void* thread_run(void* arg)
{
printf("im thread1\n");
return (void*)5;
}
int main()
{
pthread_t pid;
pthread_create(&pid,NULL,thread_run,NULL);
void* ret;
pthread_join(pid,&ret); //将线程处理函数的返回值通过ret保存
printf("%d\n",(int)ret);
return 0;
}
运行结果:(可以看到pid线程的返回值为5,主线程等待该线程后,拿到的返回值为5)
1.3 线程的终止
1.3.1 线程终止有三种方法
(1)线程处理函数里return,返回值为线程的退出码;
(2)线程调用pthread_exit(主线程调用该函数,我们无法获得主线程的退出信息,主线程表示的是进程的退出必须采用进程的退出方式);
(3)线程可以被同一进程中的其它线程取消掉(通过pthread_cancel函数)。
1.3.2 pthread_exit
1.函数原型:
#include <pthread.h>
void pthread_exit(void *retval);
//在线程处理函数里面调用该函数,用来终止该线程,并且线程的退出码保存在retval里
2.示例:在thread_run函数里面调用pthread_exit函数,函数的参数为线程退出码,这里将线程退出码设为123
#include<stdio.h>
#include<pthread.h>
void* thread_run(void* arg)
{
printf("Im thread1\n");
pthread_exit((void*)123);
}
int main()
{
pthread_t pid;
pthread_create(&pid,NULL,thread_run,NULL);
void* ret;
pthread_join(pid,&ret);
printf("thread1 exit code:%d\n",(int)ret);
return 0;
}
运行结果:(可以看到:通过线程等待,通过ret拿到的线程退出码就是123)
3.示例2:主线程调用pthread_exit函数
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* thread_run(void* arg)
{
printf("thread 1\n");
}
int main()
{
pthread_t pid;
pthread_create(&pid,NULL,thread_run,NULL);
sleep(1);
printf("main thread\n");
pthread_exit((void*)456);
return 0;
}
运行结果:(可以看到主线程调用pthread_exit函数,当参数设为456时,进程的退出码并不是456而是0)
总结:主线程的退出必须采用进程的退出方式。
1.3.3 pthread_cancel
pthread_cancel,既可以取消某个线程,也可以自己取消自己
1.函数原型:
#include <pthread.h>
int pthread_cancel(pthread_t thread);
2.示例:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
void* thread_run(void* arg)
{
printf("Im thread1\n");
pthread_exit((void*)123);
}
int main()
{
pthread_t pid;
pthread_create(&pid,NULL,thread_run,NULL);
pthread_cancel(pid);
void* ret;
pthread_join(pid,&ret);
printf("thread1 exit code:%d\n",(int)ret);
return 0;
}
运行结果:(可以看到:用pthread_cancel后,线程的退出码为-1,因为这是宏PTHREAD_CANCELED的值。
如果主线程在取消线程之前sleep几秒,则那个线程就会正常退出,退出码就是其函数返回值
1.3.4 在线程处理函数中调用exit、_exit直接导致进程退出
示例:在线程处理函数thread_run函数里调用exit:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
void* thread_run(void* arg)
{
printf("Im thread1\n");
exit(123);
}
int main()
{
pthread_t pid;
pthread_create(&pid,NULL,thread_run,NULL);
void* ret;
pthread_join(pid,&ret);
printf("thread1 exit code:%d\n",(int)ret);
return 0;
}
运行结果:(可以看到在thread_run里面调用exit,将不会输出”thread1 exit code:“,因为直接导致进程退出,则主线程也不会下继续执行下面的代码,也就不会打印那句话,并且通过echo $?可以查看进程退出码为123,也就是exit里面的哪个参数。
1.4 线程的分离
1.因为线程需要被等待,如果不等待就会造成内存泄露。如果不关注线程的退出信息,可以将线程设为分离状态。
2.相关函数:
pthread_detach(pthread_t thread);
3.使用示例:
例1:
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
void* entry(void* arg)
{
printf("thread id:%d\n",pthread_self());
sleep(5);
return (void*)3;
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,entry,NULL);
// pthread_detach(id);
sleep(1);
void* ret;
pthread_join(id,&ret);
printf("thread id %d ,return %d\n",id,(int)ret);
return 0;
}
运行结果:
①如果没有pthread_detach这句,运行结果如下:
[root@localhost 线程终止]# ./threaddt
thread id:-1216820368
thread id -1216820368 ,return 3
②如果加上pthread_detach这句,运行结果如下:(可以看到输出的线程函数的返回值是一个随机数,说明已经无效,不能对一个已经分离的线程进行线程等待)
[root@localhost 线程终止]# ./threaddt
thread id:-1216951440
thread id -1216951440 ,return 134514203
例2:将线程设为分离后,再通过pthread_join等待,通过函数返回值进行进一步验证(被分离后不能再等待)
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
void* entry(void* arg)
{
pthread_detach(pthread_self());
printf("new thread\n");
return NULL;
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,entry,NULL);
sleep(1); //保证新线程先执行
int ret = pthread_join(id,NULL);
if(ret == 0)
{
//说明等待成功
printf("wait success\n");
}
else
{
printf("wait failed\n");
}
return 0;
}
运行结果:
[root@localhost 线程终止]# ./threaddt2
new thread
wait failed //输出wait failed,说明等待失败
4.注意点:
(1)设为分离状态的线程不需要等待,也不能被其它线程回收或杀死,当其运行结束后会自动释放自己的资源。
(2)线程被分离,则它的退出信息我们不再关心,退出结果我们也不再关心,资源我们也无需再回收。
(3) 但是当一个被分离的线程出错后也会影响到其它线程,因为无论怎样,在、线程都是属于进程内部的。
5.线程不仅可以被其它线程设为分离状态,也可以自己把自己设为分离状态:
pthread_detach(pthread_self()); //自己将自己设为分离状态
2 探索线程标识
1.在Linux下,线程是通过进程模拟的,每个线程就会有一个PCB;
没有线程之前,每个进程对应一个PCB,有了线程之后就会使得一个进程管理多个PCB,但是为了使得同一个进程里面的线程对应相同的进程id,所以每个PCB里面不仅需要线程id还要有标识进程的id。
2.在Linux下,一个进程就对应多个PCB,一个线程对应一个PCB;Linux内核引入了线程组的概念。则对于多线程的进程就是一个线程组,在这个线程组里面,每个线程的线程id不相同,但是他们的组id相同,这个组id就是进程id。在线程的PCB里面还包含一个指向组长线程的指针,这个组长就是主线程;主线程的线程id与进程id相同。
3.先有进程然后才会有线程。(因为进程是进行资源分配的最小单位)
4.tast_struct结构体
struct task_struct {
...
pid_t pid; //这里的pid就是线程id
pid_t tgid; //tgid表示线程组id,也就是进程id
...
struct task_struct *group_leader; //组长线程,也就是主线程
...
struct list_head thread_group; //使用一个链表,将同一个线程组里面的线程连接起来,list_head就是这个链表的头结点
...
};
5.通过命令查看线程id(通过ps -eLF)
[root@localhost 线程创建]# ps -eLF |head -n 1 && ps -eLF | grep thread2 |grep -v grep
UID PID PPID LWP C NLWP SZ RSS PSR STIME TTY TIME CMD
root 11032 3339 11032 0 2 3088 480 0 19:35 pts/0 00:00:00 ./thread2
root 11032 3339 11033 0 2 3088 480 0 19:35 pts/0 00:00:00 ./thread2
分析:
PID表示进程id,PPID表示父进程id,LWP表示轻量级进程id,也就是线程id,后面的CMD表示属于哪一个进程。
可以看到:
- 对于进程./thread2来说,里面有两个线程,他们的进程id相同,都是11032,但是各自有独立的线程id,其中线程id为11302的为主线程(与进程id相同)。
- 但是对于同一个进程./thread2,上面打印出来的线程id与通过命令看到的线程id不相同,说明线程有两个线程id。
- 同一个线程的两个线程id,一个是pthread_t类型,一个是pid_t类型。通过pthread_create函数会产生一个线程id,这个线程id是pthread_t类型的,该线程id是属于POSIX线程库范畴的,在线程库里面对线程进行相关操作都是通过该值进行;而pid_t类型的线程id,也就是LWP,是内核表示线程的唯一标识符,主要是操作系统为了进行线程调度。
- 通过gettid( )可以获得线程id,也就是可以获得PCB里面的pid的值,等于LWP的值;通过getpid( )可以获得进程id,也就是PCB里面tgid的值; 通过pthread_self( )获得的值就是pthread_t类型的线程id的值。
6.对于线程而言,没有父子之分,每个线程地位都相同,除了主线程会有些特殊之外。