目录
1. 线程概述
2.主线程和子线程
3.创建线程
线程函数
创建线程示例
4.线程退出
线程退出的原理主要包括以下两个方面:
5.线程回收
回收子线程数据
6.线程分离
7.线程取消
8.线程 ID 比较
1. 线程概述
线程是轻量级的进程(LWP:light weight process),但在 Linux 环境下线程的本质仍是进程。在计算机上运行的程序是一组指令及指令参数的组合,指令按照既定的逻辑控制计算机运行。操作系统会以进程为单位,分配系统资源,可以这样理解,进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。
线程和进程之间的区别:
进程有自己独立的地址空间,多个线程共用同一个地址空间
线程更加节省系统资源,效率不仅可以保持的,而且能够更高
在一个地址空间中多个线程独享:每个线程都有属于自己的栈区,寄存器 (内核中管理的)
在一个地址空间中多个线程共享:代码段,堆区,全局数据区,打开的文件 (文件描述符表) 都是线程共享的
根本区别:线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位
每个进程对应一个虚拟地址空间,一个进程只能抢一个 CPU 时间片
一个地址空间中可以划分出多个线程,在有效的资源基础上,能够抢更多的 CPU 时间片
CPU 的调度和切换:线程的上下文切换比进程要快的多
上下文切换:进程 / 线程分时复用 CPU 时间片,在切换之前会将上一个任务的状态进行保存,下次切换回这个任务的时候,加载这个状态继续运行,任务从保存到再次加载这个过程就是一次上下文切换。
线程更加廉价,启动速度更快,退出也快,对系统资源的冲击小。
在处理多任务程序的时候使用多线程比使用多进程要更有优势,但是线程并不是越多越好,如何控制线程的个数呢?
文件 IO 操作:文件 IO 对 CPU 是使用率不高,因此可以分时复用 CPU 时间片,线程的个数 = 2 * CPU 核心数 (效率最高)
处理复杂的算法 (主要是 CPU 进行运算,压力大),线程的个数 = CPU 的核心数 (效率最高)
2.主线程和子线程
主线程和子线程是多线程并发编程中常见的概念,它们的区别如下:
1. 主线程是程序的入口,是第一个被创建和执行的线程,通常负责执行初始化工作和创建其他子线程。而子线程是主线程创建的线程,可以执行各种任务。
2. 主线程只有一个,而子线程可以有多个。主线程负责创建和管理子线程,当所有子线程结束后,主线程也会随之结束。
3. 主线程在进程启动时被创建,而子线程则在主线程中创建。主线程和子线程的执行顺序不固定,取决于系统的调度算法。
4. 主线程和子线程共享进程的资源,如内存空间、文件句柄等。但是,它们之间的栈空间是独立的。
5. 主线程和子线程可以通过各种同步机制进行通信和协作,如互斥锁、信号量、条件变量等。
总之,主线程和子线程都是并发编程中重要的概念,主线程负责创建和管理子线程,子线程负责执行具体的任务。在实际编程中,需要合理地利用它们,充分发挥多线程的优势,提高程序的效率和性能。
3.创建线程
创建线程的本质是在进程中创建一个独立的执行流,这个执行流与主线程并行执行,共同使用进程的内存空间和其他资源。线程在用户空间被创建,但是线程切换是由操作系统内核来完成的。
创建线程的过程大致如下:
1. 调用线程库中的线程创建函数,传递线程函数和参数,得到一个新的线程ID。
2. 操作系统内核为该线程分配一些资源,如线程栈和寄存器上下文等。
3. 在新线程的栈上创建一个初始的上下文,包括程序计数器等寄存器的值,将线程的入口函数和参数传递给该上下文。
4. 线程被加入到调度队列中,等待被调用执行。
5. 线程被调度器调度执行,执行线程函数。
6. 线程执行完毕后,释放相关资源,退出。
线程的调度和执行由操作系统内核来完成,其中包括线程的转换、运行队列的管理、时间片的分配等。线程的创建和销毁由线程库来管理,线程库会利用操作系统提供的系统调用来实现这些功能。
总的来说,创建线程的本质是向操作系统请求创建一个新的执行流。操作系统会为该线程分配一些资源并建立相关的上下文,程序运行时操作系统会为线程提供调度和执行资源,从而实现多线程并发执行的功能。
线程函数:
每一个线程都有一个唯一的线程 ID,ID 类型为 pthread_t,这个 ID 是一个无符号长整形数,如果想要得到当前线程的线程 ID,可以调用如下函数:
pthread_t pthread_self(void); // 返回当前线程的线程ID
在一个进程中调用线程创建函数,就可得到一个子线程,和进程不同,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作。
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
/*
thread:用于存储新线程的标识符,该标识符可用于后续线程相关的操作。
attr:指向pthread_attr_t类型的指针,用于设置新线程的属性。如果为NULL,则使用默认属性。
start_routine:指向线程函数的指针,该函数会在新线程中执行。该函数必须返回void*类型,并接受一个void*类型的参数。如果线程函数需要传递多个参数,可以将它们封装到一个结构体中,然后将指向该结构体的指针作为arg参数传递给pthread_create()函数。
arg:指向线程函数参数的指针。如果线程函数不需要参数,则可以将其设置为NULL。
pthread_create()函数返回0表示线程创建成功,否则返回错误码。在Linux中,线程是轻量级进程,它们与主线程共享进程的内存空间。可以使用信号量、互斥量等机制来保证线程之间的互斥访问共享资源。
*/
创建线程示例:
创建一个pthread_create.c文件
编辑以下代码
// pthread_create.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 子线程的处理代码
void* working(void* arg)
{
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for (int i = 0; i < 9; ++i)
{
printf("child == i: = %d\n", i);
}
return NULL;
}
int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for (int i = 0; i < 3; ++i)
{
printf("i = %d\n", i);
}
sleep(1);
return 0;
}
然后gcc程序
4.线程退出
在编写多线程程序的时候,如果想要让线程退出,但是不会导致虚拟地址空间的释放(针对于主线程),我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用。
#include <pthread.h>
void pthread_exit(void *retval);
//参数:线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为 NULL
示例:(让主线程退出,观察子线程能否继续执行完毕)
// pthread_create.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 子线程的处理代码
void* working(void* arg)
{
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for (int i = 0; i < 9; ++i)
{
printf("child == i: = %d\n", i);
}
return NULL;
}
int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for (int i = 0; i < 3; ++i)
{
printf("i = %d\n", i);
}
pthread_exit(NULL);
return 0;
}
发现让主线程退出,不影响子线程执行
线程退出的原理主要包括以下两个方面:
1. 线程主动退出:线程可以通过调用pthread_exit函数来主动退出,该函数接受一个参数,表示线程的返回值。当线程调用pthread_exit函数后,线程就会立即退出,并将其返回值传递给其它线程。
2. 线程被动退出:当一个线程执行完毕或者出现异常时,它就会被动退出。在这种情况下,操作系统会自动回收线程的资源,并向其它线程发送一个线程退出信号。
无论是线程主动退出还是被动退出,线程在退出前都需要做一些善后处理工作,例如释放其占用的资源、关闭文件、清理内存等。在多线程编程中,如果线程退出不当,会导致内存泄露、死锁等问题,因此需要特别注意线程的退出处理。通常建议在线程中使用异常处理机制来处理可能发生的异常情况,以确保线程能够正常退出并释放其资源。
5.线程回收
线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做 pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。
另外通过线程回收函数还可以获取到子线程退出时传递出来的数据,函数原型如下:
#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);
/*
thread: 要被回收的子线程的线程 ID
retval: 二级指针,指向一级指针的地址,是一个传出参数,这个地址中存储了 pthread_exit () 传递出的数据,如果不需要这个参数,可以指定为 NULL
返回值:线程回收成功返回 0,回收失败返回错误号。
*/
回收子线程数据
在子线程退出的时候可以使用 pthread_exit() 的参数将数据传出,在回收这个子线程的时候可以通过 phread_join() 的第二个参数来接收子线程传递出的数据。接收数据有很多种处理方式,下面来列举几种:
使用子线程栈
通过函数 pthread_exit(void *retval); 可以得知,子线程退出的时候,需要将数据记录到一块内存中,通过参数传出的是存储数据的内存的地址,而不是具体数据,由因为参数是 void* 类型,所有这个万能指针可以指向任意类型的内存地址。先来看第一种方式,将子线程退出数据保存在子线程自己的栈区:
// pthread_join.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 定义结构
struct Persion
{
int id;
char name[36];
int age;
};
// 子线程的处理代码
void* working(void* arg)
{
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for (int i = 0; i < 9; ++i)
{
printf("child == i: = %d\n", i);
if (i == 6)
{
struct Persion p;
p.age = 12;
strcpy(p.name, "tom");
p.id = 100;
// 该函数的参数将这个地址传递给了主线程的pthread_join()
pthread_exit(&p);
}
}
return NULL; // 代码执行不到这个位置就退出了
}
int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for (int i = 0; i < 3; ++i)
{
printf("i = %d\n", i);
}
// 阻塞等待子线程退出
void* ptr = NULL;
// ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
// 这个内存地址就是pthread_exit() 参数指向的内存
pthread_join(tid, &ptr);
// 打印信息
struct Persion* pp = (struct Persion*)ptr;
printf("子线程返回数据: name: %s, age: %d, id: %d\n", pp->name, pp->age, pp->id);
printf("子线程资源被成功回收...\n");
return 0;
}
运行结果:
使用全局变量
位于同一虚拟地址空间中的线程,虽然不能共享栈区数据,但是可以共享全局数据区和堆区数据,因此在子线程退出的时候可以将传出数据存储到全局变量、静态变量或者堆内存中。在下面的例子中将数据存储到了全局变量中:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 定义结构
struct Persion
{
int id;
char name[36];
int age;
};
struct Persion p; // 定义全局变量
// 子线程的处理代码
void* working(void* arg)
{
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child == i: = %d\n", i);
if(i == 6)
{
// 使用全局变量
p.age =12;
strcpy(p.name, "tom");
p.id = 100;
// 该函数的参数将这个地址传递给了主线程的pthread_join()
pthread_exit(&p);
}
}
return NULL;
}
int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}
// 阻塞等待子线程退出
void* ptr = NULL;
// ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
// 这个内存地址就是pthread_exit() 参数指向的内存
pthread_join(tid, &ptr);
// 打印信息
struct Persion* pp = (struct Persion*)ptr;
printf("name: %s, age: %d, id: %d\n", pp->name, pp->age, pp->id);
printf("子线程资源被成功回收...\n");
return 0;
}
使用主线程栈
虽然每个线程都有属于自己的栈区空间,但是位于同一个地址空间的多个线程是可以相互访问对方的栈空间上的数据的。由于很多情况下还需要在主线程中回收子线程资源,所以主线程一般都是最后退出,基于这个原因在下面的程序中将子线程返回的数据保存到了主线程的栈区内存中:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 定义结构
struct Persion
{
int id;
char name[36];
int age;
};
// 子线程的处理代码
void* working(void* arg)
{
struct Persion* p = (struct Persion*)arg;
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child == i: = %d\n", i);
if(i == 6)
{
// 使用主线程的栈内存
p->age =12;
strcpy(p->name, "tom");
p->id = 100;
// 该函数的参数将这个地址传递给了主线程的pthread_join()
pthread_exit(p);
}
}
return NULL;
}
int main()
{
// 1. 创建一个子线程
pthread_t tid;
struct Persion p;
// 主线程的栈内存传递给子线程
pthread_create(&tid, NULL, working, &p);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}
// 阻塞等待子线程退出
void* ptr = NULL;
// ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
// 这个内存地址就是pthread_exit() 参数指向的内存
pthread_join(tid, &ptr);
// 打印信息
printf("name: %s, age: %d, id: %d\n", p.name, p.age, p.id);
printf("子线程资源被成功回收...\n");
return 0;
}
在上面的程序中,调用 pthread_create() 创建子线程,并将主线程中栈空间变量 p 的地址传递到了子线程中,在子线程中将要传递出的数据写入到了这块内存中。也就是说在程序的 main() 函数中,通过指针变量 ptr 或者通过结构体变量 p 都可以读出子线程传出的数据。
6.线程分离
线程分离是指在创建的线程完成执行之后,将其资源回收并释放,而不需要其他线程对其进行等待。一般情况下,线程分离可以通过调用pthread_detach函数来实现。
线程分离会使得线程的状态变成“分离状态”,分离状态的线程资源(如线程栈内存等)会被系统回收。但要注意,只有在线程执行完毕或主线程调用pthread_join之前,才能调用pthread_detach函数来将线程分离。
线程分离的好处是可以避免因为线程资源未及时回收而导致的内存泄漏问题。但需要注意,如果线程分离之后,再次对其进行操作可能会导致程序崩溃等问题,因此在进行线程分离之前需要仔细考虑。
#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);
7.线程取消
线程取消的意思就是在某些特定情况下在一个线程中杀死另一个线程。使用这个函数杀死一个线程需要分两步:
在线程 A 中调用线程取消函数 pthread_cancel,指定杀死线程 B,这时候线程 B 是死不了的
在线程 B 中进程一次系统调用(从用户区切换到内核区),否则线程 B 可以一直运行。
#include <pthread.h>
// 参数是子线程的线程ID
int pthread_cancel(pthread_t thread);
8.线程 ID 比较
在 Linux 中线程 ID 本质就是一个无符号长整形,因此可以直接使用比较操作符比较两个线程的 ID,但是线程库是可以跨平台使用的,在某些平台上 pthread_t 可能不是一个单纯的整形,这中情况下比较两个线程的 ID 必须要使用比较函数,函数原型如下:
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
//参数:t1 和 t2 是要比较的线程的线程 ID
//返回值:如果两个线程 ID 相等返回非 0 值,如果不相等返回 0
总结:本文讲述了什么是线程,以及在Linux系统下线程的相关操作