Linux应用软件编程04-线程

一、线程介绍

1.1 线程基本概念

  • 线程是一个轻量级的进程,一个任务创建、调度、消亡的过程用来实现多任务并发。
  • 线程是操作系统任务调度的最小单位。
  • 线程由某个进程创建。

1.2 线程的创建

线程由所在进程创建,创建时,进程为其分配独立的栈区空间(默认8M)。
同一个进程中的线程,共用所在进程的堆区、数据区(全局变量、静态变量、字符串常量

)、文本区。

1.3 线程的调度

与进程调度保持一致,宏观并行,微观串行

1.4 线程消亡

线程结束必须回收线程空间,否则会产生僵尸线程

1.5 进程和线程的区别

|---------------------------|--------------------------|
| 进程 | 线程 |
| 正在执行的程序 | 轻量级的进程 |
| 操作系统资源分配的最小单位 | 操作系统任务调度的最小单位 |
| 资源空间消耗大(0-4G虚拟内存空间) | 资源空间消耗小(栈区独立,其他区域共享) |
| 进程效率低(创建、任务切换) | 线程效率高(创建、任务切换) |
| 进程安全性高(进程空间独立) | 线程安全性低 |
| 进程间通信较复杂,需要用到IPC机制 | 线程间通信简单(全局变量) |
| 在相同资源平台下,多进程的并发量少于多线程 |

1.6 多进程和多线程优缺点

1)运行效率:多线程 > 多进程
多线程在同一进程空间内部任务调度效率更高。
2)安全:多进程 > 多线程
进程空间是独立的,一个进程异常结束,不会影响其余进程。
线程位于进程空间内部,一个线程异常结束,会导致进程结束,其余线程也随之结束。
3)通信实现:多线程 > 多进程
线程位于进程空间内部,共享进程的文本段、数据段(全局变量、静态变量、字符串常
量)、堆区,所以可以通过共享空间实现通信。
进程空间是独立的,没有共享空间,无法通信。
4)通信编程复杂程度:多进程 > 多线程
多线程使用全局变量通信会引入资源竞争问题,需要加锁来解决。
多进程空间独立的,没有共享空间,需要使用第三方机制来完成进程间通信。

二、线程相关接口函数

2.1 pthread_create

cpp 复制代码
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
功能:在进程中创建一个线程
参数:
    thread :存放线程ID空间的首地址
    attr :线程的属性(栈空间大小、 优先级、 分离/加入属性),默认属性传NULL
    start_routine :函数指针,指向线程要执行的函数,线程启动后要执行的任务的函数地址(线程任务函数)
    arg :传递给线程任务函数的参数
返回值:
    成功:0
    失败:返回错误码

注意:

  • 编译和链接时要加-lpthread, 表示链接线程库。
  • 线程执行时,线程所在的进程不能先退出。

2.2 pthread_exit

cpp 复制代码
void pthread_exit(void *retval);
功能:结束线程任务
参数:
    retval: 线程结束时的状态
返回值:
    缺省

2.3 pthread_join

cpp 复制代码
int pthread_join(pthread_t thread, void **retval);
功能:回收线程空间
参数:
    thread:回收线程的TID
    retval:存放线程结束状态空间的首地址
返回值:
    成功返回0
    失败返回错误码

具有阻塞功能

pthread_join() 函数会等待由 thread 参数指定的线程完成其执行过程。如果该线程已经结束执行,那么 pthread_join() 将立即返回。由 thread 指定的线程必须处于非分离状态。如果 retval 不为 NULL,则 pthread_join() 会将目标线程的退出状态复制到由 retval 所指向的位置。如果目标线程被取消(pthread_cancel),则会在由 retval 所指向的位置放置 PTHREAD_CANCELED (-1) 标志。

2.4 pthread_self

pthread_t pthread_self(void);

功能:返回调用该函数的线程自身的线程ID(TID)
练习1:创建3个线程

cpp 复制代码
#include "head.h"
void *thread1fun(void *arg)
{
    printf("线程1(TID:%#lx)开始执行\n", pthread_self());
    return NULL;
}
void *thread2fun(void *arg)
{
    printf("线程2(TID:%#lx)开始执行\n", pthread_self());
    return NULL;
}
void *thread3fun(void *arg)
{
    printf("线程3(TID:%#lx)开始执行\n", pthread_self());
    return NULL;
}
int main(void)
{
    pthread_t tid[3];
    int i = 0;
    void *(*p[3])(void *) = {thread1fun, thread2fun, thread3fun};
    for (i = 0; i < 3; i++)
    {
        pthread_create(&tid[i], NULL, p[i], NULL);
    }
    for (i = 0; i < 3; i++)
    {
        pthread_join(tid[i], NULL);
    }
#if 0
    pthread_t tid1,tid2,tid3;
    pthread_create(&tid1, NULL, thread1fun, NULL);
    pthread_create(&tid2, NULL, thread2fun, NULL);
    pthread_create(&tid3, NULL, thread3fun, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);
#endif
    return 0;
}
cpp 复制代码
#include "head.h"
#define DEBUG   0
void *thread(void *arg)
{
#if DEBUG
    int *pval = arg;
    printf("线程%d(TID:%#lx)开始执行\n", *pval, pthread_self());
#else
    int val = (int)arg;
    printf("线程%d(TID:%#lx)开始执行\n", val+1, pthread_self());
#endif
    return NULL;
}
int main(void)
{
    int i = 0;
    pthread_t tid[3] = {0};
#if DEBUG
    int val[3] = {1, 2, 3};
#endif
    for (i = 0; i < 3; i++)
    {
#if DEBUG
        pthread_create(&tid[i], NULL, thread, &val[i]);
#else
        pthread_create(&tid[i], NULL, thread, (void *)i);
#endif
    }
    for (i = 0; i < 3; i++)
    {
        pthread_join(tid[i], NULL);
    }
    return 0;
}

练习2:生产者与消费者编程思想

创建4线程任务,
线程1开始时打印: 采集线程(TID:)开始执行,然后间隔1s打印 正在采集
线程2开始时打印: 存储线程(TID:)开始执行,然后间隔2s打印 正在存储
线程3开始时打印: 显示线程(TID:)开始执行,然后间隔5s打印 正在显示
线程4开始时打印: 日志线程(TID:)开始执行,然后间隔10s打印 正在日志记录

主进程负责从终端接收字符, 如果接收到的是
'a'或者'A' 采集线程停止/开始打印
'b'或者'B' 存储线程停止/开始打印
'c'或者'C' 显示线程停止/开始打印
'd'或者'D' 日志线程停止/开始打印

cpp 复制代码
#include "head.h"
typedef struct pthread_info
{
    pthread_t tid;                      //线程ID号
    char threadname[32];                //线程名
    int IntervalSec;                    //间隔秒数
    int IsWork;                         //是否打印标志位
}PthreadInfo_t;
void *threadfun(void *arg)
{
    PthreadInfo_t *pthreadinfo = arg;
    printf("%s (TID:%#lx)开始执行\n", pthreadinfo->threadname, pthreadinfo->tid);
    while (1)
    {
        if (pthreadinfo->IsWork)
        {
            printf("%s正在运行\n", pthreadinfo->threadname);
            sleep(pthreadinfo->IntervalSec);
        } 
    }
    return NULL;
}
int main(void)
{
    char ch = 0;
    PthreadInfo_t thread[4] = {
        {
            .threadname = "采集线程",
            .IntervalSec = 1,
            .IsWork = 0xffffffff,
        },
        {
            .threadname = "存储线程",
            .IntervalSec = 2,
            .IsWork = 0xffffffff,
        },
        {
            .threadname = "显示线程",
            .IntervalSec = 5,
            .IsWork = 0xffffffff,
        },
        {
            .threadname = "日志线程",
            .IntervalSec = 10,
            .IsWork = 0xffffffff,
        },
    };
    int i = 0;
    for (i = 0; i < 4; i++)
    {
        pthread_create(&thread[i].tid, NULL, threadfun, &thread[i]);
    }
    while (1)
    {
        scanf(" %c", &ch);
        switch (ch)
        {
            case 'a':
            case 'A':thread[0].IsWork = ~thread[0].IsWork;break;
            case 'b':
            case 'B':thread[1].IsWork = ~thread[1].IsWork;break;
            case 'c':
            case 'C':thread[2].IsWork = ~thread[2].IsWork;break;
            case 'd':
            case 'D':thread[3].IsWork = ~thread[3].IsWork;break;
        }
    }
    for (i = 0; i < 4; i++)
    {
        pthread_join(thread[i].tid, NULL);
    }
    return 0;
}

三、线程的分离属性

① 分离属性:具有分离属性的线程,在运行结束后由系统自动回收其资源(如线程控制块、栈空间等),无需其他线程调用 pthread_join 进行等待。这类线程一旦终止,其资源立即被释放,外界无法感知其结束时刻。

② 非分离属性:具有非分离属性的线程,在其终止后,资源不会自动释放,必须由其他线程调用 pthread_join 来等待其结束并回收资源。

③默认属性:线程默认具有非分离属性。

④ 设置线程分离属性:

cpp 复制代码
int pthread_detach(pthread_t thread);
功能:设置线程的分离属性
参数:
    thread:要设置的线程的线程ID
返回值:
    成功:0
    失败:!0
int pthread_attr_init(pthread_attr_t *attr);
功能:将 attr 指向的线程属性进行初始化
int pthread_attr_destroy(pthread_attr_t *attr);
功能:将 attr 指向的线程属性销毁
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
功能:设置线程属性
    分离状态:PTHREAD_CREATE_DETACHED
    非分离状态(可连接状态):PTHREAD_CREATE_JOINABLE
cpp 复制代码
#include "head.h"
void *thread(void *arg)
{
    printf("线程(TID:%#lx)开始执行\n", pthread_self());
    return NULL;
}
int main(void)
{
    pthread_t tid[3];
    int i = 0;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    for (i = 0; i < 3; i++)
    {
        pthread_create(&tid[i], &attr, thread, NULL);
    }
    pthread_attr_destroy(&attr);
    while (1)
    {
       
    }
    return 0;
}

四、线程间通信

① 线程间通信方式:
线程使用全局变量可以实现通信。
多线程同时使用全局变量会引入资源竞争,需要通过互斥锁解决资源竞争问题。
② 概念:
原子操作:不能被CPU任务调度打断的一次最小的操作称为原子操作。
临界代码/临界区:加锁解锁中间的代码,这段代码用户不会被同时执行。

4.1 线程间互斥机制

① 多线程访问临界资源时,存在资源竞争问题。
② 临界资源:多个线程可以同时操作的资源,比如:全局变量,共享内存等。
③ 如何解决资源竞争问题:
线程的互斥机制:让多个线程在访问临界资源时,具有排他性访问的特性

④ 互斥机制:互斥锁、信号量、读写锁(写独占,读共享)、自旋锁

⑤ 互斥锁:实现互斥机制
创建互斥锁:pthread_mutex_t
初始化锁:pthread_mutex_init
加锁:pthread_mutex_lock/pthread_mutex_trylock
解锁:pthread_mutex_unlock
销毁锁:pthread_mutex_destroy

cpp 复制代码
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
        const pthread_mutexattr_t *restrict attr);
功能:初始化互斥锁
参数:
    mutex :互斥锁空间首地址
    attr: 互斥锁的属性, 默认属性NULL
返回值:
    成功:0
    失败:非0
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:以阻塞方式等待加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能:以非阻塞方式加锁,如果该锁已被使用,则返回
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:解锁/释放锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:销毁锁
cpp 复制代码
#include "head.h"
pthread_mutex_t lock;
int num = 0;
void *thread1(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&lock);
        num = 100;
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}
void *thread2(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&lock);
        num = 200;
        printf("num = %d\n", num);
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}
int main(void)
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_mutex_init(&lock, NULL);
    pthread_create(&tid1, NULL, thread1, NULL);
    pthread_create(&tid2, NULL, thread2, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_mutex_destroy(&lock);
    return 0;
}

⑥ 死锁

1)死锁指的是在多线程环境中,每个执行流(线程)都有未释放的资源,且互相请求对方未释放资源,从而导致陷入永久等待状态的情况。(由于多个锁的加锁解锁导致线程无法继续执行的状态称为死锁)
2)产生死锁的四个充分必要条件(四条全中 = 死锁,破掉任意一条 = 解除):
(1) 互斥条件:一个资源同一时刻只能被一个任务占用(一个执行流获取锁后,其它执行流不能再获取该锁)。
(2) 请求与保持条件:一个任务已经持有了至少一个资源,但它又提出了新的资源请求,而该新资源正被其他任务占用。此时,该请求进程会进入阻塞等待状态,但是对自己已经持有的资源又保持不放。
(3) 不可剥夺条件:资源只能由持有者主动释放,在它使用完毕主动释放之前,
系统/其他任务不能强行抢过来(A执行流拿着锁,其它执行流不能释放)。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系(多个执行流拿着对方想要的锁,并且各执行流还去请求对方的锁)。
3)避免产生死锁:

  • 使用pthread_mutex_trylock替代pthread_mutex_lock
  • 锁一定要成对出现
  • 使线程的加解锁顺序一致
  • 破坏环路等待条件使用非阻塞锁,一旦线程发现请求的锁被使用,就去释放自己拥有的资源

4.2 线程间同步机制

① 线程间同步:让多个线程任务执行某部分任务的时候具有先后顺序。
② 同步方式:信号量
③ 可以将信号量看成一个资源数,当该资源数>0的时候,可以申请到信号量,当资源时为0时,申请信号量阻塞等待。申请完信号量时,该信号量对应的资源数自动-1。释放对应的信号量时,该信号量对应的资源数自动+1。
④ 信号量是一种资源,在程序运行过程中,代码、变量、 CPU、内存都可以看成是一种资源。资源一定是有限的,资源只能被初始化、销毁、申请和释放。

⑤ 函数接口:

创建信号量:sem_t
初始化信号量:sem_init()
申请信号量:sem_wait();sem_trywait();
释放信号量:sem_post();
销毁信号量:sem_destroy();

cpp 复制代码
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化信号量
参数:
    sem:信号量的地址
    pshared:
        0: 同一进程的线程间共享
        非0: 进程间共享
    value:初始化的资源数值
返回值:
    成功返回0
    失败返回-1
int sem_destroy(sem_t *sem);
功能:销毁信号量
参数:
    sem:信号量的首地址
返回值:
    成功返回0
    失败返回-1
int sem_wait(sem_t *sem);
功能:
    申请信号量, 如果资源数>0则让资源数-1
    如果资源数 == 0,阻塞等待直到资源 > 0 才能申请到资源继续向下执行
参数:
    sem:信号量空间首地址
返回值:
    成功返回0
    失败返回-1
int sem_post(sem_t *sem);
功能:释放资源, 让资源数+1
参数:
    sem:信号量空间首地址
返回值:
    成功返回0
    失败返回-1

练习:实现3个线程任务,线程1循环打印A,线程2循环打印B,线程3循环打印C,但是要求打印出来的顺序总是ABC

cpp 复制代码
#include "head.h"
sem_t semA;
sem_t semB;
sem_t semC;
void *threadA(void *arg)
{
    while (1)
    {
        sem_wait(&semA);
        printf("A");
        sem_post(&semB);
    }
    return NULL;
}
void *threadB(void *arg)
{
    while (1)
    {
        sem_wait(&semB);
        printf("B");
        sem_post(&semC);
    }
    return NULL;
}
void *threadC(void *arg)
{
    while (1)
    {
        sem_wait(&semC);
        printf("C");
        sem_post(&semA);
    }
    return NULL;
}
int main(void)
{
    pthread_t tidA;
    pthread_t tidB;
    pthread_t tidC;
    sem_init(&semA, 0, 1);
    sem_init(&semB, 0, 0);
    sem_init(&semC, 0, 0);
    pthread_create(&tidA, NULL, threadA, NULL);
    pthread_create(&tidB, NULL, threadB, NULL);
    pthread_create(&tidC, NULL, threadC, NULL);
    pthread_join(tidA, NULL);
    pthread_join(tidB, NULL);
    pthread_join(tidC, NULL);
    sem_destroy(&semA);
    sem_destroy(&semB);
    sem_destroy(&semC);
    return 0;
}
相关推荐
charlie11451419116 分钟前
Cinux · musl 静态移植:对齐 Linux ABI、铺初始栈,以及一个被 SMAP 拦下的潜伏 bug
linux·开发语言·c++·操作系统·开源项目
流浪00131 分钟前
C/C++后端筑基系列(二):C++ 从基础到进阶
开发语言·c++
LuminousCPP1 小时前
数据结构-二叉树(五):查找、销毁与前序序列建树
c语言·数据结构·笔记·二叉树
Loongproxy1 小时前
抖音企业号更换公司主体怎么操作?矩阵账号配静态长效IP的稳定方案
服务器·网络·网络协议·tcp/ip
智恒百亿9 小时前
RTX 5090 全场景技术应用解析:从游戏算力到专业生产力落地
大数据·服务器·游戏
2401_868534789 小时前
指令集-最常用 Linux 命令速查手册(整合版)
linux·网络协议
ltl9 小时前
小文件问题:文件数量如何放大元数据与 I/O 成本
linux
Navigator_Z9 小时前
LeetCode //C - 1208. Get Equal Substrings Within Budget
c语言·算法·leetcode