线程处理是计算机编程中的一个重要概念,它涉及到多线程编程的各个方面,包括线程的创建、调度、同步、互斥、通信以及异常处理等。
线程的作用
线程处理在软件开发中扮演着重要角色,主要体现在以下几个方面:
1. 提高应用程序的响应能力:通过多线程处理,可以将耗时的操作放在后台线程中执行,从而避免阻塞主线程,使得应用程序能够更快地响应用户的操作。
2. 提高程序的吞吐量:在多处理器或多核系统上,多线程处理可以使得多个线程并行执行,从而提高程序的执行效率,减少程序的总体执行时间。
3. 实现复杂的并发操作:在需要处理多个并发任务时,如网络通信、文件操作等,多线程处理提供了一种有效的解决方案。
Linux 线程简介
Linux 中的线程是指轻量级的执行单元,相比于进程,具有以下特点:
(1)进程(Process)是正在执行的程序的实例。每个进程都有自己的地址空间、代码段、数据段和打开的文件描述符等资源。线程(Thread)是进程内的一个执行单元,它共享相同的地址空间和其他资源,包括文件描述符、信号处理等,但每个线程都有自己的栈空间。
(2)由于共享地址空间和数据段,同一进程的多线程之间进行数据交换比进程间通信方便很多,但也由此带来线程同步问题。
(3)同一进程的多线程共享大部分资源,除了每个线程独立的栈空间。这代表线程的创建、销毁、切换要比进程的创建、销毁、切换的资源消耗小很多,所以多线程比多进程更适合高并发。
线程控制
线程创建 pthread_create
#include <pthread.h>
/**
* 创建一个新线程
*
* pthread_t *thread: 指向线程标识符的指针,线程创建成功时,用于存储新创建线程的线程标识符
* const pthread_attr_t *attr: pthead_attr_t结构体,这个参数可以用来设置线程的属性,
* 如优先级、栈大小等。如果不需要定制线程属性,可以传入 NULL,此时线程将采用默认属性。
* void *(*start_routine)(void *): 一个指向函数的指针,它定义了新线程开始执行时的入口点。
* 这个函数必须接受一个 void * 类型的参数,并返回 void * 类型的结果
* void *arg: start_routine 函数的参数,可以是一个指向任意类型数据的指针
* return: int 线程创建结果
* 成功 0
* 失败 非0
*/
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
每个线程都有一个唯一的标识符(即线程ID),这个标识符是通过 pthread_t
类型的变量来表示的,当pthread_create
成功创建一个线程时,它会将新线程的标识符存储在 thread
参数指向的位置。
pthread_t定义在头文件 <pthreadtypes.h> 中,实际上是 long 类型(long 和 long int 是相同类型的不同写法)的别名。
typedef unsigned long int pthread_t;
新线程执行函数的声明为 void *(*start_routine)(void *)
,其入参和返回值都是 void *
指针。我们可以将传递给线程函数的参数包装为结构体,并将其指针作为入参,再在函数内部处理;同理我们可以在线程函数内部将要返回的状态码和返回值包装为结构体,并将指针作为返回值 return。
线程终止
线程终止有以下几种方法:
- 线程函数执行return语句;
- 线程函数内部调用
pthread_exit
函数; - 其他线程调用
pthread_cancel
函数。
线程终止函数为 pthread_exit
:
#include <pthread.h>
/**
* 结束关闭调用该方法的线程,并返回一个内存指针用于存放结果
* void *retval: 要返回给其它线程的数据
*/
void pthread_exit(void *retval);
当某个线程调用pthread_exit
方法后,该线程会被关闭(相当于return)。线程可以通过retval向其它线程传递信息,retval指向的区域不可以放在线程函数的栈内。其他线程(例如主线程)如果需要获得这个返回值,需要调用pthread_join
方法。
#include <pthread.h>
/**
* 等待指定线程结束,获取目标线程的返回值,并在目标线程结束后回收它的资源
*
* pthread_t thread: 指定线程ID
* void **retval: 这是一个可选参数,用于接收线程结束后传递的返回值。
* 如果非空,pthread_join 会在成功时将线程的 exit status 复制到 *retval 所指向的内存位置。
* 如果线程没有显式地通过 pthread_exit 提供返回值,则该参数将被设为 NULL 或忽略
* return: int 成功 0
* 失败 1
*/
int pthread_join(pthread_t thread, void **retval);
其它终止方式参数说明
#include <pthread.h>
/**
* @brief 将线程标记为detached状态。POSIX线程终止后,如果没有调用pthread_detach或pthread_join,其资源会继续占用内存,类似于僵尸进程的未回收状态。默认情况下创建线程后,它处于可join状态,此时可以调用pthread_join等待线程终止并回收资源。但是如果主线程不需要等待线程终止,可以将其标记为detached状态,这意味着线程终止后,其资源会自动被系统回收。
*
* @param thread 线程ID
* @return int 成功返回0,失败返回错误码
*/
int pthread_detach(pthread_t thread);
/**
* @brief 向目标线程发送取消请求。目标线程是否和何时响应取决于它的取消状态和类型
* 取消状态(Cancelability State):可以是enabled(默认)或disabled。如果取消状态为禁用,则取消请求会被挂起,直至线程启用取消功能。如果取消状态为启用,则线程的取消类型决定它何时取消。
* 取消类型(Cancelability Type):可以是asynchronous(异步)或deferred(被推迟,默认值)。
* asynchronous:意味着线程可能在任何时候被取消(通常立即被取消,但系统并不保证这一点)
* deferred:被推迟意味着取消请求会被挂起,直至被取消的线程执行取消点(cancellation point)函数时才会真正执行线程的取消操作。
* 取消点函数:是在POSIX线程库中专门设计用于检查和处理取消请求的函数。当被取消的线程执行这些函数时,如果线程的取消状态是enabled且类型是deferred,则它会立即响应取消请求并终止执行。man 7 pthreads可以看到取消点函数列表。
*
* @param thread 目标线程,即被取消的线程
* @return int 成功返回0,失败返回非零的错误码
* 需要注意的是,取消操作和pthread_cancel函数的调用是异步的,这个函数的返回值只能告诉调用者取消请求是否成功发送。当线程被成功取消后,通过pthread_join和线程关联将会获得PTHREAD_CANCELED作为返回信息,这是判断取消是否完成的唯一方式
*/
int pthread_cancel(pthread_t thread);
/**
* @brief 设置调用线程的取消状态
* PTHREAD_CANCEL_ENABLE:启用取消功能
* PTHREAD_CANCEL_DISABLE:禁用取消功能
*
* @param state 目标状态
* @param oldstate 指针,用于返回历史状态
* @return int 成功返回0,失败返回非零错误码
*/
int pthread_setcancelstate(int state, int *oldstate);
#include <pthread.h>
/**
* @brief 设置调用线程的取消类型
* PTHREAD_CANCEL_DEFERRED:设置取消类型为推迟
* PTHREAD_CANCEL_ASYNCHRONOUS:设置取消类型为异步
*
* @param type 目标类型
* @param oldtype 指针,用于接收历史类型
* @return int 成功返回0,失败返回非零错误码
*/
int pthread_setcanceltype(int type, int *oldtype);
测试代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <math.h>
#include <string.h>
// 定义结构体接收线程结果
typedef struct Result {
char *p;
int len;
} Result;
// 如果一个进程的多个线程同时使用到标准的输入输出流,会造成冲突的问题
// 导致只能有一个线程使用到标准到标准的输入输出流。
// 1 号线程
void * one_thread(void * arg)
{
// 初始化 Result 结构体
Result * result = malloc(sizeof(Result));
// 解析传递的参数
char code = *((char *)arg);
// 声明存放读取消息的字符串
char *ans = malloc(101);
while (1)
{
fgets(ans, 100, stdin);
if (ans[0] == code)
{
// 收到回复的消息
free(ans);
printf("1 号线程结束\n");
char * one_ans = strdup("1 号线程已被关闭");
result->p = one_ans;
result->len = strlen(one_ans);
// 结束线程 返回结果
pthread_exit((void *)result);
} else
{
printf("1 号线程正在等待\n");
}
}
}
// 2 号线程
void * two_thread(void * arg)
{
// 初始化 Result 结构体
Result * result = malloc(sizeof(Result));
// 解析传递的参数
char code = *((char *)arg);
// 声明存放读取消息的字符串
char *ans = malloc(101);
while (1)
{
fgets(ans, 100, stdin);
if (ans[0] == code)
{
// 收到回复的消息
free(ans);
printf("2 号线程结束\n");
char * one_ans = strdup("2 号线程已被关闭");
result->p = one_ans;
result->len = strlen(one_ans);
// 结束线程 返回结果
pthread_exit((void *)result);
} else
{
printf("2 号线程正在等待\n");
}
}
}
int main(int argc, char const *argv[])
{
// 创建两个线程
pthread_t pid_one;
pthread_t pid_two;
char one_code = 'o';
char two_code = 't';
Result * one_result = NULL;
Result * two_result = NULL;
// 创建线程 1
pthread_create(&pid_one, NULL, one_thread, &one_code);
// 创建线程 2
pthread_create(&pid_two, NULL, two_thread, &two_code);
// 获取线程 1 的结果
pthread_join(pid_one, (void **)&one_result);
printf("1 号线程:%s\n,统计:%d\n", one_result->p, one_result->len);
// 释放内存
free(one_result->p);
free(one_result);
// 获取线程 2 的结果
pthread_join(pid_two, (void **)&two_result);
printf("2 号线程:%s\n,统计:%d\n", two_result->p, two_result->len);
free(two_result->p);
free(two_result);
return 0;
}
后续
这里是学习中遇到的或想到的问题,以下都是个人总结。
2024-08-31
问题:上述代码中命令行输入的消息不以o或t开头,输出信息的线程是不确定的。
解决:由于没有合理控制线程的并发访问,而导致引发竞态条件。