1、什么是线程
- LWP:light weight process轻量级进程,本质上仍是进程(Linux下)
- 进程独享地址空间拥有PCB,线程拥有独立的PCB,但共享地址空间
- 进程是最小的资源单位,线程是最小的执行单位
- cpu根据线程来划分时间轮片,多线程能争取到更多的cpu资源
2、线程的实现原理
- 创建线程的底层函数与进程一样,都是使用clone函数;
- 三级映射(三级页表):进程—>页目录—>页表—>物理页面(内存单元)—>mmu—>虚拟地址空间
- 不同进程有不同的页目录,从而有不同的物理页面,不同的地址空间;
- 不同的线程有不同的PCB,但指向同一个页目录,指向相同的地址空间
- 线程可以看作寄存器和栈的集合
- 不同线程有不同的用户栈空间(保存局部变量和临时值[esp基址指针][ebp栈顶指针])
- 不同的内核栈空间(寄存器的值,为进程切换提供现场保护)
- 栈由一个个栈帧组成
- 线程共享资源
- 文件描述符表
- 各种信号处理方式(尽量不要将线程与信号交织在一起)
- 当前工作目录
- 用户id与组id
- 内存地址空间(除栈以外)
- 线程非共享资源
- 线程id
- 处理器现场和栈指针(内核栈)
- 独立的栈空间(用户空间栈)
- errno变量(全局变量)
- 信号屏蔽字
- 调度优先级
- 线程优、缺点
- 优点:
- 提高程序并发性
- 开销小
- 数据通信、共享数据方便
- 缺点:
- 库函数,不稳定
- 调试、编写困难、gdb不支持
- 对信号支持不好
- 优点相对突出,缺点不是硬伤
3、查看线程
parallels@ubuntu:~$ ps -Lf 7775
UID PID PPID LWP C NLWP STIME TTY STAT TIME CMD
paralle+ 7775 1 7775 16 59 09:55 tty2 Sl+ 0:04 /usr/lib/firefox/firefox -new-window
paralle+ 7775 1 7792 0 59 09:55 tty2 Sl+ 0:00 /usr/lib/firefox/firefox -new-window
paralle+ 7775 1 7793 0 59 09:55 tty2 Sl+ 0:00 /usr/lib/firefox/firefox -new-window
paralle+ 7775 1 7796 0 59 09:55 tty2 Sl+ 0:00 /usr/lib/firefox/firefox -new-window
paralle+ 7775 1 7798 0 59 09:55 tty2 Sl+ 0:00 /usr/lib/firefox/firefox -new-window
paralle+ 7775 1 7799 0 59 09:55 tty2 Sl+ 0:00 /usr/lib/firefox/firefox -new-window
paralle+ 7775 1 7800 0 59 09:55 tty2 Sl+ 0:00 /usr/lib/firefox/firefox -new-window
4、线程控制原语
- man page安装
- 验证:man -k pthread
- sudo apt-get undate
- sudo apt-get install manpages-posix manpages-posix-dev
- pthread_self函数
- 获取线程id,其作用相当于进程中的getpid函数
- pthread_t pthread_self(void)
- 返回值:This function always succeeds, returning the calling thread’s ID.
- pthread_create
- 创建线程,其作用相当于进程中的fork函数
- int pthread_create(pthread_t thread, const pthread_attr_t *attr, void *(start_routine) (void *), void *arg);
- 返回值:On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.
- 涉及到线程控制原语,在编译时需要加-pthread
4.1、创建线程的基本实现
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
// 线程主逻辑函数(创建线程需要执行的主体)
void* callback(void *arg){
printf("callback pthread = %lu, pid = %u, ppid = %u\n", pthread_self(), getpid(), getppid());
return NULL;
}
int main(){
pthread_t thread;
printf("main pthread = %lu, pid = %u, ppid = %u\n", pthread_self(), getpid(), getppid());
sleep(10);
// 第2个参数,创建线程的属性默认可以传null,第四个参数为线程主体函数参数
int ret = pthread_create(&thread, NULL, callback, NULL);
// 创建成功则返回0,失败则返回错误代码
if (ret != 0){
printf("pthread create error: %d\n", ret);
exit(1);
}else{
printf("create pthread success: %lu\n", thread);
}
// 创建线程成功后,默认会立即执行线程主体函数,前提是进程还在,所以sleep等待线程执行
sleep(10);
return 0;
}
执行结果:
parallels@ubuntu:~/Linux/pthread$ ./pthread.out
main pthread = 139814966679360, pid = 4726, ppid = 1918
create pthread success: 139814958167808
callback pthread = 139814958167808, pid = 4726, ppid = 1918
4.2、循环创建多线程
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
void *thread_func(void *arg){
int i = (int)(long)arg;
printf("%dth thread is created, tid = %lu\n", i, pthread_self());
}
int main(){
pthread_t tid;
int i = 0;
for (i = 0; i < 5; i++){
// 循环创建线程,并将序号通过主逻辑函数的参数带过去
tid = pthread_create(&tid, NULL, thread_func, (void *)(long)i);
sleep(1);
if (tid !=0){// 失败
// fprintf可将信息从标准文件描述符中输出,strerror函数将错误代码转成相关字符串描述
fprintf(stderr, "pthread_creat error: %s\n", strerror(tid));
}
}
return 0;
}
执行结果:
parallels@ubuntu:~/Linux/pthread$ ./m_pthread.out
0th thread is created, tid = 140010892678912
1th thread is created, tid = 140010884286208
2th thread is created, tid = 140010875893504
3th thread is created, tid = 140010867500800
4th thread is created, tid = 140010859108096
parallels@ubuntu:~/Linux/pthread$
4.3、线程的退出pthread_exit(void *retval)
- exit( )函数会将进程结束,慎重使用!!
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
void* callback(void *arg){
sleep(1);
printf("callback pthread = %lu, pid = %u, ppid = %u\n", pthread_self(), getpid(), getppid());
return NULL;
}
int main(){
pthread_t p;
printf("main pthread = %lu, pid = %u, ppid = %u\n", pthread_self(), getpid(), getppid());
int ret = pthread_create(&p, NULL, callback, NULL);
if (ret != 0){
fprintf(stderr, "pthread create error: %s\n", strerror(ret));
exit(1);
}
// 退出当前线程,如果没有退出当前线程,main函数结束后进程将结束,其他子线程将被强制结束
pthread_exit((void*)(long)1);
}
4.4、线程回收pthread_join( )函数
- int pthread_join(pthread_t thread, void **retval);
- 线程的pthread_join函数相当于进程的wait及waitpid函数
- pthread_join – join with a terminated thread
- pthread_join事实上是加入线程,将某线程加入到当前线程(有回收的效果)
- 第1个参数是指定线程id,第2个参数接收线程退出时传出的结果
- waitpid事实上也是类似的效果,只是waitpid只能传出整型数据
- 线程结束后会自动释放,进程不会。
4.4.1、线程回收示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
// 自定义结构体
struct exit_t{
int a;
int b;
};
// 线程回调函数
void *callback(void* arg){
struct exit_t* retval = (struct exit_t*)arg;
retval->a = 10;
retval->b = 20;
pthread_exit(retval);
}
int main(){
pthread_t tid;
// 定义一个结构体用于接收线程结束时数据的传出
struct exit_t* retval = malloc(sizeof(struct exit_t));
// 创建线程,并将定义好的结构体指针传入
pthread_create(&tid, NULL, callback, retval);
// 线程结束时将数据返回,join函数接收该数据
pthread_join(tid, (void **)&retval);
// 输出
printf("pthread_joined: a = %d, b = %d\n", retval->a, retval->b);
// 释放堆空间
free(retval);
return 0;
}
执行结果
pthread_joined: a = 10, b = 20
4.4.2、多线程回收示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int var = 100;
void* callback(void* arg){
int i = (int)(long)arg;
switch(i){
case 0:return (void *)(long)var;
case 1:var = 200;break;
case 2:break;
case 3:var = 300;break;
default: break;
}
return (void *)(long)var;
}
int main(){
pthread_t tid[5];
int retval[5];
int i = 0;
// 创建5个子线程
for(i = 0; i < 5; i++){
pthread_create(&tid[i], NULL, callback, (void *)(long)i);
}
// 回收5个子线程
for(i = 0; i < 5; i++){
// (void*)强转成int,那(void**)只需要传入一个int型地址即可
// 容易误解为(void**)必须传入一个指针类型的地址
pthread_join(tid[i], (void **)&retval[i]);
}
// 输出结果
for(i = 0; i < 5; i++){
printf("pthread %d return %d\n", i, retval[i]);
}
return 0;
}
执行结果
parallels@ubuntu:~/Linux/pthread$ ./m_pthread_join.out
pthread 0 return 100
pthread 1 return 200
pthread 2 return 200
pthread 3 return 300
pthread 4 return 300
parallels@ubuntu:~/Linux/pthread$
4.5、线程分离函数pthread_detach
- 一般情况下,线程终止后,其终止状态会一直保留到其他线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止会被立即回收它占用的所有资源,而不保留终止状态。
- 不能对一个detach状态的线程调用pthread_join函数
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *callback(void *arg){
}
int main(){
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);
//pthread_detach(tid);
int ret = pthread_join(tid, NULL);
printf("%d\n", ret);
return 0;
}
4.6、线程退出函数pthread_cacel
- pthread_cancel – send a cancellation request to a thread
- int pthread_cancel(pthread_t thread);
- 线程取消不是实时的,有一定的延时,需要等待线程达到某个取消点
- 可粗略地认为一个系统调用即为一个取消点,如果线程没有取消点,可通过pthread_testcancel函数调用设置一个取消点
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
void *callback1(void *arg){
int n = 3;
while(n--){
printf("pthread 1 runloop %d\n", 3-n);
sleep(1);
}
return (void *)1;
}
void *callback2(void *arg){
int n = 3;
while(n--){
printf("pthread 2 runloop %d\n", 3-n);
sleep(1);
}
pthread_exit((void *)2);
return NULL;
}
void *callback3(void *arg){
int n = 3;
while(n){
//printf("pthread 3 runloop %d\n", 3-n);
//sleep(1);
// 为pthread_cancel函数服务,如果没有相关系统调用,pthread_cancel找不到调用时机
// 人为增加取消点,退出线程
pthread_testcancel();
}
return NULL;
}
int main(){
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, callback1, NULL);
pthread_join(tid1, NULL);
pthread_create(&tid2, NULL, callback2, NULL);
pthread_detach(tid2);
pthread_create(&tid3, NULL, callback3, NULL);
pthread_cancel(tid3);
pthread_join(tid3, NULL);
sleep(3);
printf("end------------------\n");
return 0;
}
5、控制原语对比
进程 | 线程 | |
---|---|---|
创建 | fork | pthread_creat |
回收 | wait、waitpid | pthread_join (pthread_detach不能join) |
线束、取消 | kill | pthread_cancel(需要取消点,系统调用或pthread_testcancel函数设置) |
获取id | getpid | pthread_self |
6、线程属性
typedef struct __pthread_attr_s
{
int __detachstate;
int __schedpolicy;
struct __sched_param __schedparam;
int __inheritsched;
int __scope;
size_t __guardsize;
int __stackaddr_set;
void *__stackaddr;
size_t __stacksize;表示堆栈的大小。
}pthread_attr_t;
6.1、线程属性设置线程分离
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
void *callback(void *arg){
return NULL;
}
int main(){
pthread_t tid;
pthread_attr_t attr;
// 1、线程属性初始化
int ret = (&attr);
if (ret != 0){
fprintf(stderr, "pthread_attr_init error:%s - %d\n", strerror(ret), ret);
}
// 2、线程属性设置分离
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 3、使用属性创建线程
ret = pthread_create(&tid, &attr, callback, NULL);
if (ret != 0){
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(1);
}
ret = pthread_join(tid, NULL);
if (ret != 0){
fprintf(stderr, "pthread_join error:%s-%d\n", strerror(ret), ret);
exit(1);
}else{
printf("pthread_join success!!\n");
}
pthread_attr_destroy(&attr);
return 0;
}
执行结果:
parallels@ubuntu:~/Linux/pthread$ ./pthread_attr.out
pthread_join error:Invalid argument-22
parallels@ubuntu:~/Linux/pthread$
PS:如果线程函数执行速度非常快,有可能在pthread_create函数返回之前就终止了,而终止后线程号和系统资源可能移交给其他线程使用,这样调用pthread_create得到的线程就是错误的线程号,要避免这种情况一方面可使用sleep或pthread_cond_timedwait函数来解决
6.2、线程栈及栈大小修改
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
7、NPTL(Native POSIX Thread Library)
- 是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同时也符合 POSIX 的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。与 LinuxThreads 一样,NPTL 也实现了一对一的模型。
- 查看当前pthread版本,getconf GNU_LIBPTHREAD_VERSION
8、线程使用注意事项
- 主线程退出其他线程不退出,主线程应该调用pthread_exit
- 避免僵尸线程
- pthread_join
- pthread_detach
- pthread_create指定他离属性
- 被join线程可能在join函数返回前就释放相关内存空间,所以不应该返回线程栈中的值
- malloc和mmap申请的内存可以被其他线程释放
- 应该避免多线程模型中调用fork,除非exec。因为子进程中只有调用fork的线程存在,其他线程均pthread_exit
- 信号是复杂语义很难和多线程共存,应避免在多线程中引入信号机制