文章目录
- 1.fork() / vfork / clone
- 2.线程等待
- 2.1pthread_join()
- 2.2pthread_tryjoin_np()
- 3.pthread_exit()
- 4.pthread_cancel()
- 5.一些线程相关的问题
- 6.pthread_detach()
- 7.pthread_self()
- 8.认识线程标识符:pthread_self()获取线程标识符
- 9.POSIX线程库
1.fork() / vfork / clone
在Linux系统中,fork(), vfork(), 和 clone() 这三个函数都用于创建新的进程或线程。
fork()
返回值:
在父进程中,fork() 返回新创建的子进程的PID。
在子进程中,fork() 返回0。
参数:
fork() 不带任何参数。
工作原理:
fork() 创建当前进程的副本,包括父进程的代码、数据、堆、栈等内容都会被复制到子进程中。子进程(子线程)是父进程(父线程)的复制品,它从父进程继承了几乎所有的属性,包括父进程的进程ID、环境变量、打开的文件描述符等。
作用:
fork() 主要用于创建一个新的进程,这个新进程可以执行与父进程相同的程序,也可以调用 exec() 系列函数来执行另一个程序。
vfork()
返回值:
在父进程中,vfork() 的返回值与 fork() 相同,即子进程的PID。
在子进程中,vfork() 同样返回0。
参数:
vfork() 同样不带任何参数。
工作原理:
vfork() 与 fork() 的主要区别在于,vfork() 创建的子进程并不复制父进程的地址空间,而是共享父进程的地址空间。因此,vfork() 创建的子进程会直接在父进程的地址空间中运行,而不是在复制的地址空间中运行。这使得 vfork() 的创建过程比 fork() 更高效,但也带来了更多的风险,因为父子进程共享地址空间,所以必须小心避免同时修改数据。
作用:
vfork() 通常用于创建一个新的进程来执行另一个程序(通过 exec() 系列函数)。由于子进程直接在父进程的地址空间中运行,因此这种方式通常比 fork() + exec() 更高效。
clone()
返回值:
在父进程中,clone() 返回新创建的子线程的PID。
在子线程中,clone() 返回0。
参数:
clone() 函数有多个参数,其中最重要的是:
fn:新线程执行的函数。
arg:传递给 fn 的参数。
flags:标志位,用于指定如何创建新线程。
child_stack:子线程的栈指针。
ptid:父进程中的变量,用于存储新线程的PID。
工作原理:
clone() 函数用于创建新的线程,而不是进程。它允许调用者指定哪些资源(如内存、文件描述符等)应该被新线程共享,哪些应该被复制。这使得 clone() 在创建轻量级线程时比 fork() 更高效。
作用:
clone() 主要用于创建线程,这些线程在共享某些资源的同时,也可以有自己的执行上下文和栈空间。这使得线程之间的通信和协作比进程更加高效。
三者之间的异同
相同点:
这三个函数都用于创建新的进程或线程。
在父进程/父线程中,这些函数都返回新创建的子进程/子线程的PID。
在子进程/子线程中,这些函数都返回0。
不同点:
- 复制的资源:fork() 复制父进程的所有资源;vfork() 与父进程共享地址空间;clone() 可以选择性地复制父进程的资源。
- 用途:fork() 通常用于创建一个新的进程来执行相同的或不同的程序;vfork() 通常用于创建一个新的进程来执行另一个程序;clone() 主要用于创建轻量级的线程。
- 效率:由于 vfork() 和 clone() 在资源复制和共享方面的优化,它们通常比 fork() 更高效。
- 风险:由于 vfork() 创建的子进程与父进程共享地址空间,因此必须小心避免数据竞争和其他并发问题。而 fork() 和 clone() 则不存在这个问题。
线程除零中断会发生什么
在Linux下,线程执行除零操作会导致一个运行时错误,具体表现为一个算术异常(Arithmetic Exception)。在大多数现代操作系统和编程语言中,包括Linux和C/C++,直接执行除零操作是未定义行为(Undefined Behavior),并且通常会导致程序崩溃。
当线程尝试执行除零操作时,操作系统会捕获到这个异常,并通常通过发送一个信号(如SIGFPE,即浮点异常信号)来通知进程。这个信号可以由进程中的信号处理函数来处理,或者如果没有设置相应的信号处理函数,那么进程(包括其中的所有线程)将会终止。
在C或C++程序中,如果你没有设置特定的信号处理函数,并且你的程序试图执行除零操作,你通常会看到类似“Floating point exception (core dumped)”的错误消息,并且程序会异常终止。
为了避免这种情况,你应该在编写代码时仔细检查所有除法操作,确保除数不为零。你可以通过添加条件检查来实现这一点,例如:
if (divisor != 0) { result = dividend / divisor;
} else { // 处理除数为零的情况,例如设置错误标志或抛出异常
}
此外,使用浮点数时,还应该注意NaN(Not a Number)和无穷大值的情况,这些也可能导致类似的算术异常。
在多线程环境中,每个线程都有自己的执行上下文,包括自己的栈和寄存器状态。因此,一个线程的除零错误不会直接影响其他线程的执行。但是,如果除零错误导致整个进程崩溃,那么所有线程都将停止运行。
总之,Linux下线程执行除零操作会导致运行时错误,并可能导致程序崩溃。为了避免这种情况,你应该在代码中添加适当的检查和处理机制。
linux下pthread_create和clone
在Linux下,pthread_create和clone都是用于创建新线程的系统调用或库函数,但它们在使用、目的和上下文中有一些重要的区别。
pthread_create
pthread_create是POSIX线程库(Pthreads)中的一个函数,用于在程序中创建新的线程。它提供了跨平台的线程创建机制,使得开发者可以在多种操作系统上编写可移植的多线程程序。
函数原型:
c
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
thread:指向一个pthread_t类型的指针,用于存储新创建的线程的标识符。
attr:指向线程属性对象的指针,用于设置线程的属性,如堆栈大小、调度策略等。通常设置为NULL以使用默认属性。
start_routine:新线程开始执行时调用的函数(线程函数)。
arg:传递给线程函数的参数。
工作原理:
当调用pthread_create时,它会创建一个新的线程,并将该线程的标识符存储在thread指向的位置。新线程将开始执行start_routine指向的函数,并将arg作为参数传递给该函数。
clone
clone是Linux特有的系统调用,用于创建新的进程或线程。与fork系统调用相比,clone提供了更细粒度的控制,允许调用者选择性地共享父进程的资源(如内存空间、文件描述符等)。
函数原型:
c
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...);
fn:新进程或线程开始执行时调用的函数。
child_stack:指向新线程堆栈的指针。
flags:控制clone行为的标志位,如CLONE_VM、CLONE_FS等,用于指定要共享哪些资源。
arg:传递给新进程或线程函数的参数。
工作原理:
clone根据提供的标志位创建一个新的进程或线程。新进程或线程将开始执行fn指向的函数,并将arg作为参数传递给该函数。与pthread_create不同,clone允许调用者更直接地控制新进程或线程与父进程之间的资源共享关系。
异同点
跨平台性:pthread_create是POSIX标准的一部分,因此具有更好的跨平台性,可以在多种操作系统上使用。而clone是Linux特有的系统调用,只能在Linux上使用。
库与系统调用:pthread_create是库函数,而clone是系统调用。这意味着pthread_create提供了更高级别的抽象和封装,使得开发者可以更容易地编写多线程程序,而无需直接与系统调用打交道。
资源共享:clone允许调用者更直接地控制新进程或线程与父进程之间的资源共享关系,通过标志位来指定要共享哪些资源。而pthread_create则提供了更统一的线程创建机制,但资源共享方面可能不如clone灵活。
可移植性与易用性:由于pthread_create是POSIX标准的一部分,因此它提供了更好的可移植性。同时,Pthreads库也提供了丰富的线程管理功能,使得开发者可以更容易地编写多线程程序。而clone虽然提供了更底层的控制,但使用起来可能相对复杂一些。
总的来说,pthread_create和clone都是用于创建新线程的机制,但它们在跨平台性、资源共享、可移植性和易用性方面存在一些差异。开发者可以根据具体需求和目标平台选择适合的机制来创建和管理线程。
2.线程等待
2.1pthread_join()
创建子线程后,主线程同样需要等待子线程退出,获取子线程退出结果,然后操作系统回收子线程PCB。如果主线程不等待子线程,就会引起类似僵尸进程的问题,从而导致内存泄漏。与进程等待不同,线程等待不需要关心子线程是否异常,因为一旦子线程出现异常,整个进程就会随之崩溃。线程的异常退出信号就是进程的异常退出信号。主线程调用pthread_join阻塞等待子线程,让线程退出具有一定的顺序性,将来可以让主线程进行更多的收尾工作。
pthread_join 是 POSIX 线程(Pthreads)库中的一个函数,用于等待一个线程的结束,并获取该线程的返回值(如果有的话)。以下是 pthread_join 函数的参数和返回值的详细解释:
参数
pthread_t thread:
这是一个线程标识符,指定了要等待的线程。通常,这个线程标识符是通过 pthread_create 函数返回的。
void **retval:
这是一个指向 void * 类型的指针的指针。如果此参数不为 NULL,则 pthread_join 会将已结束线程的返回值存储在这个位置。线程返回值是线程启动例程(通过 pthread_create 指定的函数)的返回值。
返回值
如果函数成功,返回 0。
如果函数失败,返回一个错误码。例如,如果指定的线程标识符无效,或者该线程不是可加入的(即,已经被分离),函数会返回错误。
工作原理
当一个线程调用 pthread_join 并指定要等待的另一个线程时,调用线程会阻塞,直到指定的线程结束执行为止。如果指定线程已经结束,pthread_join 会立即返回。
pthread_join 的主要目的是:
-
同步:确保一个线程在另一个线程结束之前不会继续执行。这对于协调线程间的操作特别有用,特别是当需要确保一个线程完成其任务后,另一个线程才能开始或继续其工作时。
-
资源回收:通过调用 pthread_join,可以确保线程的资源(如栈空间)被正确地回收。如果一个线程没有通过 pthread_join 或 pthread_detach 被明确地处理,它可能会变成僵尸线程,浪费系统资源。
-
获取返回值:如果线程有返回值(通过其启动例程),pthread_join 可以让其他线程获取这个返回值。
注意事项
- 如果一个线程是“分离的”(通过 pthread_detach 函数),则无法对其调用 pthread_join。尝试这样做会导致错误。
- 多次对同一个线程调用 pthread_join 会导致错误。
- 如果不需要线程的返回值,可以将 retval 参数设置为 NULL。
总的来说,pthread_join 是一个重要的线程同步工具,用于协调线程的执行顺序,并确保线程资源的正确回收。
回顾指针
测试代码:线程的堆数据可被该进程的所有线程共享
pthread_create的启动例程threadRoutine的返回值可由pthread_join的参数retval获取,进而在主线程中可以访问到新线程的数据(前提是可以访问,数据的共有/私有在上篇博客)。
- 如果新线程异常中断,主线程怎么知道这个信息?
压根不用获取,线程异常中断,主线程和所有线程直接退出。
#include <iostream>
#include <thread>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
using namespace std;void *threadRoutine(void *args)
{int i = 0;int *data = new int[10];while (true){cout << "新线程: " << (char *)args << " running ..." << endl;sleep(1);data[i] = i;if (i++ == 3)break;}cout << "new thread quit!" << endl;return (void *)data;// return (void *)10; 这里的10仅是为了测试新线程能够将数据传给主线程 若非要探讨他的含义,就是传回去一个地址,这个地址是10,即也许某一个变量的地址是10(实际无意义,如果去访问地址为10的数据,有可能报错。)// 返回给给main thread, main通过pthread_join获取
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");int *ret = nullptr;pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出for (int i = 0; i < 10; i++){cout << ret[i] << " ";}cout << endl;
//如果启动例程传(void *)10,这里要输出10这个整数(实际是一个地址),要强转(long long)ret【ret64位下是8字节】cout << "main thread wait done, main quit!" << endl;return 0;
}
新线程内部调用exit
不要调用,exit是终止进程而不是终止线程的。
测试代码主线程和新线程共同维护数据
#include <iostream>
#include <thread>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
using namespace std;
void *ThreadRoutine(void *data)
{cout << "[" << getpid() << "]: child thread running!" << endl;// 子线程处理堆空间数据for (int i = 0; i < 10; ++i){((int *)data)[i] = i;}cout << "[" << getpid() << "]: child thread quit!" << endl;// 线程函数返回,线程退出return (void *)data; // 返回堆空间的指针
}int main()
{cout << "[" << getpid() << "]: main thread running!" << endl;// 创建一批堆区数据int *data = new int[10]{0};cout << "before: ";for (int i = 0; i < 10; ++i){cout << data[i] << " ";}cout << endl;pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, (void *)data);int *ret;pthread_join(tid, (void **)&ret); // 阻塞等待子线程退出// 打印处理后的堆空间数据cout << "data change: ";for (int i = 0; i < 10; ++i){cout << data[i] << " ";}cout << endl;cout << "show ret: ";for (int i = 0; i < 10; ++i){cout << ret[i] << " ";}cout << endl;cout << "[" << getpid() << "]: main thread quit!" << endl;// 释放堆空间delete[] data;return 0;
}
2.2pthread_tryjoin_np()
C++中,可以使用pthread_tryjoin_np函数来进行非阻塞等待线程
#include <pthread.h>
int pthread_tryjoin_np(pthread_t thread, void **retval);
返回值
线程函数已经结束,pthread_tryjoin_np返回0,表示线程成功结束;
线程函数尚未结束,pthread_tryjoin_np返回EBUSY,表示线程尚未结束;
出现其他错误,pthread_tryjoin_np会返回相应的错误代码。
3.pthread_exit()
终止一个线程的方法:
不要在子线程中调用exit函数,在子线程中调用exit会止整个进程
在启动流例程start_routine中执行return语句,终止子线程。
在子线程的任意位置调用pthread_exit函数,直接终止子线程。
主线程调用pthread_cancel函数,终止指定tid的子线程。
pthread_exit 函数是 POSIX 线程(Pthreads)库中的一个函数,它用于显式地结束当前线程的执行。下面是关于这个函数的详细描述:
函数功能
pthread_exit 函数的功能是终止当前线程的执行,并允许其他线程通过 pthread_join 函数检索该线程的退出状态。当线程调用 pthread_exit 时,它不会立即释放其占用的所有资源;这些资源的清理通常会在其他线程调用 pthread_join 时进行。
参数
pthread_exit 函数接受一个指向 void 的指针作为参数,该指针指向线程的退出状态。这个状态值可以被其他线程通过 pthread_join 函数获取。这个指针可以是 NULL,表示线程没有特定的退出状态;也可以是指向任何类型数据的指针,该数据表示线程的退出状态。
返回值
pthread_exit 函数没有返回值。调用这个函数后,当前线程的执行会立即停止,控制权会返回到调用线程库。注意,这并不意味着整个进程会立即终止;只有当所有非分离(non-detached)线程都结束时,进程才会终止。
示例
下面是一个简单的示例,展示了如何使用 pthread_exit 函数:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h> void *thread_function(void *arg) { printf("Thread is running...\n"); pthread_exit((void *)10); // 线程退出,并设置退出状态为 10
} int main() { pthread_t thread_id; void *exit_status; int ret; ret = pthread_create(&thread_id, NULL, thread_function, NULL); if (ret != 0) { perror("pthread_create"); exit(EXIT_FAILURE); } ret = pthread_join(thread_id, &exit_status); if (ret != 0) { perror("pthread_join"); exit(EXIT_FAILURE); } printf("Thread exited with status: %ld\n", (long long)exit_status); // 打印线程的退出状态 exit(EXIT_SUCCESS);
}
在这个示例中,新创建的线程运行 thread_function 函数,该函数打印一条消息,然后调用 pthread_exit 函数来结束线程的执行,并设置退出状态为 10。主线程通过调用 pthread_join 函数等待新线程结束,并检索其退出状态。最后,主线程打印出线程的退出状态并正常退出。
4.pthread_cancel()
线程被取消,join的时候,退出码是-1 #define PTHREAD_CANCELED ((void *) -1)
。
子线程不要调用pthread_cancel向主线程发送取消请求,因为主线程负责等待所有的子线程退出,取消主线程可能还会影响整个进程。
不要向当前线程的tid发送取消请求,该行为属于未定义行为,可能出现各种意想不到的错误。
pthread_cancel函数是POSIX线程库中的一个重要函数,主要用于向指定的线程发送取消请求。以下是关于pthread_cancel函数的详细解释:
函数功能
pthread_cancel函数的主要功能是向指定的线程发送一个取消请求,请求该线程终止执行。当线程接收到这个取消请求时,它可以选择在合适的时机退出,或者继续执行直到达到某个取消点。
工作原理
当调用pthread_cancel函数时,它并不会立即终止目标线程,而是仅仅提出一个取消请求。目标线程在接收到这个请求后,会继续执行,直到它到达一个取消点(Cancellation Point)。取消点是线程检查是否被取消,并按照请求进行动作的一个位置。如果线程被设置为可取消状态,并且到达了一个取消点,那么它会根据取消请求进行相应的处理,比如退出线程。
此外,线程的取消类型也会影响其响应取消请求的方式。通过pthread_setcanceltype函数,线程可以设置其取消动作的执行时机。有两种取值:PTHREAD_CANCEL_DEFERRED和PTHREAD_CANCEL_ASYNCHRONOUS。前者表示线程在接收到取消请求后会继续执行,直到到达下一个取消点再退出;后者表示线程会立即执行取消动作(即退出)。
参数
pthread_cancel函数的参数是线程的标识符(pthread_t类型),它指定了要取消的线程。
返回值
如果成功发送了取消请求,pthread_cancel函数返回0;否则,返回非零值以表示错误。
与pthread_exit的异同
pthread_exit和pthread_cancel都是用于线程管理的函数,但它们在功能和使用上存在一些差异:
异同点:
目的:pthread_exit是线程主动请求终止,而pthread_cancel是从外部向线程发送取消请求。
控制权:pthread_exit由线程自己调用,线程在决定何时终止时有完全的控制权。而pthread_cancel是由其他线程调用的,被取消的线程在何时被终止方面没有直接的控制权。
取消状态与类型:线程可以设置自己的取消状态和取消类型,以决定是否可以被取消以及何时可以被取消。这些设置会影响pthread_cancel的效果。而pthread_exit则不受这些设置的影响。
退出状态:pthread_exit允许线程返回一个指向某种类型的指针作为退出状态,这可以被其他线程通过pthread_join获取。而pthread_cancel并不直接涉及线程的退出状态;被取消的线程如果没有设置取消处理函数,通常不会返回任何状态。
资源回收:无论是通过pthread_exit还是pthread_cancel终止的线程,最终都需要通过pthread_join来回收其资源,确保系统资源的正确释放。
总结来说,pthread_exit和pthread_cancel都是用于管理线程生命周期的机制,但它们在如何终止线程以及线程在终止过程中的控制权方面有所不同。pthread_exit是线程主动请求终止,并允许线程返回一个退出状态;而pthread_cancel则是从外部请求终止线程,并允许线程通过设置取消状态和类型来控制其可取消性。
取消线程pthread_cancel
取消一个线程,首先这个线程得存在,其次在实际应用中,通常是新线程执行完毕或执行了一段时间,主线程要去取消新线程。如果取消一个不存在的新线程,这样的行为是未定义的。
能不能用新线程取消主线程
能,但是毫无意义,主线程要等待新线程,如果把主线程取消,谁来等待新线程,其次主线程被取消,有可能使得进程出现问题进而影响新线程。
5.一些线程相关的问题
各线程共享进程收到的信号,和信号的处理方法(pending信号集,handler信号处理方法表)
如何理解Linux下OS和线程库共同维护线程?
OS管理线程的调度和内核数据结构⇒ 内核级线程,线程库维护线程的id,栈,属性⇒ 用户级线程
Linux下,__thread会给全局变量带来什么变化?他的工作原理是什么?
在Linux下,__thread是GCC编译器提供的一个关键字,它用于声明线程局部存储(Thread-Local Storage,TLS)的变量。当应用于全局变量时,__thread会给这些变量带来一些特殊的变化和工作原理。
变化:
线程独立性:使用__thread修饰的全局变量不再是传统意义上的全局变量,而是每个线程都拥有其独立副本的变量。这意味着每个线程都可以独立地读写自己的变量副本,而不会干扰其他线程中的相同变量。
避免竞争条件:由于每个线程都有自己的变量副本,因此使用__thread可以有效地避免多线程环境下的竞争条件(race conditions)。竞争条件是由于多个线程同时访问和修改共享数据而可能导致的不可预测的行为。
提高性能:由于线程局部存储的变量访问速度通常比通过同步机制(如互斥锁)访问共享变量要快,因此使用__thread可以提高多线程程序的性能。
工作原理:
__thread关键字告诉编译器为每个线程分配该变量的独立存储空间。当线程创建时,其TLS区域也会被初始化,包括所有__thread修饰的变量。每个线程在访问这些变量时,都会直接访问其TLS区域中的对应副本,而不是全局内存区域。
需要注意的是,__thread变量有一些限制:
它们必须是POD(Plain Old Data)类型,即简单的数据类型,如整数、浮点数和指针,不能是带有自定义构造函数、析构函数或复杂拷贝语义的类类型。
__thread变量通常只能被初始化为常量表达式,因为它们需要在程序启动时被初始化,并且每个线程的副本需要保持一致。
总的来说,__thread关键字提供了一种方便且高效的方式来在Linux多线程环境中管理线程局部存储的变量,从而避免了竞争条件并提高了性能。
在Linux线程内部调用execl会对其他线程和主线程以及整个进程有什么影响?
在Linux中,当一个线程调用execl(或任何其他exec系列函数)时,它实际上会替换当前进程的地址空间。这意味着整个进程,包括其所有线程,都将被新的程序替换。因此,对execl的调用将对其他线程和主线程以及整个进程产生重大影响。
以下是具体的影响:
其他线程:调用execl的线程将不再存在,因为exec系列函数会替换整个进程的地址空间。其他线程也将被终止,因为它们的执行上下文(包括栈和寄存器状态)不再有效。换句话说,所有线程都将停止执行,并且它们的资源将被释放。
主线程:在Linux中,没有真正意义上的“主线程”概念。线程只是进程中的执行流。但是,通常我们所说的“主线程”是指第一个启动的线程,或者是创建其他线程的那个线程。无论如何,当任何线程调用execl时,这个“主线程”也将被终止,因为它也是进程的一部分。
整个进程:execl会替换整个进程的地址空间,这意味着进程的所有资源(包括内存、打开的文件描述符等)都将被新程序接管。原进程的代码、数据和堆栈都将被新程序的代码、数据和堆栈替换。因此,进程的状态将完全改变,并且新程序将从其main函数开始执行。
需要注意的是,execl等exec系列函数不会返回给调用者。一旦调用成功,原进程的映像将被新程序替换,并且新程序开始执行。如果execl返回,那通常意味着出现了错误(例如,文件未找到或权限不足),并且原程序将继续执行(尽管在这种情况下,由于已经调用了exec,程序可能处于不稳定状态)。
总的来说,在Linux线程内部调用execl会对整个进程产生重大影响,包括终止所有线程并替换进程的地址空间。如果需要在不终止其他线程的情况下启动新程序,应该考虑使用fork和exec的组合,或者使用其他进程间通信(IPC)机制。
在Linux线程内部调用fork()会对其他线程和主线程以及整个进程有什么影响?这与在单线程单进程中调用fork()有什么区别?
在Linux线程内部调用fork()时,会对其他线程、主线程以及整个进程产生特定的影响。同时,这种调用与在单线程单进程中调用fork()确实存在一些区别。以下是详细的分析:
在线程内部调用fork()的影响:
子进程状态:当线程内部调用fork()时,子进程只会复制调用fork()的那个线程的上下文,包括其栈、寄存器状态等。其他已经创建的线程不会被子进程继承。这意味着子进程将只拥有一个线程,即复制自调用fork()的那个线程的副本。
锁和同步机制:由fork()创建的子进程会继承父进程的锁(如互斥锁)及其状态。然而,由于子进程可能不清楚从父进程继承而来的锁的具体状态(是加锁还是解锁状态),这可能导致潜在的问题,如死锁。因此,确保在fork()调用后父进程和子进程都拥有清晰的锁状态非常重要。
进程和线程资源:虽然子进程只继承了一个线程,但它仍然继承了父进程的所有其他资源,如内存、文件描述符等。
与在单线程单进程中调用fork()的区别:
线程继承:在单线程单进程中调用fork()时,子进程会复制父进程的全部上下文,包括其唯一的线程。然而,在多线程环境中调用fork()时,只有调用fork()的那个线程的上下文会被复制到子进程,其他线程不会被子进程继承。
同步和通信复杂性:在单线程环境中,由于只有一个执行流,同步和通信的问题相对简单。但在多线程环境中,线程间的同步和通信变得更加复杂,尤其是在调用fork()后,需要特别关注锁和其他同步机制的状态。
性能考虑:多线程程序通常比单线程程序更复杂,因此在多线程程序中调用fork()可能会引入更多的性能和资源管理问题。例如,子进程可能需要重新创建或管理那些未被继承的线程。
总的来说,在Linux线程内部调用fork()会对其他线程、主线程以及整个进程产生特定影响,这与在单线程单进程中调用fork()存在显著区别。因此,在多线程环境中使用fork()时需要特别小心,以确保正确地管理资源和同步机制。
Linux下设置为分离状态的线程发生了除零异常,对于主线程和其他线程和进程有什么影响
在Linux下,一个设置为分离状态的线程如果发生了除零异常,通常会导致该线程自身崩溃。由于线程是进程的执行分支,线程的崩溃可能会触发信号机制,导致整个进程终止。因此,不仅主线程会受到影响,该进程内的所有线程,包括其他非分离状态的线程,都将被终止。
值得注意的是,设置为分离状态的线程在结束时,系统会自动回收其占用的资源,而无需主线程或其他线程进行资源的清理操作。然而,这并不意味着分离状态的线程崩溃不会对系统或进程造成任何影响。实际上,由于整个进程被终止,所有线程的资源都将被释放,这可能导致数据丢失或其他未预期的行为。
为了避免这种情况,程序员在编写多线程程序时应该特别注意异常处理。对于可能导致除零异常的代码段,应该添加适当的检查和处理机制,以防止线程崩溃。此外,合理设计线程的数量和状态,以及合理分配和管理系统资源,也是减少线程崩溃对进程和系统影响的重要措施。
总的来说,Linux下设置为分离状态的线程发生除零异常会对主线程、其他线程以及整个进程造成严重影响,可能导致进程崩溃和资源丢失。因此,程序员在编写多线程程序时需要特别谨慎,确保线程的稳定性和安全性。
Linux下如果线程设置为了分离状态,而主线程提前退出了会发生什么
在Linux下,当线程被设置为分离状态(通过pthread_detach函数或在创建线程时设置PTHREAD_CREATE_DETACHED属性),它的结束处理与非分离线程是不同的。
如果一个线程被设置为分离状态,当它结束时,其所有资源(包括线程ID和栈空间)会自动被系统回收,而不需要其他线程来调用pthread_join来等待它并回收资源。
现在,考虑你描述的场景:一个主线程创建了一个或多个分离线程,然后主线程提前退出了。
主线程退出:当主线程退出时,整个进程(包括主线程和其他所有线程)都会收到一个终止信号。
分离线程的处理:由于这些线程是分离的,所以它们的资源会在它们自己结束时自动被回收。但是,如果主线程退出了,那么整个进程都会终止,这意味着所有还在运行的线程(无论它们是否是分离的)都会被强制终止。因此,这些分离线程可能并不会正常地结束其执行并自动回收资源,而是会被突然终止。
资源回收:尽管分离线程可能没有完全执行其清理操作,但由于整个进程都被终止了,所以操作系统会负责回收该进程的所有资源。
因此,虽然分离线程的设计初衷是为了避免线程同步问题(如pthread_join可能导致的阻塞),但在主线程提前退出的情况下,这些线程可能并不会像预期的那样正常结束。为了确保所有线程都能正常结束并释放资源,可能需要其他机制来协调线程的结束,或者确保主线程在所有其他线程结束后再退出。
- 主线程必须最后退出,他要回收资源
- 如果主线程先于新线程退出,那么所有其他线程都会不正常的退出
- pthread_detach()的应用是有场景的:主线程创建了一个新线程让他去执行任务而不关心他的退出状态,可以让该新线程设置为分离状态,执行完后自动回收资源。
inux下C++调用语言的线程库写线程的代码时底层需要调用linux的线程库pthread,如果这份CPP代码放到windows,底层调用的是不是就成了windows的线程库?
是正确的。在Linux下使用C++编写多线程代码时,通常是通过调用POSIX线程库(pthread库)来实现的。pthread库是Linux和其他UNIX-like系统提供的标准线程库,用于创建、管理和同步线程。
然而,当同样的C++代码被移植到Windows平台时,底层的线程实现会有所不同。Windows有自己的线程库和API,用于管理线程。因此,在Windows下编写多线程C++代码时,通常会使用Windows提供的线程函数和机制,如CreateThread函数、WaitForSingleObject函数以及同步原语(如事件、互斥量、信号量等)。
为了在Linux和Windows之间实现跨平台的多线程编程,开发者通常会使用一些跨平台的线程库,如Boost.Thread、C++11标准库中的线程支持等。这些库提供了统一的接口,使得开发者可以编写一次代码,然后在不同的操作系统上运行而无需修改底层线程调用。
对于C++11及更高版本的标准,它们提供了原生的线程支持,包括std::thread类、互斥量、条件变量等。这些功能在底层会根据不同的操作系统调用相应的线程库。因此,使用C++11或更高版本的线程支持,可以更容易地实现跨平台的多线程编程。
总结来说,C++代码在Linux下使用pthread库编写线程,而在Windows下则使用Windows的线程库。为了实现跨平台编程,可以使用跨平台的线程库或C++标准库中的线程支持。
Windows下的多线程是如何实现的?
在Windows操作系统中,线程的诞生和设计是基于操作系统的核心功能和架构的。与Linux类似,Windows也是通过底层的操作系统支持和API来实现多线程的。不过,Windows和Linux在内部实现线程的方式上可能有所不同,但两者都提供了相似的线程抽象给开发者使用。
在Windows中,线程是由操作系统内核管理的,每个线程都有它自己的栈空间、程序计数器、寄存器集合以及线程上下文信息。当线程被创建时,操作系统会为其分配必要的资源,并将其加入到系统的线程调度队列中。
Windows的线程设计主要基于以下几个核心概念:
线程内核对象:每个线程在Windows内核中都有一个对应的线程内核对象。这个对象包含了线程的状态信息、优先级、安全描述符等。通过操作这个对象,操作系统可以管理线程的生命周期和状态。
线程调度:Windows的线程调度器负责将CPU时间片分配给各个线程,以实现并发执行。调度器会考虑线程的优先级、亲和性以及其他因素来做出决策。
线程同步:Windows提供了多种同步机制,如临界区、互斥量、信号量、事件等,以帮助开发者协调不同线程之间的执行顺序,防止数据竞争和其他并发问题。
线程创建和销毁:Windows API提供了函数(如CreateThread)来创建新线程,并提供了机制来销毁线程(通常通过线程自然结束或调用终止线程的函数)。
在底层实现上,Windows可能会使用不同的技术来模拟或实现线程。例如,它可能使用轻量级进程(类似于Linux中的线程实现),或者采用其他优化技术来减少线程创建和销毁的开销。不过,这些细节通常对开发者是透明的,开发者只需要使用Windows提供的线程API来编写多线程代码。
总的来说,Windows下的线程设计是一个复杂的系统工程,涉及到操作系统的多个方面。它旨在提供一种高效、灵活且易于使用的多线程编程模型,以支持并发执行和资源共享。虽然Windows和Linux在内部实现上可能有所不同,但它们都提供了类似的线程抽象和API,使得开发者可以编写跨平台的多线程代码。
语言级别的多线程接口
在Linux平台下,C++、Java、Python等语言级别的多线程接口,底层也一定封装了pthread线程库。所以在编译时必须在g++命令中指明链接pthread库。
提供语言级别的多线程接口的原因有二:1. 简化操作,方便用户使用。 2. 为了实现语言的跨平台性
6.pthread_detach()
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
pthread_detach函数必须在目标线程尚未被其他线程调用pthread_join函数之前调用。如果目标线程已经被其他线程调用pthread_join函数等待,那么调用pthread_detach函数将会失败。
不能调用pthread_join等待已经分离的线程。若非要调用,等待线程失败,报错:无效参数(tid)。
pthread_detach()参数,返回值,工作原理,函数功能
pthread_detach()是POSIX线程库中的一个函数,用于将一个线程标记为可分离状态,使得该线程在退出时可以自动回收其资源,无需其他线程调用pthread_join()来等待其结束。
参数
pthread_detach()函数的参数是线程的标识符(pthread_t类型),用于指定需要分离的线程。
返回值
函数的返回值是一个整数。如果成功将线程设置为分离状态,则返回0;如果失败,则返回错误号。
工作原理
在Linux中,线程有两种状态:可结合的(joinable)和分离的(detached)。线程默认创建为可结合态。当线程处于可结合态时,即使线程函数返回退出或调用pthread_exit(),也不会自动释放线程所占用的堆栈和线程描述符等资源。这些资源必须由其他线程通过调用pthread_join()来获取线程的退出状态并进行清理。
通过调用pthread_detach()函数,可以将线程的状态设置为分离态。一旦线程被设置为分离态,当该线程结束时,它的资源(包括线程栈等)会被自动回收,无需其他线程调用pthread_join()来进行回收。这使得主线程或其他线程可以继续执行,而不必等待被分离的线程结束。如果在设置为分离状态后,还调用了pthread_join()来等待该线程,那么pthread_join()的返回值就会被设为对应的errno
需要注意的是,一旦线程被标记为可分离状态,就无法再次将其状态改为可连接状态。
函数功能
pthread_detach()函数的主要功能是从状态上实现线程分离。具体来说,它将指定线程的状态设置为分离态,使得线程主动与主控线程断开关系。在线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而是直接自动释放(自己清理掉PCB的残留资源)。因此,该函数常用于网络和多线程服务器等场景,以减少系统资源的占用和提高程序的性能。
此外,使用pthread_detach(pthread_self())可以将当前线程自身设置为分离状态。pthread_self()是一个函数,用于获取当前线程的ID,然后pthread_detach()使用这个ID将当前线程设置为分离状态。这在线程对主程序不再有依赖关系时特别有用,可以减少系统资源的占用。但请注意,分离状态的线程无法再进行pthread_join()操作,因此需要谨慎使用。
7.pthread_self()
8.认识线程标识符:pthread_self()获取线程标识符
观察运行结果中pthread_create获取的线程标识符,并不是我们预想中的LWP的值。
LWP属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度的基本单位,所以需要一个数值来唯一表示该线程。
而pthread_create获取的线程ID(tid)是一个地址,指向一个虚拟内存单元,属于pthread线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程ID是一个唯一标识符,用于区分不同的线程。在多线程程序中,每个线程都有自己的线程ID。
pthread_t类型是一个不透明的数据类型,实际上是一个结构体指针。它通常被用作线程的标识符,用于创建、操作和等待线程
在创建和运行线程时,我们调用的是pthread库函数,并不是Linux系统直接提供的系统调用。
Linux系统不提供线程相关接口,没有专门的线程结构,而是统一提供轻量级进程的接口和内核数据结构,只负责调度执行轻量级进程。也就是说在内核看来,进程和线程没有结构上的区别。
但是各线程仍需要有独属于自己的一份属性和资源,如线程ID,线程退出结果,栈结构等。这些属性和资源是在内核数据结构中无法体现的。所以pthread库除了提供线程相关的操作外,还专门为线程设计了一个线程属性结构,作为内核轻量级进程的补充数据。线程ID(pthread_t类型)其实就是该结构体的指针。
在调用pthread_create创建线程时:一方面,系统会创建轻量级进程的内核数据结构,如task_struct等。另一方面,pthread线程库也会在动态库的虚拟内存中(共享区)创建一个线程属性结构,用于存储该线程的相关属性和资源。其中就包含了该线程的独立栈结构、线程局部存储等。
pthread库是如何做到指定线程的栈结构的呢?pthread_create函数底层封装了系统调用clone,clone用于创建一个新的进程或线程
其中参数fn是指向新进程或线程要执行的函数的指针;参数child_stack是指向新进程或线程的栈空间的指针。pthread_create会将在动态库中创建的栈结构地址,作为child_stack参数传递给clone系统调用。操作系统会将其作为新线程的栈结构。新线程在调度执行时,就会使用动态库中的栈结构了。
主线程使用的是内核级的栈结构(地址空间中的栈区),而各子线程使用的是动态库中的独立栈结构(地址空间中的共享区)。各线程调用栈,执行函数互不影响。
线程的局部存储
进程的全局变量被所有进程共享。
__thread修饰全局变量,使该全局变量被每个线程各自独立拥有,这就是线程的局部存储。
线程的局部存储:
作用域:线程的全局作用域;
存储位置:地址空间共享区 --> pthread_动态库 --> 线程属性结构;
访问权限:被每个线程各自独立拥有。
printf("%lu, %p\n", tid, tid);
输出的是一个很大的值,%p按地址十六进制打印,我们发现 pthread_t tid; 本质是一个地址!
9.POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
要使用这些函数库,要通过引入头文<pthread.h>
链接这些线程函数库时要使用编译器命令的“-lpthread”选项 - 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小 - 进程ID和线程ID
在Linux中,目前的线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。
没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题呢?==> Linux内核引入了线程组的概念。
struct task_struct {
...
pid_t pid;//进程id
pid_t tgid;//线程组id==主线程id==进程id
...
struct task_struct *group_leader;//指向主线程的pcb
...
struct list_head thread_group;//同一个线程组的所有线程的pcb
...
};
多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应。
[root@localhost linux]# ps -eLf |head -1 && ps -eLf |grep a.out |grep -v grep
UID PID PPID LWP C NLWP STIME TTY TIME CMD
root 28543 22937 28543 0 2 15:32 pts/0 00:00:00 ./a.out
root 28543 22937 28544 0 2 15:32 pts/0 00:00:00 ./a.out
LWP: 线程ID,既gettid()系统调用的返回值。
NLWP: 线程组内线程的个数
pid:进程id <= = => 主线程id <= = => 线程组id
Linux提供了gettid系统调用来返回其线程ID,可是glibc并没有将该系统调用封装起来,在开放接口来共程序员使用。
如果确实需要获得线程ID,可以采用如下方法: #include <sys/syscall.h> pid_t tid; tid = syscall(SYS_gettid);
线程标识符
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程标识符,属于NPTL线程库的范畴。线程库的后续操作,就是根据该标识符来操作线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程自身的标识符。pthread_t类型的线程标识符,本质就是一个进程地址空间上的一个地址
void pthread_exit(void *value_ptr);
pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
为什么需要线程等待?
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
创建新的线程不会复用刚才退出线程的地址空间。
int pthread_join(pthread_t thread, void **value_ptr);
- thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
- thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数