目录
一、使用线程
1、pthread_create创建线程
2、pthread_join等待线程
主线程获取新线程退出结果
获取新线程退出返回的数组
3、线程异常导致进程终止
4、pthread_exit
5、pthread_cancel
6、主线程可以取消新线程,新线程可以取消主线程吗
二、如何管理线程
pthread_self函数:
三、多线程共享进程资源
四、分离线程
一、使用线程
1、pthread_create
创建线程
在Linux环境下,POSIX线程库(Pthreads)为多线程编程提供了一系列强大的工具函数,这些函数均以前缀“pthread_”开始。为了在程序中使用这些函数,开发者需要包含头文件 <pthread.h>
,并在编译阶段通过 -lpthread
参数链接线程库。
关于线程的创建,POSIX线程库提供了一个关键函数 pthread_create()
,用于生成一个新的执行线程。函数原型如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
-
thread
:这是一个指向pthread_t
类型变量的指针,用于存储新创建线程的唯一标识符。在函数成功执行后,新线程的ID会被填入这个指针指向的内存位置。 -
attr
:这是一个指向pthread_attr_t
结构体的指针,用于指定线程的属性,如堆栈大小、调度策略等。如果传入NULL
,则表示线程使用默认属性创建。 -
start_routine
:这是一个指向线程入口函数的指针,当新线程开始执行时,会先调用这个函数。该函数必须接受一个指向void
的指针作为参数,并返回void*
类型的结果。 -
arg
:这是一个通用指针,它会被作为参数传递给start_routine
函数,这样开发者可以在启动线程时传递必要的数据给新线程。
函数的返回值:如果成功创建线程,pthread_create()
会返回零;如果创建失败,则会返回一个非零的错误码。不同于许多传统的POSIX函数,pthread_create()
不会修改全局的 errno
变量来报告错误,而是直接通过返回值反映错误状态。
虽然如此,Pthreads库仍然在每个线程内部维护了自己的 errno
变量副本,以便在使用依赖于 errno
的代码时能够正常工作。但在实际编程实践中,为了优化性能并确保准确性,推荐直接通过检查 pthread_create()
函数的返回值来判断是否成功创建线程,而不是通过读取线程内部的 errno
变量。
-lpthread
: 是链接线程库的选项,表示在编译时链接POSIX线程库,以便支持多线程编程。
mytest:test.ccg++ -o mytest test.cc -lpthread
.PHONY: clean
clean:rm -f mytest
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <string>
using namespace std;void *threadRoutine(void *args)
{while (true){cout << "新线程:" << (char *)args << "running" << endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");while (true){cout << "主线程running" << endl;sleep(1);}return 0;
}
这段C++代码创建了一个简单的多线程程序,它展示了如何使用POSIX线程库(pthread)在Linux环境下创建并运行一个子线程。下面详细解析一下代码的工作原理:
-
首先,包含了必要的头文件:
<iostream>
用于C++的标准输入输出功能。<pthread.h>
是POSIX线程库头文件,提供创建和管理线程所需的函数原型。<unistd.h>
提供了sleep()
函数,用于让线程休眠指定秒数。<stdio.h>
提供了C风格的输入输出函数,这里没有直接使用,但通常包含此头文件以备不时之需。
-
定义了一个名为
threadRoutine
的函数,该函数接受一个指向void
类型的指针作为参数,返回值也是一个指向void
类型的指针。这是线程执行体,线程运行时会执行这个函数的内容。在这个例子中,函数会无限循环打印一条信息,指出这是一个新的线程在运行,并在每次打印后让线程休眠1秒。 -
main()
函数是程序的入口点,它做了以下几件事:- 定义了一个
pthread_t
类型的变量tid
,用来存储线程ID。 - 使用
pthread_create()
函数创建一个新线程,传入四个参数:- 第一个参数是线程ID的指针,用于接收新创建线程的ID。
- 第二个参数是线程属性指针,这里设置为
nullptr
表示使用默认属性。 - 第三个参数是线程执行函数的指针,指向
threadRoutine
函数。 - 第四个参数是要传递给新线程函数的参数,这里是字符串常量"thread 1"的地址。
- 定义了一个
-
创建完新线程后,主线程也开始无限循环,不断打印"主线程running",并在每次打印后同样休眠1秒。
-
当程序运行时,你会看到终端交替打印出主线程和新线程的消息,这是因为两个线程都在独立地并发执行。操作系统会在两个线程之间进行上下文切换,看起来就像是两个线程在轮流执行。
hbr@VM-16-9-centos thread]$ ./mytest 主线程running新线程:thread 1running主线程running 新线程:thread 1running 主线程running 新线程:thread 1running 主线程running 新线程:thread 1running 主线程running 新线程:thread 1running 主线程running 新线程:thread 1running
注意:在这个示例中,因为没有显式地调用pthread_join()
函数去等待子线程结束,所以主线程和子线程都会一直运行下去,除非手动停止程序。如果想要在主线程结束前等待子线程完成,应该在适当的地方调用pthread_join(tid, nullptr)
来同步主线程和子线程的执行。
查看进程和线程信息
在Linux系统中,ps -aL
命令是用来查看所有进程及其所含的线程信息。这条命令的输出可以帮助我们理解上述多线程程序的运行状态。
[hbr@VM-16-9-centos thread]$ ps -aL | head -1 && ps -aL | grep mytestPID LWP TTY TIME CMD
10238 10238 pts/0 00:00:00 mytest
10238 10239 pts/0 00:00:00 mytest
输出的第一行(head -1
)通常显示列标题,表示每列的信息含义:
PID
:进程IDLWP
:轻量级进程ID,也被称为线程IDTTY
:终端设备关联的名称TIME
:该进程或线程已经消耗的CPU时间CMD
:命令名或命令行参数
接下来的两行(grep mytest
筛选出与mytest
程序相关的行)显示了你的多线程程序mytest
的详细信息:
- 第一行
10238 10238 pts/0 00:00:00 mytest
表明进程ID(PID)为10238的进程是你的mytest
程序,同时这个进程的主线程ID(LWP)也是10238,它在pts/0终端运行,自启动以来还未消耗任何CPU时间(00:00:00)。 - 第二行
10238 10239 pts/0 00:00:00 mytest
表示的是同一个进程(PID仍为10238)内的第二个线程,其线程ID(LWP)为10239,同样在pts/0终端运行,且目前还未消耗CPU时间。
为子线程添加除零操作会导致主线程也终止。
在新的代码中,子线程有一个会导致运行时错误的操作——整数除以零(a /= 0;)。在大多数系统中,这样的操作会产生一个运行时异常,具体来说,在C++中这样的行为通常会触发“除以零”错误(floating point exception或integer division by zero error),这会导致整个程序终止,包括主线程。
void *threadRoutine(void *args){while (true){cout << "新线程:" << (char *)args << "running" << endl;sleep(1);int a = 100;a /= 0;}}[hbr@VM-16-9-centos thread]$ ./mytest
主线程running
新线程:thread 1running
主线程running
Floating point exception
- 在C++中,特别是使用POSIX线程(pthreads)的情况下,如果线程由于未处理的信号(如SIGFPE,即浮点异常信号)而终止,且没有采取额外的同步措施来确保其他线程在这种情况下继续执行,那么整个进程(包括主线程)都会因此而结束。
- 为了防止这种情况导致整个程序终止,我们可以考虑在适当的地方添加信号处理函数来捕获并处理这类运行时错误,而不是让它们默认地终止进程。然而,在实际编程中,应当尽量避免产生这类运行时错误,因为它们通常是不可恢复的逻辑错误。
2、pthread_join等待线程
pthread_join()
是POSIX线程库中的一个函数,用于等待指定的线程终止,并获取该线程的退出状态。这个函数在多线程编程中扮演了同步角色,确保主线程在子线程结束后再继续执行或者获取子线程的退出信息。
函数原型如下:
int pthread_join(pthread_t thread, void **retval);
参数说明:
-
pthread_t thread
:你希望等待的那个线程的标识符,通常是由pthread_create()
函数创建线程时返回的。 -
void **retval
:这是一个可选参数,用于接收线程的退出状态。如果该参数非空,则线程结束时,其退出状态(通过调用pthread_exit()
传递的值或默认的NULL)会被复制到*retval
指向的地址。如果不需要获取线程的退出状态,可以传递NULL。
使用pthread_join()
的主要目的是确保在主线程继续执行之前,已经创建的子线程已完成其任务。如果不调用pthread_join()
,那么当主线程结束时,未结束的子线程可能会被强制终止(取决于具体的系统和线程属性),而且无法获取子线程的退出状态。
示例:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void *threadRoutine(void *args)
{int i = 0;while (true){cout << "新线程: " << (char *)args << " running" << endl;sleep(1);if (i++ == 10)break;}
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");pthread_join(tid, nullptr);cout << "main thread wait done, main quit" << endl;return 0;
}
threadRoutine()
函数是定义的线程执行体:
- 接受一个
void*
类型的参数,这个参数在线程创建时传入,此处转换为(char*)
类型并作为输出的一部分。 - 在循环中,线程每秒输出一条包含传入参数的消息,表明线程正在运行。
- 当循环计数器
i
达到10时,线程会跳出循环并自然结束。
main()
函数中:
- 定义了一个
pthread_t
类型的变量tid
,用于保存新创建线程的标识符。 - 调用
pthread_create()
函数创建一个新的线程。传入参数分别为指向线程标识符tid
的指针、线程属性(这里设置为nullptr
表示使用默认属性)、线程运行函数(即threadRoutine()
)以及传递给线程函数的参数(这里是字符串"thread1"
的地址)。 - 主线程接着调用
pthread_join()
函数,阻塞等待之前创建的线程执行完毕。这里的第二个参数同样是nullptr
,因为主线程并不关心线程结束时的返回值。 - 线程结束后,主线程打印出提示信息 "main thread wait done, main quit" 并返回0,程序结束。
由于程序创建了一个额外的线程,所以在监控输出中看到的是主进程ID(PID)为 30092
的进程有两个LWP ID,一个是主线程LWP ID 30092
,另一个是新创建的线程LWP ID 30093
。随着 sleep(1)
的执行,每次循环间隔一秒,所以监控窗口中每隔一秒就会刷新一次状态,显示出主线程和子线程都在运行的状态。当子线程执行完毕之后,pthread_join()
解除阻塞,主线程继续执行并最终退出整个程序。
主线程获取新线程退出结果
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void *threadRoutine(void *args)
{int i = 0;while (true){cout << "新线程: " << (char *)args << " running" << endl;sleep(1);if (i++ == 5)break;}cout << "new thread quit" << endl;return (void *)10;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");void *ret = nullptr;pthread_join(tid, &ret);cout << "main thread wait done, main quit: new thread quit: "<< (long long)ret << endl;return 0;
}
-
调用
pthread_join(tid, &ret)
来等待线程tid
完成其任务。当pthread_join()
返回时,线程已经终止,&ret
参数将接收到线程函数threadRoutine
的返回值。 -
最后,主线程输出一条消息,显示主线程已等待完毕,并打印出线程函数返回的值(转换为
long long
类型以便输出)。
[hbr@VM-16-9-centos thread]$ ./mytest
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
new thread quit
main thread wait done, main quit: new thread quit: 10
[hbr@VM-16-9-centos thread]$
获取新线程退出返回的数组
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void *threadRoutine(void *args)
{int i = 0;int *data = new int[5];while (true){cout << "新线程: " << (char *)args << " running" << endl;sleep(1);data[i] = i;if (i++ == 5)break;}cout << "new thread quit" << endl;return (void *)data;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");int *ret = nullptr;pthread_join(tid, (void **)&ret);cout << "main thread wait done, main quit: new thread quit: " << endl;for (int i = 0; i < 5; i++){cout << ret[i] << " ";}cout << endl;return 0;
}
首先看threadRoutine
函数:
- 在线程函数
threadRoutine
内部,首先分配了一块动态内存空间用于存储一个大小为5的整型数组data
。 - 然后进入一个无限循环,每秒打印一次提示信息,并将数组
data
的第i
个元素赋值为i
,直到i
增加到5为止,此时跳出循环。 - 循环结束后,线程函数返回一个指向
data
数组的指针,转换为void *
类型。
现在来看main
函数:
- 主线程通过
pthread_create
函数创建了一个新的线程,并将threadRoutine
函数作为新线程的入口点,同时传递一个指向字符串"thread1"的指针作为参数。 - 主线程调用
pthread_join
函数来等待新线程执行完毕。这里的关键是pthread_join
的第二个参数,它是一个指向void *
类型的指针的指针,用于接收线程结束时返回的值。因此,传入的是(void **)&ret
,这样ret
就会被赋值为线程函数threadRoutine
的返回值,也就是指向data
数组的指针。 - 主线程等待完成后,输出提示一行信息。
- 接下来,主线程尝试通过
ret
指针访问数组data
的内容。由于ret
实际上是threadRoutine
返回的data
数组的指针,因此可以通过ret[i]
的方式访问数组中的元素,并将其输出。
[hbr@VM-16-9-centos thread]$ ./mytest
主进程获取新进程退出结果
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
new thread quit
main thread wait done, main quit: new thread quit:
0 1 2 3 4
[hbr@VM-16-9-centos thread]$
3、线程异常导致进程终止
#include <iostream>
#include <pthread.h>
#include <unistd.h>
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++ == 5)break;int a = 10;a /= 0;}cout << "new thread quit" << endl;return (void *)data;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");int *ret = nullptr;pthread_join(tid, (void **)&ret);cout << "main thread wait done, main quit: new thread quit: " << (long long)ret << endl;for (int i = 0; i < 5; i++){cout << ret[i] << " ";}cout << endl;return 0;
}
[hbr@VM-16-9-centos thread]$ ./mytest
新线程: thread1 running
Floating point exception
[hbr@VM-16-9-centos thread]$
-
线程调度: 在操作系统层面,线程的启动并不意味着立即执行,而是由线程调度器决定何时切换到该线程。在上述中,
pthread_create
函数创建了一个新的线程,但它何时开始执行(即首次获取CPU时间片)取决于操作系统的线程调度策略。实际上,新线程可能在主线程之后执行,也可能在主线程之前或者同时执行,具体顺序不确定。 -
线程异常与进程退出: 在
threadRoutine
函数中,有一行int a = 10; a /= 0;
,试图执行除以零的操作,这会导致浮点异常(在某些系统上表现为SIGFPE信号)。在单线程程序中,这样的异常通常会导致程序终止。在多线程环境中,如果某个线程因异常而终止,除非异常得到妥善处理(例如通过设置线程的异常处理器pthread_setsockopt
配合PTHREAD_CANCEL_ASYNCHRONOUS
或sigaction
等方法),否则操作系统可能会终止整个进程,因为线程是进程内的执行单元,一个线程的致命错误可能导致整个进程不可恢复。 -
线程的输入和返回值问题: 该代码通过
pthread_create
的最后一个参数传递了一个指向字符串"thread1"的指针作为线程的输入参数。线程函数threadRoutine
通过强制类型转换接收并打印这个参数。线程函数最后返回一个指向int
数组的指针,主线程通过pthread_join
获取这个返回值,并将其转换回int *
类型,以便访问数组内容。 -
线程异常退出的理解: 由于上述除以零操作触发了浮点异常,
threadRoutine
线程没有机会执行到最后的cout
语句输出"new thread quit",也没有机会正常返回data
数组的指针。因此,在实际运行中,你只看到了"新线程: thread1 running"的消息,紧接着出现了“Floating point exception”错误,表明线程在尝试除以零时立刻终止。主线程由于等待pthread_join
返回而被阻塞,但由于线程异常退出,pthread_join
最终无法正确返回线程的返回值,程序也就提前结束了。
理想情况下,应该在线程函数内部捕获此类异常,或者为线程设置适当的异常处理器,以确保线程异常时能做出合适的响应(如记录错误信息、清理资源等),而不是让整个进程崩溃。在C++/POSIX线程模型中,则需要借助信号处理或其他机制来捕获和处理这类线程异常。
4、pthread_exit
在新线程的while
循环结束后调用exit(10)
退出线程,实际上并不能真正意义上仅退出当前线程,因为exit
函数会结束整个进程,而非单个线程。在多线程程序中,exit
会导致进程中所有线程全部停止运行,包括主线程。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
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++ == 5)break;}exit(10);cout << "new thread quit" << endl;return (void *)data;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");int *ret = nullptr;pthread_join(tid, (void **)&ret);cout << "main thread wait done, main quit: new thread quit " << endl;for (int i = 0; i < 5; i++){cout << ret[i] << " ";}cout << endl;return 0;
}
一旦新线程调用了exit(10)
,整个进程将立即终止,不会执行到主线程的pthread_join
和后续输出data
数组内容的部分。主线程也无法有机会得知新线程的退出状态,因为进程已经结束了。在多线程编程中,我们通常不希望使用exit
来结束线程,而是应该使用线程特有的终止机制,例如在POSIX线程中,可以使用pthread_exit
函数来终止线程,而不会影响其他仍在运行的线程。
[hbr@VM-16-9-centos thread]$ ./mytest
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
[hbr@VM-16-9-centos thread]$
pthread_exit()
是POSIX线程接口中的一个函数,用于终止当前线程的执行,并可以选择传递一个退出状态给其他线程。函数原型如下:
void pthread_exit(void *retval);
参数说明:
void *retval
:一个指向返回值的指针。这个参数是可选的,它可以是一个任意类型数据的地址,用来向其他线程返回一个特定的退出状态。当其他线程调用pthread_join()
等待这个线程结束时,可以通过pthread_join()
的第二个参数获取到这个退出状态。
使用pthread_exit()
的主要场景包括:
- 当线程完成其预设任务后,主动退出,释放系统资源,不再参与执行。
- 如果线程在执行过程中遇到了无法恢复的错误或者满足了某种退出条件,可以通过调用
pthread_exit()
来立即终止线程。
举例来说,在一个多线程程序中,当某个工作线程完成了其计算任务后,可以通过调用 pthread_exit(NULL)
来告知系统该线程已结束。主线程或者其他关心该线程状态的线程可以调用 pthread_join()
函数来等待该线程的退出,并检查其退出状态。
需要注意的是,一旦线程调用了 pthread_exit()
,该线程所拥有的所有资源(除了那些被显式加入到分离(detached)线程中的资源外)将不再可用,直到有其他线程通过 pthread_join()
成功地与其同步。此外,线程在退出后,其栈上的局部变量和自动对象将被销毁,不过通过 pthread_exit()
设置的返回值会保持有效,直到被其他线程取走。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
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++ == 5)break;}pthread_exit((void *)11);//exit(10);cout << "new thread quit" << endl;return (void *)data;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");int *ret = nullptr;pthread_join(tid, (void **)&ret);cout << "main thread wait done, main quit: new thread quit "<< (long long)ret << endl;cout << endl;return 0;
}
[hbr@VM-16-9-centos thread]$ ./mytest
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
main thread wait done, main quit: new thread quit 11
5、pthread_cancel
pthread_cancel()
是POSIX线程API中的一个函数,用于取消(terminate)一个活动的线程。函数原型如下:
int pthread_cancel(pthread_t thread);
参数说明:
pthread_t thread
:代表你想取消的线程的ID,这个ID通常是在调用pthread_create()
创建线程时得到的。
返回值:
- 在POSIX线程(pthreads)编程中,当一个线程被显式取消(通过调用
pthread_cancel
函数)时,被取消线程的状态会被标记为已取消,而pthread_join
函数在等待该线程时,如果线程是因为被取消而结束的,那么pthread_join
将返回PTHREAD_CANCELED
,这是一个预定义的宏,其值为((void *) -1)
。
函数功能:
-
调用
pthread_cancel()
会发送一个取消请求到指定的线程,目标线程在接收到取消请求后,可以通过响应特定的取消点(cancellation points)来终止其执行。取消点通常是那些可能导致线程阻塞并可能被取消的函数调用,例如pthread_cond_wait()
,pthread_join()
,sem_wait()
等。 -
默认情况下,线程在接收到取消请求后并不会立即停止,而是会在下一个取消点处检测到请求并进行清理操作,然后退出。这种行为可以通过设置线程的取消类型(cancelability type)为异步取消(asynchronous cancellation)来改变,这样线程在收到取消请求后会立即停止,但这种方式可能引发资源泄漏或数据不一致的问题,因此一般较少采用。
-
另外,线程可以通过调用
pthread_setcancelstate()
和pthread_setcanceltype()
来管理和控制其取消状态和取消类型,还可以通过在关键区域设置取消屏蔽(cancellation mask)暂时阻止取消请求的生效。 -
被取消的线程最终会以特殊的取消状态退出,调用
pthread_join()
可以获取到这个状态。同时,线程可以通过在代码中设置清理函数(cleanup handler)来确保在取消发生时进行必要的资源释放或清理工作。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void *threadRoutine(void *args)
{int i = 0;int *data = new int[10];while (true){cout << "新线程: " << (char *)args << " running" << endl;sleep(1);}cout << "new thread quit" << endl;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");int count = 0;while (true){cout << "main线程:"<< " running... " << endl;sleep(1);count++;if (count >= 5)break;}pthread_cancel(tid);cout << "pthread cancel:" << tid << endl;int *ret = nullptr;pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出cout << "main thread wait done ... main quit ...: new thead quit : "<< (long long)ret << "\n ";sleep(5);return 0;
}
6、主线程可以取消新线程,新线程可以取消主线程吗
在POSIX线程模型中,任何线程都可以发起对其他线程的取消请求,包括主线程可以取消新创建的子线程,理论上新线程也可以尝试取消主线程。但是,是否允许新线程取消主线程以及取消操作的实际效果,通常需要根据程序设计和实际需求来考虑:
-
主线程取消子线程:主线程可以通过调用
pthread_cancel()
函数来取消任何一个子线程,包括新创建的线程。被取消的线程将在遇到取消点时被终止(除非设置了特定的取消类型和状态)。 -
子线程取消主线程:在技术层面上,子线程同样可以调用
pthread_cancel()
尝试取消主线程,但这在实际应用中往往不是一个好的做法。主线程通常是负责程序总体控制和资源管理的,如果主线程被意外取消,可能会导致程序无法正确清理资源、关闭文件、释放内存等,甚至引发不可预见的错误或崩溃。通常建议主线程自己管理其生命周期,而不是由子线程取消。
另外,主线程取消与否还取决于程序的逻辑设计,如果主线程等待所有子线程完成任务(通常通过pthread_join()
函数),那么主线程在子线程完成前不应该提前结束;反之,如果主线程需要在任何时刻能够取消子线程,应当在设计时考虑到线程取消的处理和资源清理问题。在实际编程中,应尽量避免线程间的相互取消,尤其主线程的取消,以免产生难以调试和预料的结果。
二、如何管理线程
在现代操作系统中,尤其是基于POSIX线程库(如pthreads)的多线程编程环境中,线程的创建和管理涉及到内存中多个数据结构的维护。每个线程除了有自己的栈空间之外,还会有一系列与线程相关的属性和控制块(Thread Control Block, TCB),其中就包括线程ID(TID)。
主线程(或者说初始线程)在创建时由操作系统为其分配内核级栈结构,供其执行环境使用。当创建新的用户级线程时,新线程会复用父线程的地址空间,并且会拥有独立的栈结构,以保证多线程执行流之间的数据隔离。这里的“共享区”通常指的是进程的所有线程共享的地址空间,而每个线程各自的栈空间则位于各自独立的内存区域,互不影响。
在多执行流环境下,应用程序无法直接操作内核栈,而是通过线程库(如POSIX线程库pthreads)提供的接口创建和管理线程。线程库会在用户态与内核态之间进行切换,协调线程的创建、调度、同步等相关操作,确保各个线程能在共享的虚拟地址空间中独立、并发地执行,而不至于相互冲突。通过线程库创建的线程,其栈结构是由库本身在进程的地址空间内合理安排的,从而避免了对单执行流(即主线程)使用的内核栈区的干扰。
pthread_self函数:
pthread_self
函数用于获取当前执行线程的线程ID,即pthread_t
类型的数据。在NPTL下,这个函数返回的实际上是当前线程在进程地址空间中对应的线程控制块(TCB)的地址。通过这个地址,线程库可以访问和操作当前线程的各种属性和状态。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void *threadRoutine(void *args)
{int i = 0;int *data = new int[10];while (true){cout << "新线程: " << (char *)args << " running" << endl;sleep(1);}cout << "new thread quit" << endl;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");printf("%lu, %p\n", tid, tid);int count = 0;while (true){cout << "main线程:"<< " running... main tid: " << pthread_self() << endl;sleep(1);count++;if (count >= 5)break;}pthread_cancel(tid);cout << "pthread cancel:" << tid << endl;int *ret = nullptr;pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出cout << "main thread wait done ... main quit ...: new thead quit : "<< (long long)ret << "\n ";sleep(5);return 0;
}
[hbr@VM-16-9-centos thread]$ ./mytest
139972892075776, 0x7f4dfa82a700
main线程: running... main tid: 139972909463360
新线程: thread1 running
main线程: running... main tid: 139972909463360
新线程: thread1 running
main线程: running... main tid: 139972909463360
新线程: thread1 running
main线程: running...main tid: 139972909463360
新线程: thread1 running
main线程: running... main tid: 139972909463360
新线程: thread1 running
pthread cancel:139972892075776
main thread wait done ... main quit ...: new thead quit : -1[hbr@VM-16-9-centos thread]$
操作系统层面的线程ID(TID): 在操作系统层面,线程ID(TID)是一个数值,用于在系统范围内唯一标识一个线程。在多线程环境中,操作系统需要一个机制来区分不同的线程,以便进行调度和管理。操作系统内核为每个线程分配一个TID,它是进程调度的基本单位,当线程创建时,操作系统会为该线程生成一个TID,这个ID在系统中是唯一的,即使在不同的进程中也有唯一性。
NPTL线程库层面的线程ID: 在Linux中,使用Native POSIX Thread Library (NPTL) 实现的线程库中,线程ID的表示形式有所不同。pthread_create
函数在创建线程时,会返回一个pthread_t
类型的线程ID,这个ID在NPTL实现中实际上是进程地址空间内的一个地址。pthread_t
类型的线程ID不同于操作系统内核直接分配的TID,它是由线程库自身管理的一个数据结构的地址,这个数据结构中包含了线程的状态、调度信息以及其他必要的元数据。
- 总结来说,操作系统层面的线程ID(TID)是一个纯粹的数字标识符,而NPTL线程库中的
pthread_t
类型的线程ID则是一个地址,它指向存储线程相关信息的数据结构。这两者虽然都用于标识线程,但层次和用途不同,前者是系统级别的标识,后者是线程库内部使用的标识。在实际编程中,我们通常通过线程库提供的接口来管理和操作线程,如创建、销毁线程,同步线程等操作。
三、多线程共享进程资源
多线程共享进程资源是多线程编程中的常见场景。在同一个进程中,所有线程都共享同一块内存空间,这意味着它们可以访问相同的全局变量、静态变量、堆区分配的内存以及打开的文件描述符等资源。以下是一个简单的示例来阐述这一概念:
#include <iostream>
#include <pthread.h>// 共享资源:全局变量
int global_count = 0;// 线程函数,负责更新全局变量
void* increment_count(void* unused) {for(int i = 0; i < 1000000; ++i) {// 不使用锁的情况下,多个线程同时修改global_count可能导致数据竞争++global_count;}return nullptr;
}int main() {pthread_t threads[2];// 创建两个线程,它们都会访问和修改同一个全局变量global_countfor(int i = 0; i < 2; ++i) {if(pthread_create(&threads[i], NULL, increment_count, NULL)) {std::cerr << "Error creating thread." << std::endl;return 1;}}// 等待两个线程都结束for(int i = 0; i < 2; ++i) {pthread_join(threads[i], NULL);}// 输出最终的global_count值,理论上应该是2000000,但如果存在数据竞争,则可能不是这个值std::cout << "Final count: " << global_count << std::endl;return 0;
}
在这个例子中,我们创建了两个线程,它们各自执行increment_count
函数,该函数会递增全局变量global_count
一千万次。由于全局变量是进程范围内的共享资源,所以两个线程都可以访问和修改它。然而,由于没有采取任何同步措施(例如互斥锁pthread_mutex_t
),当两个线程同时尝试修改global_count
时,可能出现数据竞争(race condition),导致最终的计数结果不准确。
为了确保线程安全地共享和修改资源,实际编程中通常会使用各种同步机制,如互斥锁、条件变量、信号量等,以防止多个线程同时访问和修改同一资源引发的问题。
四、分离线程
在多线程编程中,当创建一个线程后,默认情况下它是可加入(joinable)的。
- 这意味着当线程结束执行时,它的资源(如栈空间和其他相关的内核资源)并不会立即释放,而是保持挂起状态,直到有其他线程通过调用
pthread_join
函数与其进行连接(join)。 - 若不进行
pthread_join
操作,这些资源将得不到释放,久而久之,特别是在程序长时间运行且频繁创建销毁线程的情况下,可能会累积大量的资源泄露,对系统性能产生负面影响。
然而,在某些情况下,开发者可能并不关心线程的执行结果(即线程的返回值),只需要线程独立运行并在结束后自行释放资源。这时,就可以使用pthread_detach
函数来对线程进行分离(detachment)。分离线程意味着系统在该线程终止时,会自动回收其占用的所有资源,无需其他线程执行pthread_join
操作。
有两种方式可以分离线程:
-
其他线程调用
pthread_detach(pthread_t thread)
函数来分离目标线程,其中pthread_t thread
是要分离的线程ID。 -
线程自身可以在运行时调用
pthread_detach(pthread_self())
函数来自我分离。pthread_self()
函数返回当前线程的ID,调用pthread_detach(pthread_self())
则表示当前线程在执行完自身逻辑后,将自动释放资源。
需要注意的是,一个线程不能同时处于可加入和分离两种状态。一旦线程被分离,就不能再对其执行pthread_join
操作。也就是说,线程要么是可加入的(等待其他线程通过pthread_join
来回收资源),要么是分离的(线程终止后系统自动回收资源)。一旦线程被分离,其终止时就不会有任何返回值可供其他线程获取。
线程自身可以在运行时调用的情况:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>// 定义线程执行函数
void *thread_run(void *arg)
{// 在线程开始执行时立即执行分离操作pthread_detach(pthread_self());// 输出传递给线程的参数printf("%s\n", static_cast<char*>(arg));// 线程执行结束,返回NULL(此处无实际意义,因为在detach状态下,线程的返回值不会被保留)return NULL;
}int main()
{pthread_t tid;// 创建新线程if (pthread_create(&tid, NULL, thread_run, const_cast<char*>("thread1 run...")) != 0) {printf("Failed to create the thread.\n");return EXIT_FAILURE;}// 让新创建的线程有机会先执行并完成分离操作sleep(1);// 由于线程已经被detach,此处调用pthread_join不是必须的,也不期望成功// 但为了展示逻辑,依然尝试join,实际运行时会返回错误int join_result = pthread_join(tid, NULL);// 根据pthread_join的结果输出相应信息if (join_result == 0) {printf("The call to pthread_join unexpectedly succeeded.\n");// 此处假设join成功是意外情况,返回成功代码(但这在detach线程上不应发生)return EXIT_SUCCESS;} else {printf("pthread_join failed as expected due to detached thread.\n");// 实际上,对于detach线程,失败才是预期结果,但仍返回0代表程序本身执行无误return EXIT_SUCCESS;}
}
注意,上面的代码中pthread_join
在分离线程上调用并不会成功,因为我们已经通过pthread_detach
让线程变为分离状态,故不应该尝试去join它。在实际情况中,如果你知道线程已被detach,就不必调用pthread_join
。不过,上述代码是为了说明detach线程的概念和行为而设计的。在实际开发中,应在分离线程后跳过join步骤。