c2c网站开发策划,成都市 网站建设,专业官网建设,网站如何交换链接本篇文章主要对线程的概念和线程的控制进行了讲解。其中我们再次对进程概念理解。同时对比了进程和线程的区别。希望本篇文章会对你有所帮助。 文章目录 一、线程概念 1、1 什么是线程 1、2 再次理解进程概念 1、3 轻量级进程 二、进程控制 2、1 创建线程 pthread_create 2、2… 本篇文章主要对线程的概念和线程的控制进行了讲解。其中我们再次对进程概念理解。同时对比了进程和线程的区别。希望本篇文章会对你有所帮助。 文章目录 一、线程概念 1、1 什么是线程 1、2 再次理解进程概念 1、3 轻量级进程 二、进程控制 2、1 创建线程 pthread_create 2、2 线程与进程资源 2、3 线程id 2、4 获得线程id pthread_ self 2、5 线程等待 pthread_join 2、6 线程终止 pthread_exit、pthread_cancel 2、6、1 pthread_exit 2、6、2 pthread_cancel 2、7 线程分离 pthread_detach 三、总结 ♂️ 作者Ggggggtm ♂️ 专栏Linux从入门到精通 标题线程控制 ❣️ 寄语与其忙着诉苦不如低头赶路奋路前行终将遇到一番好风景 ❣️ 一、线程概念
1、1 什么是线程 在一个程序里的一个执行路线就叫做线程thread线程在进程内部运行。什么是执行线路呢怎么是在进程内部运行的呢下面我们通过进程进行理解。 1、2 再次理解进程概念 我们之前学的进程是进程就是内核数据结构代码同时每个进程都有自己独立的内核数据结构以保持进程的独立性。具体如下图 现在我们用了特定的技术。只创建进程控制块PCBtask_struct,而不再创建对应的地址空间和页表进行映射。我们新创建的进程控制块PCBtask_struct让它指向我们已经存在的进程的地址空间。具体如下图 正如上图所示所有的进程控制块PCBtask_struct共享了大部分资源。而这些资源均来自于我们第一个创建的进程控制块PCBtask_struct。 上图的每个进程控制块PCBtask_struct执行时都是用的同一块进程地址空间。而每个进程控制块PCBtask_struct可称之为线程。我们现在再来理解在一个程序里的一个执行路线就叫做线程这个概念就不难理解了。其实就是一个 task_struct 所对应的运行起来后就是一个执行流。透过进程虚拟地址空间可以看到进程的大部分资源将进程资源合理分配给每个执行流就形成了线程执行流。 此时可能会有点疑惑之前的进程和现在的线程有什么区别呢 进程是独立的执行单元拥有独立的内存空间包括代码段、数据段和堆栈因此进程之间的数据不共享。而线程是进程内的执行单元多个线程共享同一个进程的内存空间包括代码和数据。具体也可结合下图理解 如上图所示现在的进程是包含了多个进程控制块PCBtask_struct和其对应的内数据结构。我们之前所学的进程里面只有一个执行流而现在就不同了。 一个进程内最少有一个执行流也就是一个进程内部最少有一个线程主线程。我们在主线程最初的进程控制块PCBtask_struct也可理解为第一个进程控制块PCBtask_struct內部可创建多个新线程也就是创建进程控制块PCBtask_struct。 站在用户的角度我们理解进程进程内核数据结构对应的代码和数据。站在内核的角度我们理解进程承担分配系统资源的实体。 1、3 轻量级进程 在CPU调度中只会对进程控制块PCBtask_struct进行调度。并不会关心是一个进程或者线程。我们也可以认为CPU进行调度的单位是线程。在Linux系统中在CPU眼中看到的PCB都要比传统的进程更加轻量化。为什么呢 它们与传统进程相比具有较小的资源占用和更快的创建、切换以及通信速度。轻量化进程具有以下特点 共享地址空间不同于传统进程拥有独立的地址空间轻量化进程与父进程共享相同的地址空间。这样可以减少内存开销和减少上下文切换的开销。 轻量级创建和销毁由于轻量化进程与父进程共享地址空间创建和销毁的开销较小因为无需分配新的地址空间和重复加载代码段等操作。 快速上下文切换由于轻量化进程共享相同的地址空间所以在进行线程之间的切换时不需要切换页表从而使得上下文切换速度更快。 共享资源轻量化进程可以通过共享内存等机制方便地进行线程间通信和共享数据避免了复杂的进程间通信机制带来的开销。 并发执行轻量化进程可以在多个CPU核心上并发执行充分利用多核处理器的性能。 所以在Linux下进程控制块又称之为轻量级进程Lightweight Process。 二、进程控制 上面我们了解了进程的概念后我们接下来看看在Linux怎么创建进程和对进程的一系列操作。 2、1 创建线程 pthread_create 在学习pthread_create之前我们先了解一下第三方库。本片文章不再讲解Linux操作系统提供了一种创建线程的接口。选择使用第三方库pthread来实现创建线程的一系列操作。pthread库提供了更多功能和跨平台的能力使得多线程编程更加便捷和灵活。而且大部分语言底层封装的就是第三方库pthread。 pthread_create函数是pthread库中用于创建线程的函数。下面是对pthread_create函数使用的详细解释 int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg); thread指向pthread_t类型变量的指针用于存储新线程的标识符。在成功创建线程后该指针将被填充。attr指向pthread_attr_t类型变量的指针用于设置新线程的属性。可以为NULL表示使用默认属性。start_routine指向一个返回类型为void*、接受一个void*参数的函数指针。该函数是新线程的起始点线程将从这个函数开始执行。arg传递给start_routine函数的参数。它是一个void*类型的指针可以传递任何类型的数据通常用于向新线程传递参数。 返回值的含义 返回值为0表示线程创建成功。返回值为正整数表示线程创建失败具体的返回值通常对应不同的错误情况可以使用errno来获取错误码并查看具体错误信息。常见的错误码包括 EAGAIN当前系统资源不足无法创建线程。EINVAL传递给pthread_create函数的参数无效。EPERM没有足够的权限来创建线程。 下面我们看一个实际的例子 #includeiostream
#includepthread.h
#includeunistd.husing namespace std;void* fun(void* name)
{cout (char*)name , pid: getpid() endl;
}int main()
{pthread_t id;int npthread_create(id,nullptr,fun,(void*)new thread 1);sleep(1);cout main thread , pid: getpid() endl;return 0;
} 我们上面就是简单创建了一个线程然后进行打印不同的内容和进程id。运行结果如下图 我们发现他们的进程id确实是相同的。也就我们上述所说的线程在进程内部运行。 我们再看下一段代码 int x 100;void show(const string name)
{cout name , pid: getpid() x \n endl;
}void *threadRun(void *args)
{const string name (char *)args;while (true){show(name);sleep(1);}
}int main()
{pthread_t tid[5];char name[64];for (int i 0; i 5; i){snprintf(name, sizeof name, %s-%d, thread, i);pthread_create(tid i, nullptr, threadRun, (void *)name);sleep(1); // 缓解传参的bug}while (true){cout main thread, pid: getpid() endl;sleep(3);}
} 上述代码就是创建了多个线程去执行同一个函数。同时打印一个全局变量和进程id。我们看运行结果 我们也不难发现线程之间确实有共享的一部分数据。上述例子中的全局变量就被所有线程执行流共享。那么线程如何看待进程内部的资源呢 2、2 线程与进程资源 我们知道线程大部分资源是与进程共享的。同时线程也是拥有属于自己的资源。那么到底有哪些资源共享有哪些资源私有呢 在Linux下线程与进程之间共享的资源有以下几种 内存空间线程和进程都可以访问相同的内存空间包括代码段、数据段和堆栈段。这意味着线程可以读取和修改相同的变量和数据结构而不需要进行显式的通信。 文件描述符每个进程都有一张文件描述符表用于跟踪它们打开的文件。当一个线程打开或关闭文件描述符时其他线程也可以通过相同的文件描述符进行访问。 信号处理器进程中的信号处理器对所有线程可见当一个线程接收到信号时所有线程都可以对其进行处理。 共享库和代码段共享库和可执行文件的代码段可以被多个线程共享。这意味着不同的线程可以同时执行相同的函数或方法。 其他系统资源还有一些其他资源如进程ID、进程组ID、用户ID等在一个进程中创建的线程也会继承这些属性。 每个线程私有的资源主要包括以下几种 栈每个线程都有自己的栈空间用于保存函数调用、局部变量和返回地址等信息。 寄存器线程使用寄存器来保存当前执行的上下文信息包括程序计数器、栈指针等。 线程特定数据线程可以使用线程特定数据Thread-Specific DataTSD来存储每个线程独有的数据。这些数据在同一进程的不同线程之间是隔离的。 线程ID每个线程都有唯一的线程ID用于标识线程的身份。 错误号变量每个线程有自己的错误号变量用于保存最近的系统调用错误码。 2、3 线程id 细心的同学发现了上述并没对线程创建的参数pthread_t *thread 进行过多解释。那么pthread_t 是什么类型呢其实 pthread_t 是一个 unsigned long int 类型的。又有什么用呢 我们在Linux下可通过指令ps -aL来查看进程和线程资源。具体如下图 其中我们看到有PID、LWP。LWPLightweight Process所对应的就是线程的id。PID与LWP相等的就是主线程。这里的LWP与pthread_t *thread是一样的吗我们不妨打印一下看看pthread_t *thread的值。如下图 事实上这里所说的 thread 与我们上述将LWP的id值并不是相同的。pthread_t *thread到底指的是什么呢取决于实现。对于Linux目前实现的NPTL实现而言pthread_t类型的线程ID本质就是一个进程地址空间上的一个地址。怎么是地址呢我们接着往下看。 我们知道线程的执行过程中需要保存和管理各自的局部变量、函数调用以及其他线程执行时需要的临时数据。每个线程是必须有自己的栈空间。如果没有独立的栈空间那么每个线程在压栈的时候数据就会混乱。但是进程地址空间只有一个栈空间。怎么保证每个栈都有独立的占空间呢具体如下图 为了保证每个线程有独立的栈空间在每当创建一个线程的时候都会在共享内存区为线程创建一个独立的的struct pthread当中包含了对应线程的各种属性包括栈空间。每个线程都有自己的线程局部存储当中包含了对应线程被切换时的上下文数据。而这部分数据是有线程库给我们创建和维护的。 每一个新线程在共享区都有这样一块区域对其进行描述怎么找到这块空间呢于是当创建成功后就会把该块空间的起始地址进行返回而pthread_t *thread就是接受的该地址 pthread_ create函数会产生一个线程ID存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程是操作系统调度器的最小单位所以需要一个数值来唯一表示该线程。pthread_ create函数第一个参数指向一个虚拟内存单元该内存单元的地址即为新创建线程的线程ID属于NPTL线程库的范畴。线程库的后续操作就是根据该线程ID来操作线程的。 线程库NPTL提供了pthread_ self函数可以获得线程自身的ID。 2、4 获得线程id pthread_ self pthread_self()函数是POSIX线程库中的一个函数它用于获取当前线程的线程ID。该函数的定义如下所示 pthread_t pthread_self(void); 在调用该函数时它会返回一个用于表示当前线程的pthread_t类型的值。通常我们将这个值存储在一个变量中以便后续使用。下面是一个使用pthread_self()函数的示例 #include stdio.h
#include pthread.hvoid* thread_func(void* arg) {pthread_t tid pthread_self();printf(Thread ID: %lu\n, tid);// 执行其他操作...return NULL;
}int main() {pthread_t tid;pthread_create(tid, NULL, thread_func, NULL);pthread_join(tid, NULL); // 用来阻塞等待线程回收资源 return 0;
} 运行结果如下 2、5 线程等待 pthread_join 线程等待是什么呢与进程等待相似。线程在创建并执行的时候线程也是需要进行等待的如果主线程如果不等待即会引起类似于进程的僵尸问题导致内存泄漏。其主要原因是已经退出的线程其空间没有被释放仍然在进程的地址空间内。创建新的线程不会复用刚才退出线程的地址空间。 函数原型如下 int pthread_join(pthread_t thread, void **value_ptr); 参数说明 thread要等待的线程的标识符类型为pthread_t。value_ptr指向一个指针的指针用于存储被等待线程的返回值。返回值通过该指针间接传递给调用者。 返回值 成功返回0失败返回错误码 函数功能 当调用pthread_join时会阻塞当前线程直到指定的线程完成其执行并返回其返回值。如果线程已经结束那么pthread_join会立即返回。当一个线程终止后它的返回值会被保留起来并且可以由其他线程使用pthread_join进行获取。 我们先来看一下其使用稍后会解释返回值的情况代码如下 void* fun(void* name)
{int cnt5;while(true){cout (char*)name , pid: getpid() endl;sleep(1);if(!--cnt)break;}
}int main()
{pthread_t id;int npthread_create(id,nullptr,fun,(void*)new thread 1);(void)n;int* ret;pthread_join(id,(void**)ret);cout main thread , pid: getpid() endl;return 0;
} 直接看运行结果 根据结果看到时进行阻塞式等待。能不能像父进程等待子进程那样进行循环检测等待呢答案是不能的。 那么接下来我们再看一下其value_ptr到底是什么和它是怎么来的。 当我们使用pthread_create创建线程时其返回值是void*。而pthread_join的第二个参数就是接受的线程结束的返回值。这也是其类型为void** 的原因。我们不妨通过代码来看一下。代码如下 void* fun(void* name)
{int cnt1;while(true){cout (char*)name , pid: getpid() endl;sleep(1);if(!--cnt)break;}return (void*) 10;
}int main()
{pthread_t id;int npthread_create(id,nullptr,fun,(void*)new thread 1);(void)n;int* retnullptr;pthread_join(id,(void**)ret);cout main thread , pid: getpid() endl;cout return value: (int)ret endl;return 0;
} 注意返回值是一个以指针的形式进行返回的。我们可以对其进行强制类型转换后打印不可以对其进行解引用。否泽就会引起段错误。具体结果如下 2、6 线程终止 pthread_exit、pthread_cancel 如果需要只终止某个线程而不终止整个进程,可以有三种方法: 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。线程可以调用pthread_ exit终止自己。 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。 接下来我们看看其用法和细节都有哪些。 2、6、1 pthread_exit pthread_exit是、用于终止当前线程的执行并返回一个特定的值。它可以通过调用pthread_exit来显式地结束线程也可以在线程函数的返回语句中隐式调用。 下面是一个简单的示例说明了pthread_exit的使用方法和其中的一些细节 void* thread_function(void* arg) {int thread_arg *(int*)arg;// 打印线程参数printf(Thread argument: %d\n, thread_arg);// 结束线程pthread_exit((void**)13);
}int main() {pthread_t thread;int arg_value 100;// 创建线程并传递参数if (pthread_create(thread, NULL, thread_function, (void*)arg_value) ! 0) {fprintf(stderr, Failed to create thread.\n);return 1;}// 等待线程结束int* thread_result0;int npthread_join(thread, (void**)thread_result);(void)n;coutthread_result : (int)thread_resultendl;printf(Thread finished.\n);return 0;
} 在上述示例中我们首先创建了一个线程由pthread_create函数执行并将参数arg_value传递给线程函数thread_function。在线程函数中我们将打印出传递的参数值然后通过调用pthread_exit函数来显式地终止线程的执行。 参数可以是任意类型的指针void*用于传递线程的退出状态。通常情况下这个参数被用来告知父线程关于子线程执行的结果或者其他相关信息。当线程调用pthread_exit时它会将退出状态作为返回值传递给等待它的父线程。 当线程终止时它的资源会被自动释放包括线程栈和线程局部变量等。同时父线程也可以通过pthread_join函数来等待子线程的退出并获取其退出状态。 下面我们来看一下上述的运行结果具体如下图 我们之前学过exit函数用来终止当前运行的程序。但是exit函数和 pthread_exit函数是有所区别的。exit函数是用来终止进程的。当我们在新线程中使用exit函数那么整个进程将会被终止掉。 2、6、2 pthread_cancel pthread_cancel函数是用于取消线程的函数它允许一个线程取消同一进程中的另一个线程的执行。pthread_cancel函数原型如下 int pthread_cancel(pthread_t thread); 参数 pthread_t thread目标线程的标识符即要取消的线程。 返回值 成功返回0。失败返回非0的错误码表明函数调用失败的具体原因。 下面我们来看一个实际例子来理解一下 pthread_cancel函数 的使用。 // 目标线程函数
void* threadFunc(void* arg) {std::cout Thread has started. std::endl;// 模拟工作for (int i 0; i 10; i) {std::cout Working... std::endl;sleep(1);}std::cout Thread is finished. std::endl;// 清理工作释放资源pthread_exit(NULL);
}int main() {pthread_t tid;// 创建目标线程if (pthread_create(tid, NULL, threadFunc, NULL) ! 0) {std::cerr Failed to create thread. std::endl;return 1;}// 主线程等待一段时间sleep(3);// 向目标线程发送取消请求if (pthread_cancel(tid) ! 0) {std::cerr Failed to cancel thread. std::endl;return 1;}// 等待目标线程结束if (pthread_join(tid, NULL) ! 0) {std::cerr Failed to join thread. std::endl;return 1;}std::cout Main thread is finished. std::endl;return 0;
} 上述例子我们就是使用了pthread_cancel来终止线程。当然线程还没有运行结束时就对其进行终止。具体运行结果如下图 能不能在线程的內部进行自己终止自己呢代码如下 pthread_cancel(pthread_self()); 这是一个取消自身线程的操作。首先取消自身线程可能会导致未完成的工作无法正常结束尤其是当你的线程在执行某些关键任务时。这可能导致资源泄漏或数据不一致的问题。 其次取消自身线程可能打破了线程安全的设计原则。如果其他线程依赖于你的线程的状态或结果那么取消自身线程可能会导致这些线程的行为出现问题。因此一般来说推荐使用pthread_cancel函数取消其他线程而不是自身线程。这样可以更好地控制线程的取消操作并确保线程能够优雅地退出以避免可能的问题。 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。 2、7 线程分离 pthread_detach pthread_detach() 函数用于将指定线程标记为分离状态。当一个线程被标记为分离状态后该线程的系统资源将在其退出时自动释放无需其他线程调用 pthread_join() 来获取其返回状态。 下面是一个示例演示了如何使用 pthread_detach() 函数将线程设置为分离状态 void* thread_function(void* arg) {printf(子线程正在执行\n);sleep(3);printf(子线程执行完毕\n);return NULL;
}int main() {pthread_t tid;if (pthread_create(tid, NULL, thread_function, NULL) ! 0) {printf(线程创建失败\n);return 1;}if (pthread_detach(tid) ! 0) {printf(线程分离失败\n);return 1;}printf(主线程继续执行\n);sleep(5);printf(主线程执行完毕\n);return 0;
} 在上述示例中我们首先创建了一个新的线程线程函数被设计为休眠3秒后退出。然后我们调用 pthread_detach() 函数将线程 tid 标记为分离状态。之后主线程继续执行并休眠5秒后退出。 由于我们将线程 tid 分离因此不需要调用 pthread_join() 来等待子线程结束。相反当线程 tid 执行完毕时系统将自动回收其资源。 三、总结 当了解完线程的控制以后我们先大概的总结一下线程的优缺点。 线程的优点 创建一个新线程的代价要比创建一个新进程小得多。与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多。线程占用的资源要比进程少很多。能充分利用多处理器的可并行数量。在等待慢速I/O操作结束的同时程序可执行其他的计算任务。计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现。I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。 线程的缺点 难以调试和协调多线程程序因为涉及到共享资源的并发访问会面临复杂的调试和协调问题。例如线程间的竞争条件Race Condition会导致数据不一致或死锁等问题这些问题难以定位和排查。 资源消耗线程的创建和销毁需要消耗系统资源包括内存和CPU时间片等。同时线程之间的切换也会引入一定的开销。过多的线程数量可能会导致系统资源耗尽或降低整体性能。 容易出现同步问题多线程程序在访问共享资源时需要进行同步操作如加锁和解锁。而同步操作的过度使用可能导致性能下降因为在执行同步操作期间其他线程可能被阻塞等待资源释放。 可能引发安全问题多线程程序中存在着线程间的竞争如果没有正确处理竞争条件可能会引发安全问题如数据损坏、数据泄露等。 编程复杂性高多线程编程相对于单线程编程来说更加复杂需要考虑并发控制、同步机制等问题。编写高效且正确的多线程程序需要对并发编程概念和技术有深入的了解对开发者的要求较高。 上述的线程缺点大部分来自于代码的问题。当然线程的大部分问题对一个优秀的的程序员来说并不是问题。 线程还有异常问题单个线程如果出现除零野指针等问题导致线程崩溃进程也会随着崩溃。为什么呢主要是因为线程共享进程的资源。 线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该进程内的所有线程也就随即退出 在一个进程中多个线程共享相同的内存空间和其他系统资源。这意味着当一个线程发生崩溃时它可能会影响到共享的资源。例如如果一个线程遇到除零错误导致异常终止它可能会导致相关的共享数据被破坏或变得不可用。 此外操作系统为了保证进程的稳定性和安全性在出现线程崩溃的情况下通常会终止整个进程。这是因为一个线程的崩溃可能会对其他线程产生意想不到的影响进而导致进程无法继续正常运行。为了避免这种情况下可能出现的更严重的问题操作系统会选择终止整个进程以确保系统的稳定性。 那么上述我们也讲解了线程分离。如果线程分离后线程出现错误导致崩溃后会引起整个进程进行崩溃吗答案是会的因为本质上他们还是在共享一个进程的资源。