Linux多线程
引言
在计算机科学的世界中,线程是一项关键的概念,它扮演着协调和优化程序执行的重要角色。与传统的程序执行方式相比,使用线程能够更有效地处理任务,提高程序的响应性和性能。在Linux系统中,线程的实现采用了独特的模型,使得多任务处理变得更为灵活和高效。本文将带领您深入了解Linux线程的基础知识,探讨其在计算机编程中的重要性,并展示如何充分利用这一概念来提升程序的效率和并发性。不论您是初学者还是有一些经验的开发者,通过学习Linux线程,您将更好地理解现代计算机系统的运行机制。
线程基础
在计算机科学中,线程是程序执行的最小单位,是进程内的一个独立执行流。与传统的进程相比,线程更加轻量级,能够更灵活地执行任务。
- 线程与进程的区别:
- 进程是独立的执行单元,有自己的地址空间和资源。
- 线程是进程的子集,共享相同的地址空间和资源,但拥有独立的执行流。
- 地址空间: 进程有独立的地址空间,每个进程有自己的内存空间,互不干扰。而线程共享相同的地址空间,它们可以直接访问进程的共享数据。
- 资源: 进程之间是相互独立的,一个进程崩溃不会影响其他进程。而线程之间是共享进程资源的,包括打开的文件、信号处理等。
- 线程的生命周期:
- 创建:通过系统调用创建新线程。
- 运行:线程执行其指定的任务。
- 阻塞:在等待某些条件满足时暂停执行。
- 解除阻塞:条件满足后,线程重新开始执行。
- 销毁:线程完成任务或被强制终止。
- 线程的基本操作:
- 创建线程: 使用系统调用(例如
pthread_create
)创建新线程。 - 销毁线程: 线程执行完任务后,可以通过系统调用释放资源(如
pthread_exit
)。 - 等待线程: 主线程可以等待其他线程完成执行(例如
pthread_join
)。
- 创建线程: 使用系统调用(例如
进程与线程
进程是资源分配的基本单位
- 进程是操作系统中的一个独立执行单位,拥有独立的内存空间、文件句柄和其他系统资源。每个进程都运行在自己的地址空间中,相互之间不直接共享内存。进程之间的通信通常需要使用进程间通信(IPC)机制,如管道、消息队列等。
线程是调度的基本单位
- 线程是进程内的一个执行单元,共享相同的进程资源。线程拥有独立的程序计数器、栈、寄存器等,但与同一进程的其他线程共享相同的内存空间和文件句柄。由于线程共享进程的地址空间,线程间通信相对容易,但也需要考虑同步和互斥问题。
在多线程环境中,线程共享进程的数据空间,但同时也拥有自己的一部分数据,以保障线程的独立性和隔离性。以下是线程共享和私有的一些关键数据:
1. 共享数据:
- 进程数据空间: 所有线程共享进程的内存空间,包括代码段、数据段、堆和共享库等。
- 文件描述符表: 所有线程共享相同的文件描述符表,使它们能够访问相同的文件和资源。
- 全局变量和静态变量: 进程中的全局变量和静态变量对所有线程都是可见的。
2. 线程私有数据:
-
线程ID: 每个线程都有自己独立的线程ID,用于标识和区分不同的线程。
-
栈: 每个线程拥有独立的栈空间,用于存储局部变量、方法调用和临时数据,确保线程之间不会相互干扰。
-
errno:
errno
是一个线程私有的错误码,用于记录最近一次发生的错误。 -
信号屏蔽字: 用于控制线程对信号的屏蔽,每个线程可以独立设置屏蔽或非屏蔽的信号,以影响信号的处理。
-
调度优先级: 线程可以具有自己的调度优先级,影响操作系统在多线程环境中的调度决策。
-
**寄存器集合(Registers):**线程拥有自己的寄存器集合,包括通用寄存器、程序状态寄存器等。这些寄存器用于存储线程执行过程中的临时数据和计算结果。
-
**程序计数器(Program Counter):**每个线程都有自己的程序计数器,用于记录当前正在执行的指令的地址。在线程切换时,程序计数器保存了线程的执行状态,确保线程能够从正确的位置继续执行。
这种共享和独立的组合方式既允许线程之间共享重要的上下文和资源,又保障了线程的隔离性,使它们能够独立运行和管理各自的数据。需要注意,线程间的共享数据需要进行适当的同步操作,以防止竞争条件和数据不一致性。
进程与线程关系图
线程优点
- 并发执行: 线程允许程序以并发的方式执行多个任务,从而提高了系统的吞吐量和响应速度。这对于需要同时处理多个任务的应用程序,如服务器和多媒体应用,是非常重要的。
- 资源共享: 线程可以共享相同的地址空间和文件描述符等资源,这使得数据共享和通信更为简便。这有助于提高程序的灵活性,减少资源的冗余使用。
- 响应性: 多线程使得程序可以保持响应性,即使某个线程阻塞,其他线程仍然可以执行。这对于需要实时响应用户输入的应用程序非常关键,如图形用户界面(GUI)应用。
- 资源利用率: 相比多进程模型,线程使用的资源较少。线程的创建、销毁和切换成本相对较低,因此可以更有效地利用系统资源,提高系统的整体性能。
- 任务分解: 线程可以将大任务分解为多个小任务并行执行,从而提高程序的整体性能。这对于需要处理大规模数据或复杂计算的应用程序是一项重要的优势。
线程缺点
- 同步与互斥: 多线程编程需要考虑同步和互斥的问题,以防止多个线程同时访问和修改共享资源。这涉及到使用锁、信号量等同步机制,增加了编程的复杂性。
- 死锁: 不当的同步可能导致死锁,即一组线程互相等待对方释放资源,导致程序无法继续执行。死锁是多线程编程中常见且难以解决的问题。
- 调试困难: 多线程程序的调试相对复杂。由于线程可能以不可预测的方式相互影响,因此发现和修复错误可能需要更多的时间和资源。
- 资源竞争: 多个线程访问共享资源时可能引发资源竞争问题,导致数据一致性的问题。必须小心设计和管理,以避免潜在的竞争条件。
- 性能开销: 创建和维护线程需要一定的系统资源。如果线程数量过多,可能导致性能开销增加,甚至反而降低程序的执行效率。因此,需要谨慎使用大量线程。
线程异常
-
空指针引用(NullPointerException):
- 原因: 访问对象时,对象引用为null。
- 防范: 在访问对象之前进行有效性检查。
-
数组越界异常(ArrayIndexOutOfBoundsException):
- 原因: 访问了数组的无效索引。
- 防范: 在访问数组元素之前检查数组的长度,确保索引在有效范围内。
-
除零异常(ArithmeticException):
- 原因: 执行除以零的算术运算。
- 防范: 在进行除法运算之前,确保分母不为零。
-
并发修改异常(ConcurrentModificationException):
- 原因: 在迭代集合的同时,另一个线程修改了集合结构。
- 防范: 在迭代集合时使用迭代器,并在修改集合结构时使用同步机制。
-
线程死锁(ThreadDeadlock):
- 原因: 两个或多个线程相互等待对方释放锁。
- 防范: 谨慎设计锁的获取顺序,避免循环依赖。
-
线程饥饿(ThreadStarvation):
- 原因: 一个或多个线程长时间无法获取到所需的资源。
- 防范: 使用适当的同步机制,避免某个线程独占资源过长时间。
-
内存溢出异常(OutOfMemoryError):
- 原因: 尝试分配超出堆空间限制的内存。
- 防范: 合理管理内存资源,监控程序的内存使用情况。
-
栈溢出异常(StackOverflowError):
- 原因: 调用栈递归层次过深,导致栈空间耗尽。
- 防范: 调整递归算法,增加栈的大小,或优化代码逻辑。
线程是进程的执行分支,而在多线程的环境中,线程出现异常的确有可能导致整个进程的终止。当一个线程抛出未捕获的异常时,这个异常会沿着调用栈向上传播,如果没有在适当的位置捕获和处理这个异常,它最终会导致整个进程的异常终止。
在多线程环境中,一个线程的异常通常不会直接影响其他线程,但如果这个异常没有被捕获,将会成为整个进程的致命错误。在这种情况下,操作系统可能会终止整个进程,并触发相应的信号机制,以确保系统的稳定性。
线程运用
1. 提高CPU密集型程序的执行效率:
- 任务并行执行: 将大任务划分为多个子任务,并在多个线程上并行执行,充分利用多核处理器,加速计算过程。
- 并发编程模型: 采用合适的并发编程模型,如Fork-Join框架或线程池,以优化任务的划分和调度。
2. 改善IO密集型程序的用户体验:
- 并发IO操作: 在一个线程执行IO操作的同时,让其他线程执行计算任务,提高程序整体的并发性。
- 并行下载和上传: 在网络通信中,多线程可以同时进行多个文件的下载和上传,提高数据传输效率。
线程接口
POSIX(Portable Operating System Interface)线程库是一个跨平台的线程管理标准,定义了在UNIX及类UNIX操作系统上进行多线程编程的API接口。在POSIX线程库中,常用的线程相关函数由
pthread
(POSIX threads)前缀。
以下是一些常见的 POSIX 线程库函数:
1. 线程创建:
-
函数原型:
1
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
-
参数说明:
thread
:指向pthread_t
类型的变量,用于存储新创建线程的标识符。attr
:线程属性,通常设置为NULL
,表示使用默认属性。start_routine
:线程的入口函数,是一个指向返回类型为void*
、接受一个void*
参数的函数指针。arg
:传递给线程入口函数的参数。
2. 线程等待:
-
函数原型:
1
int pthread_join(pthread_t thread, void **retval);
-
参数说明:
thread
:要等待的线程标识符。retval
:指向指针的指针,用于获取线程的返回值,可以设置为NULL
,输出型参数。
-
等待原因
-
等待线程完成: 主线程通常需要等待其他线程完成它们的任务,以确保在继续执行后续操作之前,所有线程都已经执行完毕。
-
获取线程的返回值:
pthread_join
允许主线程获取被等待线程的返回值。当线程通过pthread_exit
终止时,它可以返回一个值,而这个值可以通过pthread_join
获取。这对于线程之间的信息传递和结果收集很有用。 -
释放线程资源: 当一个线程终止时,它的资源(如栈空间等)不会立即被系统回收,而是被保留,以供其他线程查询。通过
pthread_join
,主线程等待被终止线程,确保系统可以安全地释放线程所占用的资源。 -
避免主线程提前终止: 如果主线程在其他线程执行完成之前就终止,那么整个进程可能会提前终止,导致未完成的线程无法完成它们的工作。使用
pthread_join
可以防止这种情况发生。
-
3. 线程睡眠:
-
函数原型:
1
int usleep(useconds_t usec);
-
参数说明:
usec
:以微秒为单位的睡眠时间。
4.线程终止:
-
从线程函数
return
:-
接口: 在线程函数中使用
return
语句。 -
使用场景: 适用于非主线程,当线程函数执行完毕时,自动终止线程。
-
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void *thread_function(void *arg) {
// 线程执行的工作
printf("Thread is executing...\n");
// 线程终止
return NULL;
}
int main() {
pthread_t thread;
// 创建线程
pthread_create(&thread, NULL, thread_function, NULL);
// 等待线程结束
pthread_join(thread, NULL);
printf("Thread has terminated.\n");
return 0;
}
-
-
线程调用
pthread_exit
:-
接口:
1
void pthread_exit(void *retval);
-
参数说明:
retval
:线程的返回值,可以为NULL
。
-
使用场景: 显式地告知线程要退出,可以在任意位置调用。
-
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void *thread_function(void *arg) {
// 线程执行的工作
printf("Thread is executing...\n");
// 线程终止
pthread_exit(NULL);
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, thread_function, NULL);
// 等待线程结束
pthread_join(thread, NULL);
printf("Thread has terminated.\n");
return 0;
}
-
-
线程调用
pthread_cancel
:-
接口:
1
int pthread_cancel(pthread_t thread);
-
参数说明:
thread
:要取消的线程标识符。
-
使用场景: 一个线程请求终止同一进程中的另一个线程。
-
注意事项:
- 被取消的线程需要注册取消清理函数。
- 取消功能可能由于系统或库的实现差异而表现不同。
-
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void cleanup_function(void *arg)
{
// 取消清理函数
printf("Cleanup function called.\n");
}
void *thread_function(void *arg)
{
// 注册取消清理函数
pthread_cleanup_push(cleanup_function, NULL);
// 线程执行的工作
printf("Thread is executing...\n");
// 自己取消自己不会触发清理函数
// pthread_cancel(pthread_self());
// 弹出取消清理函数
// 参数为 0 表示弹出时不执行清理函数,其他非零值表示执行清理函数
pthread_cleanup_pop(0);
printf("Thread has terminated.\n");
// 线程终止
return NULL;
}
int main()
{
pthread_t thread;
pthread_create(&thread, NULL, thread_function, (void *)"newThread");
// 取消创建的子线程
pthread_cancel(thread);
// 读者可自行取消此取消函数 观察输出结果
// 等待线程结束
pthread_join(thread, NULL);
return 0;
}
-
在 POSIX 线程中,如果线程自己请求取消(使用
pthread_cancel(pthread_self())
),它将立即被取消,不会等待线程执行完清理函数。这是为了避免在取消线程时产生死锁或不一致的情况。因此,自己请求取消时,清理函数可能不会被执行。这些函数提供了不同的方式来终止线程,开发者可以根据实际需求选择适当的方法。在使用
pthread_cancel
时,需要注意注册和正确使用取消清理函数。
5.线程分离:
-
函数原型:
1
int pthread_detach(pthread_t thread);
-
参数说明:
thread
:要分离的线程标识符。
-
功能说明:
- 该函数用于将指定线程标识符标记为分离状态,一旦线程终止,它的资源会被自动回收。分离线程不需要其他线程调用
pthread_join
来等待其终止。
- 该函数用于将指定线程标识符标记为分离状态,一旦线程终止,它的资源会被自动回收。分离线程不需要其他线程调用
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统即分离线程,当线程退出时,自动释放线程资源。
- 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
- joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
6.获取线程标识符:
-
函数原型:
1
pthread_t pthread_self(void);
-
返回值:
- 返回调用线程的线程标识符。
-
功能说明:
- 该函数用于获取当前线程的线程标识符,可以用于识别当前执行线程。