在多任务操作系统中,线程是操作系统中的基本执行单元,可以看作是一个进程内的子任务,与同一进程的其他线程共享相同的内存空间和资源。线程可以并发地执行,使得程序能够更有效地利用多核处理器和资源。本篇博客将详细介绍线程的基础知识,包括线程的引入,进程和线程的区别,线程的资源,以及线程API和线程属性。
在传统的操作系统中,进程是操作系统分配资源和调度的基本单位。然而,对于很多需要并发执行的任务,使用进程可能会导致资源的浪费。这是因为每个进程都有自己的地址空间,进程间的切换需要保存和恢复大量的上下文信息,这使得进程的创建和切换成本相当高。
线程,也称为轻量级进程,是在一个进程内部并发执行的单元。相比于进程,线程有更小的上下文切换开销,因此更适合于高并发的场景。
(1)资源拥有
进程是资源拥有的单位,包括内存空间,文件描述符,信号处理函数等。相比之下,线程只拥有一小部分资源,如执行栈和自己的寄存器值,其他资源如内存空间,文件描述符等都与其所在的进程共享。
(2)调度和切换
进程是操作系统调度的基本单位,进程间的切换需要交换很多上下文信息,开销较大。线程作为轻量级进程,线程间的切换只需要交换很少的上下文信息,开销较小。
(3)通信方式
进程间通信需要使用IPC(进程间通信)机制,如管道,消息队列,共享内存等。线程间通信更为方便,可以直接通过读写同一进程内的数据进行通信。
线程拥有自己的一些资源,如执行栈,寄存器值(包括程序计数器),线程本地存储,以及线程的属性和优先级等。其他的资源,如内存空间,文件描述符,信号处理函数等,都与其所在的进程共享。
在C语言中,线程的操作主要通过POSIX线程(pthread)库进行。以下是一些常用的线程API(程序编程接口)。这些线程相关函数不属于标准C库,编译多线程代码的时候,需要链接线程库 pthread,但Ubuntu版本到22时或更高就可以省略该操作。
线程的创建主要通过 pthread_create() 函数进行。这个函数会创建一个新的线程,并开始执行指定的函数。
#include
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);参数:
pthread_t *thread, //创建成功的时候得到的线程ID
const pthread_attr_t *attr, //线程属性
void *(*start_routine) (void *), //线程任务函数
void *arg //线程任务函数需要的参数
返回值:成功返回0,失败返回错误码
一个最简单的创建线程函数代码:
#include
#include
void* myThreadFunc(void* arg) {
sleep(5);// 线程的执行函数
return NULL;
}
int main() {
pthread_t threadId;
if (pthread_create(&threadId, NULL, myThreadFunc, NULL)) {
// 处理错误
}
return 0;
}
当一个线程结束时,需要通过 pthread_join() 函数来接合(回收)线程,否则线程可能会变成僵尸线程。 pthread_join() 函数还可以获取线程的返回值。
int pthread_join(pthread_t thread, void **retval);
int pthread_tryjoin_np(pthread_t thread, void **retval);
参数:
thread 线程 TID
retval 储存线程退出值的内存的指针
返回值:
成功 0
失败 errno
#include
#include
void* myThreadFunc(void* arg) {
sleep(5);// 线程的执行函数
return NULL;
}
int main() {
pthread_t threadId;
if (pthread_create(&threadId, NULL, myThreadFunc, NULL)) {
// 处理错误
}
if (pthread_join(threadId, NULL)) {
// 处理错误
}
return 0;
}
注意:
(1)如果线程退出时没有退出值,那么 retval 可以指定为 NULL;
(2)pthread_join( )指定的线程如果尚在运行,那么他将会阻塞等待;
(3)pthread_tryjoin_np( )指定的线程如果尚在运行,那么他将会立即出错返回。
线程可以通过 pthread_exit() 函数来主动结束自己的执行,并返回一个值。
#include
#include
void* myThreadFunc(void* arg) {
sleep(5);// 线程的执行函数
return NULL;
}
int main() {
pthread_t threadId;
if (pthread_create(&threadId, NULL, myThreadFunc, NULL)) {
// 处理错误
}
if (pthread_join(threadId, NULL)) {
// 处理错误
}
return 0;
}
注意:
由于局部变量在函数退出的时候会释放,如果线程函数需要返回数据,那么这个数据必须保证函数退出依然有效,所以我们建议,要么用静态数据,要么用堆空间。
pthread_exit()在主线程里面,他只会退出主线程,但是进程不会退出。进程里面相关的资源也暂时不会释放,等到其他线程依次退出,进程才会退出。
我们可以通过 pthread_cancel ()函数来结束一个线程的执行。
#include
#include
void* myThreadFunc(void* arg) {
sleep(5);// 线程的执行函数
return NULL;
}
int main() {
pthread_t threadId;
if (pthread_create(&threadId, NULL, myThreadFunc, NULL)) {
// 处理错误
}
pthread_cancel(threadId);
if (pthread_join(threadId, NULL)) {
// 处理错误
}
return 0;
}
线程属性是线程的一些可配置的属性,如是否可连接,堆栈大小,调度策略等。
(1)使用线程属性一般需要以下步骤:使用 pthread_attr_init ()初始化线程属性对象;
(2) 使用 pthread_attr_setxxx 函数设置线程属性;
(3)创建线程时,将线程属性对象传递给 pthread_create ()函数;
(4)使用完线程属性后,使用 pthread_attr_destroy 销毁线程属性对象。
#include
void* myThreadFunc(void* arg) {
// 线程的执行函数
}
int main() {
pthread_t threadId;
pthread_attr_t attr;
pthread_attr_init(&attr);
// 设置线程属性
if (pthread_create(&threadId, &attr, myThreadFunc, NULL)) {
// 处理错误
}
pthread_attr_destroy(&attr);
if (pthread_join(threadId, NULL)) {
// 处理错误
}
return 0;
}
线程的分离属性决定一个线程结束后,是否自动释放其资源。一个分离的线程在结束后会自动释放其资源,而一个非分离的线程则需要通过 pthread_join ()函数来回收。
线程的调度策略决定了线程的运行顺序。常见的调度策略有FIFO(先进先出),RR(轮转)和其他,默认的调度策略是系统决定的。
(1)设置和获取静态优先级
int pthread_attr_setschedparam(pthread_attr_t *attr,const struct sched_param *param);
int pthread_attr_getschedparam(pthread_attr_t *attr,struct sched_param *param);
静态优先级:0 到 99
0 为默认的非实时普通进程
1-99 为实时进程,数值越大,优先级越高
(2)设置和获取动态优先级
int nice(int inc);
动态优先级:-20 到 19
动态优先级数值越大,优先级越低
每个线程都有自己的栈空间,用于存放局部变量和函数调用的上下文。栈空间的大小可以通过线程属性来设置。警戒区是线程栈的一部分,用于防止栈溢出时覆盖其他内存区域的数据。
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
int pthread_attr_getguardsize(pthread_attr_t *attr, size_t guardsize);
参数:
attr 线程属性变量
stacksize 线程栈的大小(默认8M)
guardsize 警戒区的大小(默认4Kb)
#include
#include
#include
#include
//线程任务函数
void *task(void *arg)
{
printf("线程任务函数执行一个10秒钟的任务\n");
sleep(10);
printf("线程任务函数结束\n");
}
int main(int argc, char const *argv[])
{
//定义一个线程属性变量
pthread_attr_t attr;
//初始化
pthread_attr_init(&attr);
//获取栈大小
size_t stacksize;
pthread_attr_getstacksize(&attr, &stacksize);
printf("栈大小:%ld\n", stacksize);
//获取警戒区大小
size_t guardsize;
pthread_attr_getguardsize(&attr, &guardsize);
printf("警戒区大小:%ld\n", guardsize);
//创建线程
pthread_t pid;
pthread_create(&pid, &attr, task, NULL);
//保证线程任务函数执行完毕,进程在退出
pthread_join(pid, NULL);
return 0;
}
pthread_setcancelstate(int state, int *oldstate);
用于设置线程的取消状态。它有两个参数:
state:用于设置取消状态,可以是 PTHREAD_CANCEL_ENABLE(使能取消请求)或 PTHREAD_CANCEL_DISABLE(关闭取消请求)。
oldstate:一个指针,用于存储之前的取消状态。
pthread_setcanceltype(int type, int *oldtype);用于设置线程的取消类型。它有两个参数:
type:用于设置取消类型,可以是 PTHREAD_CANCEL_DEFERRED(延时响应)或 PTHREAD_CANCEL_ASYNCHRONOUS(立即响应)。
oldtype:一个指针,用于存储之前的取消类型。
这两个函数通常在线程任务函数内部使用,用于控制线程的取消行为。
void pthread_cleanup_push(void (*routine)(void *), void *arg);
用于注册退出处理例程,它有两个参数:
routine:是一个函数指针,指向一个要在线程退出时执行的函数。
arg:是传递给处理例程的参数。
void pthread_cleanup_pop(int execute);用于取消之前使用 pthread_cleanup_push 注册的处理例程。它有一个参数:
execute:如果该参数为非零值,表示要执行注册的处理例程,如果为零,则不执行。
这两个函数必须成对使用,而且必须出现在同一层代码块中。它们允许在线程退出时执行清理工作,比如释放资源、关闭文件等操作。在你的示例中,handler1
()函数就是一个退出处理例程,在子线程的任务函数中使用 pthread_cleanup_push
()注册,在需要时通过pthread_cleanup_pop
()执行。
总的来说,这些函数允许你控制线程的取消行为和在线程退出时执行特定的清理操作,增加了多线程程序的可控性和健壮性,下面是一个例子:
#include
#include
#include
#include
//线程退出处理例程函数
void func(void *arg)
{
printf("线程退出处理例程函数\n");//一般作用用来释放线程退出的时候,没有释放的资源
free((int *)arg);
}
//线程任务函数
void *task(void *arg)
{
int *p = malloc(12);
//压栈线程退出处理例程函数
pthread_cleanup_push(func, p);
int a = 5;
while(a--)
{
printf("线程执行1秒钟的任务\n");
sleep(1);
}
printf("线程结束运行\n");
//弹栈线程退出处理例程函数
pthread_cleanup_pop(-1);
}
int main(int argc, char const *argv[])
{
printf("主线程开始运行\n");
pthread_t pid;
pthread_create(&pid, NULL, task, NULL);
sleep(3);
pthread_cancel(pid);//取消请求
sleep(5);
return 0;
}
更多C/C++语言、Linux系统、数据结构和ARM板实战相关文章,关注专栏:
手撕C语言
玩转linux
脚踢数据结构
系统、网络编程
探索C++
6818(ARM)开发板实战
一键三连喔
~