Linux线程
1 线程的基本概念
-
轻量级的进程(LWP:light weight process),在Linux环境下线程的本质任是进程。
-
进程:拥有独立的地址空间,拥有PCB,相当于独居。
-
线程:有PCB,但没有独立的地址空间,多个线程共享进程空间,相当于合租。
-
在 Linux操作系统下:
- 线程:最小的执行单位
- 进程:最小分配资源单位,可以看成只有一个线程的进程。
-
线程的特点:
- 类Unix系统中,早期没有“线程”的概念,80年代才引入,借助进程机制实现出线程的概念。因此在这类系统中,进程和线程关系密切。
- 线程是轻量级进程(light weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
- 从内核看进程和线程是一样的,都有各自不同的PCB
- 进程可以蜕变成线程
- 在 linux 下,线程是最小的执行单位;进程是最小的分配资源单位
查看指定线程的LWP号:ps -Lf pid
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内存函数
- 如果复制对方的地址空间,那么就产出一个“进程”
- 如果共享对方的地址空间,就产生一个“线程”。
因此,Linux 内核是不区分进程和线程的,只在用户层面上进行区分。
所有,线程所有操作函数 pthread_* 是库函数,而非系统调用
2 线程共享资源
- 文件描述符表
- 每种信号的处理方式
- 当前工作目录
- 用户ID和组ID
- 内存地址空间(.text / .data / .bss / heap / 共享库)
3 线程非共享资源
- 线程id
- 处理器现场和栈指针(内核栈)
- 独立的栈空间(用户空间栈)
- errno 变量
- 信号屏蔽字
- 调度优先级
4 线程优、缺点
-
优点:
- 提高程序并发性
- 开销小
- 数据通信、共享数据方便
-
缺点:
- 库函数,不稳定
- gdb调试、编写困难
- 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux 下由于实现方法导致线程、进程差别不是很大。
5 pthread_create函数–创建子线程
#include <pthread.h>
/*
* function: 创建一个线程
*
* function arguments:
* argv1: 传出参数,保存系统分配好的线程ID
* argv2: 通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数
* argv3: 函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束
* argv4: 线程主函数指向期间所使用的参数。
*
* return value:
* success: 返回0
* faild: 返回错误号
*/
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
/*
* 注意点:
* 1.由于pthread_create的错误码不保存在errno中,因此不能直接使用perror()打印错误信息,
* 可以先用strerror()把错误码转换成错误信息再打印。
* 2.如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止
* (在main函数调用return也相当于调用了exit)
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
struct Person
{
char name[64];
int age;
};
void *MyThread(void *arg)
{
//struct Person *stPersonPtr = (struct Person*)arg;
// 此处尽量不要用指针接收,而应该接收传入的值,因为传入的是主线程的栈地址(1.生存周期有限,2.之后可能被主线程修改此这块内存空间的值,子线程会受影响)
struct Person stPerson = *(struct Person*)arg;
printf("姓名:%s 年龄:%d\n", stPerson.name, stPerson.age);
printf("child thread, pid = %d, threadId = %d\n", getpid(), pthread_self());
}
int main(int argc, char *argv[])
{
struct Person stPerson{};
strcpy(stPerson.name, "小明");
stPerson.age = 18;
pthread_t threadID;
// 创建子线程
int ret = pthread_create(&threadID, NULL, MyThread, (void*)&stPerson);
if (ret != 0)
{
printf("create thread faild: %s\n", strerror(ret));
return -1;
}
printf("main thread, pid = %d, threadId = %d\n", getpid(), pthread_self());
sleep(1); // 为了让子线程执行完成
return 0;
}
6 pthread_exit函数–退出线程
#include <pthread.h>
/*
* function: 将单个线程退出
*
* function arguments:
* argv1: 表示线程的退出状态,通常传NULL
*/
void pthread_exit(void *retval);
/*
* 注意点:
* pthread_exit或者return返回的指针所指向的内存单元必须是全局变量或者堆内存变量,
* 不能在线程函数的栈分配,因为当其他线程得到这个返回指针时,线程函数已经退出了
* 栈空间就会被回收。
*/
7 pthread_join函数–回收线程
#include <pthread.h>
/*
* function: 阻塞等待线程退出,获取线程退出状态。其作用,对应进程中的waitpid()函数
*
* function arguments:
* argv1: 等待退出的线程ID
* argv2: 存储线程结束状态,整个指针和pthread_exit函数的参数是同一块地址
*
* return value:
* success: 返回0
* faild: 返回错误号
*/
int pthread_join(pthread_t thread, void **retval);
include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
struct Person
{
char name[64];
int age;
};
int g_val = 99;
void *MyThread(void *arg)
{
//struct Person *stPersonPtr = (struct Person*)arg;
// 此处尽量不要用指针接收,而应该接收传入的值,因为传入的是主线程的栈地址(1.生存周期有限,2.之后可能被主线程修改此这块内存空间的值,子线程会受影响)
struct Person stPerson = *(struct Person*)arg;
printf("姓名:%s 年龄:%d\n", stPerson.name, stPerson.age);
printf("child thread, pid = %d, threadId = %d\n", getpid(), pthread_self());
pthread_exit(&g_val);
}
int main(int argc, char *argv[])
{
struct Person stPerson{};
strcpy(stPerson.name, "小明");
stPerson.age = 18;
pthread_t threadID;
// 创建子线程
int ret = pthread_create(&threadID, NULL, MyThread, &stPerson);
if (ret != 0)
{
printf("create thread faild: %s\n", strerror(ret));
return -1;
}
printf("main thread, pid = %d, threadId = %d\n", getpid(), pthread_self());
// 回收子线程,并获取线程退出状态
void *p = NULL;
pthread_join(threadID, &p);
int *pInt = (int*)p;
printf("status: %d\n", *pInt);
return 0;
}
8 pthread_detach函数–实现线程分离
#include <pthread.h>
/*
* function: 实现线程分离
*
* function arguments:
* argv1: 需要分离的线程的线程号
*
* return value:
* success: 返回0
* faild: 返回错误号
*/
int pthread_detach(pthread_t thread);
/*
* 一情况下,线程终止后,其终止状态保留到其他线程调用pthread_join获取它的终止状态为止。
* 但是线程也可以设置detcah状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不是保留其终止状态。
* 不能对一个处于detach状态的线程调用pthread_join,就算调用了pthread_join也将返回错误。也就是说,
* 如果对一个线程调用了pthread_detach就不能再调用pthread_join了。
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
struct Person
{
char name[64];
int age;
};
void *MyThread(void *arg)
{
//struct Person *stPersonPtr = (struct Person*)arg;
// 此处尽量不要用指针接收,而应该接收传入的值,因为传入的是主线程的栈地址(1.生存周期有限,2.之后可能被主线程修改此这块内存空间的值,子线程会受影响)
struct Person stPerson = *(struct Person*)arg;
printf("姓名:%s 年龄:%d\n", stPerson.name, stPerson.age);
printf("child thread, pid = %d, threadID = %d\n", getpid(), pthread_self());
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
struct Person stPerson{};
strcpy(stPerson.name, "小明");
stPerson.age = 18;
pthread_t threadID;
// 创建子线程
int ret = pthread_create(&threadID, NULL, MyThread, &stPerson);
if (ret != 0)
{
printf("create thread faild: %s\n", strerror(ret));
return -1;
}
printf("main thread, pid = %d, threadID = %d\n", getpid(), pthread_self());
// 设置线程分离
pthread_detach(threadID);
ret = pthread_join(threadID, NULL);
if (ret != 0)
{
printf("pthread_join faild:%s\n", strerror(ret));
}
return 0;
}
9 pthread_cancel函数–取消线程
#include <pthread.h>
/*
* function: 杀死(取消)线程。其作用,对应进程中的kill()函数
*
* function arguments:
* argv1: 需要取消的线程的线程号
*
* return value:
* success: 返回0
* faild: 返回错误号
*/
int pthread_cancel(pthread_t thread);
// 注意: 线程的取消并不是实时的,而有一定的时延。需要等待线程到达某个取消点(检查点)
// 取消点: 是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open
// pause,close,read,write...(可以粗略认为会进入内核即为一个取消点)
// 执行 man 7 pthreads可以查看具备这些取消点的系统调用总列表
// 或者通过pthread_testcancel函数设置一个取消点
10 pthread_testcancel函数–设置取消点
#include <pthread.h>
/*
* function: 设置取消点
*/
void pthread_testcancel(void);
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *MyTread(void *argv)
{
while (1)
{
int a;
int b;
// 设置取消点
pthread_testcancel();
}
}
int main()
{
pthread_t threadID;
int ret = pthread_create(&threadID, NULL, MyTread, NULL);
if (ret != 0)
{
printf("pthread_create faild:%s\n", strerror(ret));
}
// 取消线程
ret = pthread_cancel(threadID);
if (ret != 0)
{
printf("pthread_cancel faild:%s\n", strerror(ret));
}
sleep(10);
return 0;
}
11 进程函数和线程函数比较
进程 | 线程 |
---|---|
fork | pthread_create |
exit | pthread_exit |
wait/waitpid | pthread_join |
kill | pthread_cancel |
getpid | pthread_self |
- | pthread_detach |
12 线程属性
根据pthread_create函数的第二个参数可以设置线程属性(分离/非分离)。
线程的分离状态决定一个线程以什么样的方式来终止自己,有两种状态:
- 非分离状态:线程的默认属性是非分离状态,这个情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
- 分离属性:分离线程没有被其他的线程等待回收,自己运行结束了,线程也就终止了,马上释放系统资源。
默认线程属性是非分离的,可以通过以下步骤将线程设置为分离属性
- 第一步:定义线程数据类型变量
- pthread_arrt_t arrt;
- 第二步: 对线程属性变量进行初始化
- int pthread_attr_init(pthread_attr_t* attr);
- 第三步:设置线程分离属性
- int pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate)
- 参数:
- attr: 线程属性
- detachstates:
- PTHREAD_CREATE_DETACHED(分离)
- PTHREAD_CREATE_OINABLE(非分离)
- 注意:这一步完成之后调用pthread_create创建线程,将attr传入,创建的线程就是分离属性
- int pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate)
- 第四步:释放线程属性资源
- int pthread_attr_destroy(pthread_attr_t* attr);
- 参数:线程属性
- int pthread_attr_destroy(pthread_attr_t* attr);
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
void *MyThread(void *argv)
{
printf("I'm child tread\n");
pthread_exit(NULL);
}
int main()
{
// 1.创建分离属性变量
pthread_attr_t attr;
// 2.对分离属性变量进行初始化
pthread_attr_init(&attr);
// 3.设置线程分离属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_t threadID;
int ret = pthread_create(&threadID, &attr, MyThread, NULL); // 创建子线程(第二个参数为线程属性)
if (ret != 0)
{
printf("pthread_create faild:%s\n", strerror(ret));
return -1;
}
// 4.释放线程属性资源
pthread_attr_destroy(&attr);
// 验证线程是否为分离属性
ret = pthread_join(threadID, NULL);
if (ret != 0)
{
printf("pthread_join faild:%s, 线程是分离属性\n", strerror(ret));
}
sleep(1);
return 0;
}
13 线程使用注意事项
- 主线程退出其他线程不退出,主线程应调用pthread_exit
- 避免僵尸线程(pthread_join、pthread_detach、pthread_create指定分离属性)
- malloc 和 mmap申请的内存可以被其他线程释放
- 应该避免在多线程模型中调用fork,除非马上exec。(子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit)
- 信号的复杂语义很难和多线程共存,应该避免在多线程引入信号机制