目录
一、使用线程
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.cc
g++ -o mytest test.cc -g -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 mytest
PID 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、为什么需要线程等待?
1. 资源共享
在多线程程序中,多个线程可能会共享相同的资源,如内存中的数据结构。如果没有适当的同步机制,可能会导致数据竞争(race conditions),即多个线程同时访问和修改同一个资源,从而导致不可预测的结果或程序崩溃。线程等待可以帮助确保在某个线程访问或修改资源期间,其他线程不会干扰这一过程。
2. 任务依赖
有些线程的任务可能依赖于其他线程的完成情况。例如,一个线程可能负责计算某个结果,而另一个线程需要等待这个结果出来后才能继续执行。在这种情况下,线程等待是必要的,以确保任务的正确执行顺序。
3. 避免死锁
线程等待还可以用来避免死锁,这是一种多个线程互相等待对方持有的资源而导致的僵局。通过合理的设计线程等待逻辑,可以预防这种情况的发生。
4. 同步执行
有时候我们需要确保所有线程都到达某个点之后再一起执行后续操作,这通常被称为屏障同步。例如,在并行算法中,可能需要所有线程完成各自部分的计算后再汇总结果。线程等待可以用来实现这样的同步点。
5. 线程池管理
在线程池中,空闲的线程会等待新的任务到来。这种等待可以避免线程在没有任务时浪费CPU资源。
6. 数据一致性
确保数据的一致性是多线程编程中的一个挑战。线程等待可以通过确保某个线程完成对数据的修改之后再允许其他线程访问,从而保证数据的一致性。
7. 提高性能
虽然听上去线程等待似乎会降低程序的执行效率,但实际上,适当的线程等待可以提高整体性能。这是因为避免了不必要的竞争和冲突,减少了上下文切换的次数,使得线程能够在更合适的时候执行。
3、pthread_join等待线程
pthread_join()
是POSIX线程库中的一个函数