一、线程创建
1.1 pthread_create函数
pthread_create()
函数是一个用于创建线程的函数,它属于pthread线程库(POSIX线程库)。
函数原型如下:
#include <pthread.h>
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
:指向线程函数的指针,该函数是线程的入口点,线程将从该函数开始执行。arg
:传递给线程函数的参数,可以是任意类型的指针。
函数返回值:
- 成功创建线程时,返回0。
- 创建线程失败时,返回一个非零的错误码。
需要注意的是,想要调用pthread库函数,必须在g++/gcc命令中指明链接pthread库。
g++ mythread.cc -o mythread -l pthread
测试程序:
void *ThreadRoutine(void *name)
{
int cnt = 3;
while (cnt--)
{
printf("[%d]: %s\n", getpid(), (char *)name);
sleep(1);
}
cnt /= 0; // 除0错误
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void *)"child thread 1");
while (1)
{
printf("[%d]: main thread\n", getpid());
sleep(1);
}
return 0;
}
运行结果:
-
线程的执行顺序由调度器决定。
-
线程出现异常,整个进程随之崩溃。
1.2 线程异常
- 单个线程如果出现除零,野指针等硬件异常或者收到其他退出信号导致线程崩溃,进程也会随之崩溃。
- 线程是进程的执行分支,线程出异常,就是进程出异常,进而触发信号机制,终止进程;进程终止,该进程内的所有线程也就随即退出。
- 我们之前学习的信号是以进程为基本载体的,各线程共享进程收到的信号,和信号的处理方法(pending信号集,handler信号处理方法表)。
1.3 在多线程中进行程序替换
在多线程进程中,如果任何一个线程调用exec
函数进行进程程序替换,操作系统会将新的可执行文件加载到当前进程的地址空间中,并重新初始化进程的线程资源。这意味着原有的线程和线程相关的资源都会被销毁,包括线程栈、线程局部变量、线程的上下文等。
测试程序:
//mythread.cc
void *ThreadRoutine(void *name)
{
cout << "child thread running..." << endl;
sleep(2);
execl("./test", "test", nullptr); // 进程程序替换
return nullptr;
}
int main()
{
cout << "main thread running..." << endl;
sleep(2);
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
while(1) sleep(1);
}
//test.cc
int main()
{
while (1)
{
cout << "hello world!" << endl;
sleep(1);
}
}
运行结果:
二、线程等待
创建子线程后,主线程同样需要等待子线程退出,获取子线程退出结果,然后操作系统回收子线程PCB。
如果主线程不等待子线程,就会引起类似僵尸进程的问题,从而导致内存泄漏。
2.1 pthread_join函数
pthread_join函数是一个用于等待线程终止的函数。
pthread_join函数的语法如下:
#include <pthread.h>
int pthread_join(pthread_t thread, void **status);
参数:
thread
:要等待的线程的线程标识符。pthread_join函数会阻塞等待线程,直到指定的线程终止。
status
:指向存储线程退出结果的位置的指针(二级指针)。一旦线程终止,它的退出结果将存储在由status指针指向的位置。如果不关心线程的退出结果,可以将status参数设置为NULL。注意线程函数的返回值是void*,所以要使用二级指针做输出型参数。
返回值:
- 等待线程成功,返回0。
- 等待线程失败,返回一个非零的错误码。
注意:与进程等待不同,线程等待不需要关心子线程是否异常,因为一旦子线程出现异常,整个进程就会随之崩溃。线程的异常退出信号就是进程的异常退出信号。
测试程序:
void *ThreadRoutine(void *name)
{
int cnt = 3;
while (cnt--)
{
printf("[%d]: %s\n", getpid(), (char *)name);
sleep(1);
}
cout << "child thread quit!" << endl;
// 线程函数返回,线程退出
return (void *)10; //返回字面常量10的地址(只读常量区)
}
int main()
{
printf("[%d]: main thread\n", getpid());
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void *)"child thread 1");
void *ret;
pthread_join(tid, &ret); // 阻塞等待子线程退出
cout << "main thread quit!" << endl;
cout << "child thread return value: " << (long long)ret << endl;
return 0;
}
运行结果:
主线程调用pthread_join阻塞等待子线程,让线程退出具有一定的顺序性,将来可以让主线程进行更多的收尾工作。
2.2 pthread_tryjoin_np函数(拓展)
在C++中,可以使用pthread_tryjoin_np
函数来进行非阻塞等待线程函数。
#include <pthread.h>
int pthread_tryjoin_np(pthread_t thread, void **retval);
pthread_tryjoin_np函数会尝试非阻塞地等待线程函数的结束。
- 如果线程函数已经结束,pthread_tryjoin_np会返回0,表示线程成功结束;
- 如果线程函数尚未结束,pthread_tryjoin_np会返回EBUSY,表示线程尚未结束;
- 如果出现其他错误,pthread_tryjoin_np会返回相应的错误代码。
测试代码:
#include <iostream>
#include <pthread.h>
void *threadFunction(void *arg)
{
// 线程函数的逻辑
sleep(5);
return nullptr;
}
int main()
{
pthread_t thread;
// 创建线程并执行线程函数
pthread_create(&thread, nullptr, threadFunction, nullptr);
int res;
// 尝试非阻塞等待线程函数
while (res = pthread_tryjoin_np(thread, nullptr))
{
if (res == EBUSY)
{
std::cout << "线程尚未结束" << std::endl;
}
else
{
std::cout << "出现错误" << std::endl;
}
sleep(1);
}
std::cout << "线程成功结束" << std::endl;
return 0;
}
运行结果:
2.2 线程入口点函数的参数和返回值
线程入口点函数的参数通过pthread_create函数传入;返回值通过pthread_join接收。
测试程序
void *ThreadRoutine(void *data)
{
printf("[%d]: %s\n", getpid(), "child thread running!");
// 子线程处理堆空间数据
for (int i = 0; i < 10; ++i)
{
((int *)data)[i] = i;
}
printf("[%d]: %s\n", getpid(), "child thread quit!");
// 线程函数返回,线程退出
return (void *)data; // 返回堆空间的指针
}
int main()
{
printf("[%d]: main thread running!\n", getpid());
// 创建一批堆区数据
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 << "after: ";
for (int i = 0; i < 10; ++i)
{
cout << data[i] << " ";
}
cout << endl;
for (int i = 0; i < 10; ++i)
{
cout << ret[i] << " ";
}
cout << endl;
printf("[%d]: main thread quit!\n", getpid());
// data和ret指向的堆空间相同,地址相同
printf("[%d]: child thread return value: %p:%p\n", getpid(), ret, data);
}
运行结果:
线程入口点函数的参数和返回值,不仅仅可以是字面常量的地址,还可以是堆空间的地址。因为各线程共享进程的地址空间,也包括堆空间。所以我们可以通过堆空间将一批数据传入子线程,当然子线程也可以将一批数据传递给主线程。
三、线程终止
终止一个线程的方法有:
- 方法一:在子线程入口点函数中执行return语句,终止子线程。
- 方法二:在子线程的任意位置,任意函数中调用pthread_exit函数,直接终止子线程。
- 方法三:主线程调用pthread_cancel函数,终止指定tid的子线程。
注意:不要在子线程中调用exit函数,在子线程中调用exit会止整个进程。
3.1 pthread_exit
pthread_exit函数是一个线程终止函数,用于终止当前线程的执行并返回一个指定的退出结果。
函数原型如下:
void pthread_exit(void *retval);
参数说明:
- retval:指定线程的退出结果,可以是任意类型的指针。
注意事项:
-
当线程调用pthread_exit函数时,它会立即终止当前线程的执行,并将retval作为线程的退出结果返回给等待该线程的其他线程。
-
如果主线程调用pthread_exit函数,那么整个进程将会终止。
3.2 pthread_cancel
pthread_cancel函数用于向指定的线程发送取消请求,以请求终止该线程的执行。
函数原型如下:
int pthread_cancel(pthread_t thread);
参数说明:
- thread:要取消的线程的标识符。
注意事项:
- 当调用pthread_cancel函数时,会向指定的线程发送一个取消请求。
- 被取消的线程可以选择在合适的时机终止自己的执行,并将
PTHREAD_CANCELED(-1)
作为退出结果返回。 - 子线程不要调用pthread_cancel向主线程发送取消请求,因为主线程负责等待所有的子线程退出,取消主线程可能还会影响整个进程。
- 不要向当前线程的tid发送取消请求,该行为属于未定义行为,可能出现各种意想不到的错误。
测试程序:
void *ThreadRoutine(void *arg)
{
while (true)
{
cout << "child thread running..." << endl;
sleep(1);
}
pthread_exit((void *)12); // 子线程不会执行到这里
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
sleep(3);
pthread_cancel(tid); // 向子线程发送取消请求
void *ret;
pthread_join(tid, &ret);
cout << "cancel child thread, tid: " << tid << " retval: " << (long long)ret << endl;
sleep(2);
return 0;
}
运行结果:
3.3 线程ID
3.3.1 线程ID和LWP
观察运行结果中pthread_create
获取的线程标识符(tid),并不是我们预想中的LWP的值。
- LWP属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度的基本单位,所以需要一个数值来唯一表示该线程。
- 而
pthread_create
获取的线程ID(tid)是一个地址,指向一个虚拟内存单元,属于pthread线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
pthread_self
函数是pthread
库中的一个函数,用于获取当前线程的线程ID(Thread ID)。
函数原型如下:
pthread_t pthread_self(void);
pthread_self
函数没有参数,直接返回当前线程的线程ID,即pthread_t
类型的值。
线程ID是一个唯一标识符,用于区分不同的线程。在多线程程序中,每个线程都有自己的线程ID。通过pthread_self
函数,可以获取当前线程的线程ID,以便进行线程的标识和管理。
pthread_t
类型是一个不透明的数据类型,实际上是一个结构体指针。它通常被用作线程的标识符,用于创建、操作和等待线程。
3.3.2 线程属性结构
在创建和运行线程时,我们调用的是pthread库函数,并不是Linux系统直接提供的系统调用。
Linux系统不提供线程相关接口,没有专门的线程结构,而是统一提供轻量级进程的接口和内核数据结构,只负责调度执行轻量级进程。也就是说在内核看来,进程和线程没有结构上的区别。
但是各线程仍需要有独属于自己的一份属性和资源,如线程ID,线程退出结果,栈结构等。这些属性和资源是在内核数据结构中无法体现的。所以pthread库除了提供线程相关的操作外,还专门为线程设计了一个线程属性结构,作为内核轻量级进程的补充数据。线程ID(pthread_t类型)其实就是该结构体的指针。
在调用pthread_create
创建线程时:一方面,系统会创建轻量级进程的内核数据结构,如task_struct等。另一方面,pthread线程库也会在动态库的虚拟内存中(共享区)创建一个线程属性结构,用于存储该线程的相关属性和资源。其中就包含了该线程的独立栈结构、线程局部存储等。
3.3.3 线程的独立栈结构
pthread库是如何做到指定线程的栈结构的呢?pthread_create
函数底层封装了系统调用clone
,clone
用于创建一个新的进程或线程。其函数原型如下:
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...);
其中参数fn
是指向新进程或线程要执行的函数的指针;参数child_stack
是指向新进程或线程的栈空间的指针。pthread_create
会将在动态库中创建的栈结构地址,作为child_stack参数传递给clone系统调用。操作系统会将其作为新线程的栈结构。新线程在调度执行时,就会使用动态库中的栈结构了。
注意:主线程使用的是内核级的栈结构(地址空间中的栈区),而各子线程使用的是动态库中的独立栈结构(地址空间中的共享区)。各线程调用栈,执行函数互不影响。
3.3.4 线程的局部存储
- 进程的全局变量被所有进程共享。
- __thread修饰全局变量,使该全局变量被每个线程各自独立拥有,这就是线程的局部存储。
- 线程的局部存储:
- 作用域:线程的全局作用域;
- 存储位置:地址空间共享区 --> pthread_动态库 --> 线程属性结构;
- 访问权限:被每个线程各自独立拥有。
测试程序:
//int g_val = 0;
__thread int g_val = 0;
void* ThreadRoutine(void* name)
{
while(true)
{
sleep(1);
cout << (char*)name << pthread_self() << " g_val: " << g_val << " &g_val: " << &g_val << endl;
++g_val;
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void*)"child thread 1");
while(true)
{
cout << "main thread: " << pthread_self() << " g_val: " << g_val << " &g_val: " << &g_val << endl;
sleep(1);
}
}
运行结果:
四、线程分离
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
pthread_detach
函数用于将一个线程标记为可分离的(detached)。可分离的线程在其执行结束后会自动释放其资源,无需其他线程调用pthread_join
函数来等待该线程的结束。
函数原型如下:
int pthread_detach(pthread_t thread);
参数:
thread
:是一个pthread_t
类型的线程标识符,用于指定要分离的线程。
返回值:
- 等待线程成功,返回0。
- 等待线程失败,返回一个非零的错误码。
pthread_detach
函数的作用是将指定的线程标记为可分离的。一旦线程被标记为可分离,它的资源将在线程结束时自动释放,无需其他线程调用pthread_join
函数来等待它的结束。
需要注意的是,pthread_detach
函数必须在目标线程尚未被其他线程调用pthread_join
函数之前调用。如果目标线程已经被其他线程调用pthread_join
函数等待,那么调用pthread_detach
函数将会失败。
被分离的线程出现异常,仍然会影响到整个进程,使整个进程崩溃。
测试程序:
void *ThreadRoutine(void *arg)
{
pthread_detach(pthread_self()); // 线程分离
cout << "child thread running..." << endl;
sleep(2);
pthread_exit((void*)12);
}
int main()
{
cout << "main thread running..." << endl;
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
sleep(1);
int ret = pthread_join(tid, nullptr);
cout << "ret = " << ret << ", strerrno: " << strerror(ret) << endl;
}
运行结果:
等待线程失败,报错:无效参数(tid)。不能调用pthread_join等待已经分离的线程。
五、语言级别的多线程接口
- 在Linux平台下,C++、Java、Python等语言级别的多线程接口,底层也一定封装了pthread线程库。所以在编译时必须在g++命令中指明链接pthread库。
- 提供语言级别的多线程接口的原因有二:1. 简化操作,方便用户使用。 2. 为了实现语言的跨平台性