Linux系统编程系列之线程

一、什么是线程

线程(Thread)是计算机中的基本执行单元,是操作系统调度的最小单位。 线程是进程内的一个独立执行流程,一个进程可以包含多个线程 ,这些线程共享进程的资源,但每个线程都有自己的独立栈空间以及程序计数器。

二、线程与进程的优缺点

1、线程的优点

(1)、线程创建和销毁的开销比进程小,因为线程共享进程中的地址空间和其他资源。

(2)、线程可以同时执行多个任务,提高了系统的并发性能。

(3)、线程之间的通信和同步比进程之间的通信和同步更快捷和简单,因为线程共享同一进程的内存。

(4)、线程可用于执行GUI等交互性任务而不会卡住整个应用程序。

2、线程的缺点

(1)、 多线程访问共享数据时,需要使用同步技术,否则会导致不可预期的结果。

(2)、 线程的调试和bug定位比较困难,因为多个线程共享进程的执行环境。

(3)、 如果线程中出现了异常,可能会影响整个进程。

3、进程的优点

(1)、 进程相互独立,不会相互影响,因此更加健壮和安全。

(2)、进程可以在不同的硬件和操作系统上运行,更具有可移植性。

(3)、 进程使用管道等IPC(进程间通信)机制可以方便的实现进程之间的通信和同步。

4、进程的缺点

(1)、进程创建和销毁的开销比较大,因为每个进程都需要独立的地址空间和系统资源。

(2)、进程之间通信和同步需要使用IPC技术,比较繁琐和复杂。

(3)、进程的并发性能比较差,不能同时执行多个任务。

三、线程的使用场景

1、多任务处理:多线程可以同时处理多个任务,提高程序的执行效率和响应速度。

2、并发访问:当多个线程同时访问共享资源时,需要使用线程控制技术,避免出现竞态条件和死锁等问题。

3、异步编程:线程可以在后台执行一些耗时的操作,不会阻塞主线程,提高程序的用户体验。

4、服务器编程:服务器一般要同时处理多个客户端请求,使用多线程可以提高服务器的并发处理能力。

5、图形界面编程:图形界面程序中需要使用线程避免阻塞用户界面,实现异步更新UI界面。

6、大数据处理:对于大数据处理和分析,多线程可以提高数据处理的效率和速度。

7、游戏开发:游戏开发中需要实时更新游戏画面和处理用户输入,需要使用多线程技术实现。

四、与线程有关的函数API

1、线程的创建

创建一条POSIX线程非常简单,只需要指定线程的执行函数即可

cpp 复制代码
// 创建一条线程
int pthread_create(pthread_t *thread, 
                   const pthread_attr_t *attr,
                   void *(*start_routine) (void *), 
                   void *arg);

// 接口说明:
    返回值:成功返回0,失败返回一个错误码
    参数thread:新线程的TID
    参数attr:线程属性,若创建标准线程则该参数可设置为NULL
    参数start_routine:线程函数,是一个回调函数,跟信号的例程函数有点像
    参数arg:线程函数的参数

2、线程的退出

与进程类似,当一条线程执行完毕其任务时,可以使用接口来退出

cpp 复制代码
// 线程的退出
void pthread_exit(void *retval);

// 接口说明
    参数retval:线程的返回值,若线程没有数据可返回则可写成NULL

pthread_exit()与exit()的区别
pthread_exit():退出当前线程
exit():退出当前进程(即退出进程中的所有进程)

3、线程的结合

与进程类似,线程退出后不会立即释放其所占有的系统资源 ,而会成为一个僵尸线程。 其他线程可使用**pthread_join()**来释放僵尸线程的资源,并可获得其退出时返回的退出值,该函数接口被称为线程的接合函数:

cpp 复制代码
// 阻塞等待指定线程退出
int pthread_join(pthread_t tid, void **val);

// 非阻塞接合指定线程退出
int pthread_tryjoin_np(pthread_t tid, void **retval);

// 在指定时间内阻塞接合指定线程的退出
int pthread_timedjoin_np(pthread_t tid, void **retval, const struct timespec *ashtime);

// 接口说明
    (1)若指定tid的线程尚未退出,那么该函数将持续阻塞
    (2)若只想阻塞等待指定线程tid退出,而不想要其退出值,那么val可置为NULL
    (3)若指定tid的线程处于分离状态,或者不存在,则该函数会出错返回

4、获取线程TID

cpp 复制代码
// 获取线程TID
pthread_t pthread_self(void);

// 接口说明
    返回值:线程TID

该接口类似进程管理中的getpid(),但是进程的PID是系统全局资源,而线程的TID仅限于进程内部的线程间有效。当我们要对某条线程执行发送信号,取消,阻塞接合等操作时,需要用到线程的TID。

5、线程错误码

线程函数对系统错误码的处理跟标准C库函数的处理方式有很大不同,标准C库函数会对全局错误码errno进行设置,而线程函数发生错误时会直接返回错误码。以线程接合为例,若要判定接合是否成功,成功的情况下输出僵尸线程的退出值,失败的情况下输出失败的原因,实现代码应该这么写

cpp 复制代码
#include <error.h> // 头文件中定义了errno变量

void *val;

errno = pthread_join(tid, &val);

if(errno == 0)
{
    printf("成功接合线程,其退出值为:%d\n", (int)val;
}
else
{
    perror("接合线程失败");
}

6、函数单例

许多时候,我们希望某个函数只被严格执行一次,这种需求在一些初始化功能模块中尤为常见,但是如果某个进程中内含多条线程,无法预先知晓哪条线程会先执行,那么初始化就会被执行多次,但如果使用函数单例就会只执行一次。

cpp 复制代码
// 函数单例启动接口
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

// 接口说明
    
    参数once_control:用来关联某个函数单例,被关联的函数单例只会被执行一遍
    参数init_routine:函数指针指向的函数就是只执行一遍的函数单例

// 通常参数once_control指定为函数单例控制
    pthread_once_t once_control = PTHREAD_ONCE_INIT;

五、案例

cpp 复制代码
// 线程的案例

#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <string.h>

int flag = 0;   // 简单的标志位来控制同步
char data[100];

// 线程1的例程函数,用来接收数据
void *recv_routine(void *arg)
{
    printf("I am recv_routine, my tid = %ld\n", pthread_self());
    while(1)
    {
        if(flag)
        {
            printf("pthread1 read data: %s\n", data);
            memset(data, 0, sizeof(data));
            flag = 0;
        }
    }
}

// 线程2的例程函数,用来发送数据
void *send_routine(void *arg)
{
    printf("I am send_routine, my tid = %ld\n", pthread_self());
    while(1)
    {
        printf("please input data:\n");
        fgets(data, 100, stdin);
        printf("pthread2 send data\n");
        flag = 1;
    }
}

int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;

    // 创建线程1,用来接收数据
    errno = pthread_create(&tid1, NULL, recv_routine, NULL);
    if(errno == 0)
    {
        printf("pthread create recv_routine success, tid = %ld\n", tid1);
    }
    else
    {
        perror("pthread create recv_routine fail\n");
    }

    // 创建线程2,用来发送数据
    errno = pthread_create(&tid2, NULL, send_routine, NULL);
    if(errno == 0)
    {
        printf("pthread create send_routine success, tid = %ld\n", tid2);
    }
    else
    {
        perror("pthread create send_routine fail\n");
    }

    // 一定要加这个,否则主函数直接退出,相当于进程退出,所有线程也退出
    // 或者加上while(1)等让主函数不退出
    pthread_exit(0);
    
    return 0;
}

六、总结

线程可以提供系统的并发性,开销比进程更加小,但是不如进程健壮,移植性好。线程有自己的属性,下一篇博客将讲解。

相关推荐
Crossoads10 分钟前
【数据结构】排序算法---桶排序
c语言·开发语言·数据结构·算法·排序算法
DREAM依旧41 分钟前
《深入了解 Linux 操作系统》
linux
QXH20000042 分钟前
数据结构—单链表
c语言·开发语言·数据结构
David猪大卫1 小时前
数据结构修炼——顺序表和链表的区别与联系
c语言·数据结构·学习·算法·leetcode·链表·蓝桥杯
阿赭ochre1 小时前
Linux环境变量&&进程地址空间
linux·服务器
Iceberg_wWzZ1 小时前
数据结构(Day14)
linux·c语言·数据结构·算法
微尘81 小时前
C语言存储类型 auto,register,static,extern
服务器·c语言·开发语言·c++·后端
可儿·四系桜1 小时前
如何在多台Linux虚拟机上安装和配置Zookeeper集群
linux·服务器·zookeeper
Flying_Fish_roe1 小时前
linux-软件包管理-包管理工具(Debian 系)
linux·运维·debian
五味香2 小时前
C++学习,动态内存
java·c语言·开发语言·jvm·c++·学习·算法