【C语言实战(67)】从0到1:C语言多线程编程实战(POSIX线程版)

目录

  • 一、引言
  • 二、多线程基础概念
    • [2.1 线程与进程的区别](#2.1 线程与进程的区别)
    • [2.2 POSIX 线程库(pthread)简介](#2.2 POSIX 线程库(pthread)简介)
    • [2.3 多线程的优势](#2.3 多线程的优势)
  • 三、多线程基础实战
    • [3.1 线程创建与销毁](#3.1 线程创建与销毁)
      • [3.1.1 pthread_create 函数](#3.1.1 pthread_create 函数)
      • [3.1.2 pthread_join 函数](#3.1.2 pthread_join 函数)
      • [3.1.3 实战](#3.1.3 实战)
    • [3.2 线程同步:互斥锁(解决资源竞争)](#3.2 线程同步:互斥锁(解决资源竞争))
      • [3.2.1 互斥锁原理](#3.2.1 互斥锁原理)
      • [3.2.2 实战](#3.2.2 实战)
      • [3.2.3 死锁避免](#3.2.3 死锁避免)
  • 四、多线程进阶实战
    • [4.1 线程池基础](#4.1 线程池基础)
    • [4.2 实战](#4.2 实战)
    • [4.3 多线程调试](#4.3 多线程调试)
  • 五、总结

一、引言

在当今的软件开发领域,随着硬件性能的不断提升,尤其是多核处理器的广泛应用,如何充分利用硬件资源,提高程序的执行效率,成为了开发者们关注的重点。多线程编程作为一种有效的解决方案,在 C 语言开发中占据着愈发重要的地位。它允许一个程序同时执行多个任务,从而显著提升程序的性能和响应性。

多线程编程在众多场景中都有着广泛的应用。在网络服务器开发中,通过多线程可以让服务器同时处理多个客户端的连接请求和数据传输,大大提高服务器的并发处理能力;在多媒体处理领域,如音视频编解码,不同的线程可以分别负责音频处理、视频处理等任务,实现高效的并行处理;在数据处理和分析场景中,多线程能够加速对大规模数据的处理,提高数据处理的效率。

在 Linux 系统中,POSIX 线程(pthread)库为 C 语言开发者提供了一套强大且标准的多线程编程接口。通过 POSIX 线程,开发者可以方便地创建、管理线程,以及处理线程之间的同步和通信问题,实现高效的多线程编程。接下来,就让我们深入探索基于 POSIX 线程的 C 语言多线程编程实战。

二、多线程基础概念

2.1 线程与进程的区别

在操作系统中,进程是资源分配的基本单位,它拥有独立的地址空间、代码段、数据段和堆栈段。每个进程在运行时,操作系统会为其分配一系列的资源,包括内存、文件描述符、设备等,这些资源使得进程能够独立地执行其任务,不同进程之间的资源相互隔离,互不干扰。例如,当我们同时打开浏览器和音乐播放器时,它们分别是两个独立的进程,各自拥有自己的内存空间和资源,浏览器进程崩溃不会影响音乐播放器进程的正常运行。

而线程则是进程内的执行单元,是可调度的基本实体。一个进程可以包含多个线程,这些线程共享所属进程的资源,如内存地址空间、打开的文件等。以浏览器进程为例,它可能包含多个线程,一个线程负责处理用户界面的交互,如点击、滚动等操作;另一个线程负责网络请求,获取网页数据;还有线程负责解析和渲染网页内容。这些线程共享浏览器进程的资源,通过协同工作,提高了浏览器的整体性能和响应速度。

从资源分配角度看,进程拥有独立资源,不同进程之间资源不共享;而线程共享进程资源,线程间通信相对容易,但也容易引发资源竞争问题。在调度执行方面,进程的切换开销较大,因为切换进程时需要保存和恢复大量的状态信息,包括程序计数器、寄存器内容、内存映射等;而线程切换开销较小,由于线程共享进程地址空间,切换时只需保存和恢复少量的寄存器和栈指针内容。在并发性和并行性上,进程和线程都支持并发执行,在单处理器系统中,通过时间片轮转等调度算法,实现多个进程或线程的并发执行;在多处理器系统中,不同进程可以真正并行运行,而线程在多处理器系统中更具优势,能够充分利用多核 CPU 的计算能力,实现真正的并行计算。

2.2 POSIX 线程库(pthread)简介

POSIX 线程库(pthread)是 Unix 系统下的标准化多线程编程接口,由 IEEE POSIX 标准(IEEE 1003.1c)定义。它提供了一组丰富的函数,用于在同一进程内创建、管理和同步多个线程,从而实现并发和并行处理。在大多数 Linux 系统中,pthread 库是默认安装的原生线程库,这使得开发者可以方便地使用它进行多线程编程。

pthread 库的核心函数包括pthread_create、pthread_join和pthread_mutex_t等。pthread_create用于创建一个新线程,它的函数原型为:

c 复制代码
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

其中,thread是指向pthread_t类型变量的指针,用于返回新创建线程的 ID;attr用于设置线程的属性,如栈大小、分离状态等,通常传入NULL表示使用默认属性;start_routine是线程的入口函数,即新线程启动后要执行的函数,该函数必须接受一个void类型的参数并返回一个void类型的值;arg是传递给入口函数start_routine的参数。

pthread_join函数用于等待指定线程结束,并回收该线程的资源,其函数原型为:

c 复制代码
int pthread_join(pthread_t thread, void **retval);

thread为要等待的线程 ID,retval用于获取线程函数的返回值。通过使用pthread_join,可以避免出现僵尸线程,确保线程资源的正确回收。

pthread_mutex_t是互斥锁类型,用于线程同步,解决多线程环境下的资源竞争问题。与之相关的函数有pthread_mutex_lock(加锁)和pthread_mutex_unlock(解锁)。当一个线程需要访问共享资源时,先调用pthread_mutex_lock对互斥锁进行加锁,如果此时互斥锁未被其他线程占用,则该线程获得锁并可以访问共享资源;如果互斥锁已被占用,则该线程会被阻塞,直到锁被释放。访问完共享资源后,线程调用pthread_mutex_unlock解锁,以便其他线程可以获取锁并访问共享资源。

2.3 多线程的优势

多线程编程的一个显著优势是能够提高 CPU 利用率。在多核处理器的环境下,多线程可以将不同的任务分配到不同的 CPU 核心上并行处理,充分利用硬件资源,从而显著提高程序的整体计算效率。例如,在一个数据分析程序中,可能有一个线程负责从文件中读取数据,另一个线程对读取到的数据进行预处理,还有线程进行复杂的数据分析计算。这些线程可以在不同的 CPU 核心上同时执行,避免了单个线程在执行计算任务时其他 CPU 核心处于空闲状态的情况,大大加快了数据分析的速度。

对于 IO 密集型程序,如网络通信、文件读写等,多线程同样具有出色的优化效果。在这类程序中,大部分时间都花费在等待 IO 操作完成上,如网络请求等待服务器响应、文件读写等待磁盘操作完成等。使用多线程可以在一个线程等待 IO 操作时,让其他线程继续执行,从而充分利用 CPU 资源,提高程序的并发性能。以网络爬虫程序为例,当一个线程发送网络请求后,会进入等待响应的状态,此时 CPU 处于空闲状态,而多线程爬虫可以利用这个时间,让其他线程继续发送新的网络请求,从而大大提高了爬虫的效率,缩短了获取网页数据的时间。

三、多线程基础实战

3.1 线程创建与销毁

3.1.1 pthread_create 函数

pthread_create函数用于创建一个新的线程,是 POSIX 线程库中非常关键的函数。它的原型为:

c 复制代码
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数含义如下:

  • thread:这是一个指向pthread_t类型变量的指针,函数成功返回时,新创建线程的 ID 会被写入这个变量。每个线程都有唯一的 ID,就像每个人都有唯一的身份证号一样,通过这个 ID 可以对线程进行管理和操作,例如等待线程结束、取消线程等。
  • attr:用于设置线程的属性,如栈大小、调度策略、分离状态等。如果传入NULL,则表示使用默认属性。属性设置可以满足不同场景下对线程的特殊需求,比如在某些实时性要求较高的场景中,可以通过设置调度策略属性,让线程获得更高的优先级 。
  • start_routine:这是线程执行函数的指针,即新线程启动后会执行这个函数。该函数的返回值类型为void*,参数类型也为void*。它就像是线程的 "工作内容",线程启动后就会按照这个函数的逻辑开始执行任务。
  • arg:传递给start_routine函数的参数。如果需要传递多个参数,可以将这些参数封装在一个结构体中,然后传递该结构体的指针。

pthread_create函数的返回值用于表示函数执行的结果:如果函数成功创建线程,返回值为0;如果创建线程失败,会返回一个非零的错误码,不同的错误码对应不同的错误原因,例如EAGAIN表示系统资源不足,无法创建新线程;EINVAL表示传入的参数无效等。

3.1.2 pthread_join 函数

pthread_join函数的作用是等待指定线程结束,并回收该线程的资源。其函数原型为:

c 复制代码
int pthread_join(pthread_t thread, void **retval);

其中,thread参数指定要等待的线程 ID,通过这个 ID,函数能够准确地找到对应的线程并等待其完成任务。retval参数是一个指针,用于接收目标线程执行结束时返回的结果。如果不需要获取线程的返回值,可以将retval设置为NULL。

当一个线程调用pthread_join函数等待另一个线程结束时,调用线程会被阻塞,就像暂停了自己的工作,直到目标线程执行完毕。这种阻塞机制确保了线程之间的同步,避免了在目标线程还未完成时,调用线程就继续执行后续操作而导致的数据不一致或其他问题。同时,通过回收线程资源,pthread_join函数可以避免产生僵尸线程。僵尸线程就像程序中的 "垃圾",会占用系统资源,如果不及时回收,可能会导致系统资源的浪费,甚至影响整个程序的稳定性。

3.1.3 实战

下面是一个使用pthread_create创建 2 个线程的完整代码示例,每个线程分别执行不同任务并打印信息:

c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>

// 线程1执行的函数
void* taskA(void* arg) {
    printf("线程1:执行任务A\n");
    return NULL;
}

// 线程2执行的函数
void* taskB(void* arg) {
    printf("线程2:执行任务B\n");
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    int ret1, ret2;

    // 创建线程1
    ret1 = pthread_create(&tid1, NULL, taskA, NULL);
    if (ret1 != 0) {
        fprintf(stderr, "线程1创建失败: %d\n", ret1);
        return 1;
    }

    // 创建线程2
    ret2 = pthread_create(&tid2, NULL, taskB, NULL);
    if (ret2 != 0) {
        fprintf(stderr, "线程2创建失败: %d\n", ret2);
        pthread_cancel(tid1); // 取消线程1
        return 1;
    }

    // 等待线程1结束
    ret1 = pthread_join(tid1, NULL);
    if (ret1 != 0) {
        fprintf(stderr, "等待线程1失败: %d\n", ret1);
    }

    // 等待线程2结束
    ret2 = pthread_join(tid2, NULL);
    if (ret2 != 0) {
        fprintf(stderr, "等待线程2失败: %d\n", ret2);
    }

    return 0;
}

在这个示例中,我们首先定义了两个线程执行函数taskA和taskB,分别打印 "线程 1:执行任务 A" 和 "线程 2:执行任务 B"。在main函数中,使用pthread_create创建了两个线程tid1和tid2,并分别关联到taskA和taskB函数。创建线程后,通过pthread_join等待两个线程执行结束。

当我们多次运行这段代码时,会发现线程 1 和线程 2 的执行顺序并不固定。这是因为多线程是并发执行的,操作系统的线程调度器会根据一定的调度算法来决定每个线程在何时获得 CPU 时间片进行执行 。在不同的运行时刻,调度器的决策可能不同,所以线程的执行顺序会呈现出不确定性。这种不确定性是多线程编程的一个重要特点,也带来了线程同步和资源竞争等问题,需要我们在编程中加以注意和处理。

3.2 线程同步:互斥锁(解决资源竞争)

3.2.1 互斥锁原理

互斥锁是一种用于线程同步的机制,其核心作用是解决多线程环境下对共享资源的竞争问题。在多线程程序中,当多个线程同时访问和修改共享资源时,可能会导致数据不一致或其他错误。例如,多个线程同时对一个全局变量进行累加操作,由于线程执行的不确定性,可能会出现某个线程读取到的变量值不是最新的,从而导致累加结果错误。

互斥锁通过pthread_mutex_lock和pthread_mutex_unlock这两个函数来实现对共享资源的保护。当一个线程需要访问共享资源时,它首先调用pthread_mutex_lock函数尝试对互斥锁进行加锁。如果此时互斥锁处于未锁定状态,即没有其他线程持有该锁,那么调用线程将成功获取锁,并可以安全地访问共享资源;如果互斥锁已经被其他线程锁定,那么调用线程会被阻塞,进入等待状态,直到持有锁的线程调用pthread_mutex_unlock函数解锁后,该线程才有机会获取锁并访问共享资源。

这种机制确保了在同一时间内,只有一个线程能够进入被互斥锁保护的临界区,即访问共享资源的代码段,从而避免了多个线程同时访问共享资源时可能出现的竞争条件和数据不一致问题 。就好比一间教室只有一把钥匙,每个学生要进入教室学习(访问共享资源),必须先拿到钥匙(获取互斥锁),在学习结束后归还钥匙(释放互斥锁),这样就保证了同一时间只有一个学生在教室里学习,避免了混乱和冲突。

3.2.2 实战

下面是一个多线程累加全局变量的代码示例,展示不加锁时由于资源竞争导致的计算错误结果,以及添加互斥锁后验证计算结果的正确性:

c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>

#define THREAD_NUM 10
#define LOOP_COUNT 100000

int sum = 0;
pthread_mutex_t mutex;

// 线程执行的函数
void* add(void* arg) {
    int i;
    for (i = 0; i < LOOP_COUNT; i++) {
        // 不加锁时,直接访问共享资源sum
        // sum++;

        // 加锁
        pthread_mutex_lock(&mutex);
        sum++;
        // 解锁
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t threads[THREAD_NUM];
    int i, ret;

    // 初始化互斥锁
    ret = pthread_mutex_init(&mutex, NULL);
    if (ret != 0) {
        fprintf(stderr, "互斥锁初始化失败: %d\n", ret);
        return 1;
    }

    // 创建多个线程
    for (i = 0; i < THREAD_NUM; i++) {
        ret = pthread_create(&threads[i], NULL, add, NULL);
        if (ret != 0) {
            fprintf(stderr, "线程创建失败: %d\n", ret);
            return 1;
        }
    }

    // 等待所有线程结束
    for (i = 0; i < THREAD_NUM; i++) {
        ret = pthread_join(threads[i], NULL);
        if (ret != 0) {
            fprintf(stderr, "等待线程失败: %d\n", ret);
        }
    }

    // 销毁互斥锁
    ret = pthread_mutex_destroy(&mutex);
    if (ret != 0) {
        fprintf(stderr, "互斥锁销毁失败: %d\n", ret);
        return 1;
    }

    printf("累加结果: %d\n", sum);
    return 0;
}

在上述代码中,我们定义了一个全局变量sum,并创建了THREAD_NUM个线程,每个线程对sum进行LOOP_COUNT次累加操作。在不加锁的情况下,运行程序多次,会发现每次输出的sum结果并不总是等于THREAD_NUM * LOOP_COUNT,这是因为多个线程同时访问和修改sum时发生了资源竞争,导致计算结果错误。

当我们添加互斥锁后,每个线程在访问sum之前先调用pthread_mutex_lock加锁,访问结束后调用pthread_mutex_unlock解锁。这样,同一时间只有一个线程能够对sum进行操作,避免了资源竞争。多次运行加锁后的程序,可以验证计算结果始终等于THREAD_NUM * LOOP_COUNT,从而证明了互斥锁有效地解决了资源竞争问题。

3.2.3 死锁避免

死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种相互等待的现象。若无外力作用,这些线程都将无法继续推进。例如,线程 A 持有资源 1 并等待资源 2,而线程 B 持有资源 2 并等待资源 1,此时两个线程都在等待对方释放自己需要的资源,从而陷入死锁状态。

死锁的产生通常需要满足以下四个必要条件:

  • 互斥条件:资源不能被共享,只能被一个线程独占。例如,一把锁在同一时间只能被一个线程持有。
  • 占有并等待条件:线程已经占有了至少一个资源,但还在等待获取其他线程持有的资源。如线程 A 已经获取了锁 1,又试图获取锁 2。
  • 不可剥夺条件:资源一旦被某个线程获取,在该线程主动释放之前,其他线程不能强行剥夺。
  • 循环等待条件:存在一个线程等待资源的循环链,例如线程 A 等待线程 B 持有的资源,线程 B 等待线程 C 持有的资源,线程 C 又等待线程 A 持有的资源。

为了避免死锁,可以遵循 "按顺序加锁" 和 "及时解锁" 的原则。"按顺序加锁" 是指所有线程按照相同的顺序获取锁,这样可以打破循环等待条件。例如,所有线程都先获取锁 1,再获取锁 2,就不会出现线程 A 获取锁 1 后等待锁 2,而线程 B 获取锁 2 后等待锁 1 的死锁情况。"及时解锁" 则要求线程在使用完共享资源后,尽快调用pthread_mutex_unlock函数释放锁,避免长时间持有锁导致其他线程等待。

下面是一个简单的死锁场景示例:

c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

// 线程1执行的函数
void* thread1_func(void* arg) {
    pthread_mutex_lock(&mutex1);
    printf("线程1获取了mutex1\n");
    sleep(1);  // 模拟一些操作
    pthread_mutex_lock(&mutex2);
    printf("线程1获取了mutex2\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}

// 线程2执行的函数
void* thread2_func(void* arg) {
    pthread_mutex_lock(&mutex2);
    printf("线程2获取了mutex2\n");
    sleep(1);  // 模拟一些操作
    pthread_mutex_lock(&mutex1);
    printf("线程2获取了mutex1\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    int ret1, ret2;

    // 创建线程1
    ret1 = pthread_create(&tid1, NULL, thread1_func, NULL);
    if (ret1 != 0) {
        fprintf(stderr, "线程1创建失败: %d\n", ret1);
        return 1;
    }

    // 创建线程2
    ret2 = pthread_create(&tid2, NULL, thread2_func, NULL);
    if (ret2 != 0) {
        fprintf(stderr, "线程2创建失败: %d\n", ret2);
        pthread_cancel(tid1); // 取消线程1
        return 1;
    }

    // 等待线程1结束
    ret1 = pthread_join(tid1, NULL);
    if (ret1 != 0) {
        fprintf(stderr, "等待线程1失败: %d\n", ret1);
    }

    // 等待线程2结束
    ret2 = pthread_join(tid2, NULL);
    if (ret2 != 0) {
        fprintf(stderr, "等待线程2失败: %d\n", ret2);
    }

    return 0;
}

在这个示例中,线程 1 先获取mutex1,然后尝试获取mutex2;线程 2 先获取mutex2,然后尝试获取mutex1。由于两个线程获取锁的顺序不同,很容易导致死锁。如果按照 "按顺序加锁" 的原则,修改线程 2 的加锁顺序,让它也先获取mutex1,再获取mutex2,就可以避免死锁的发生 。同时,在实际编程中,要确保在合适的时机及时解锁,避免因忘记解锁而导致其他线程无法获取锁,进而引发死锁或性能问题。

四、多线程进阶实战

4.1 线程池基础

线程池是一种多线程处理技术,它预先创建多个线程并维护一个任务队列。当有任务到来时,线程池会从任务队列中取出任务,并分配给线程池中的某个线程去执行。任务执行完毕后,线程不会被销毁,而是返回线程池等待下一个任务。这种机制避免了频繁创建和销毁线程带来的开销,提高了系统的性能和响应速度。

在实际应用中,创建和销毁线程需要消耗一定的系统资源,包括内存、CPU 时间等。如果一个程序需要频繁地处理大量短时间任务,如网络服务器中处理大量客户端的连接请求和数据传输,每次任务到来时都创建新线程,任务结束后销毁线程,那么这些线程创建和销毁的开销可能会占据系统资源的很大一部分,导致系统性能下降。而线程池通过复用已创建的线程,使得线程在任务之间循环利用,大大减少了线程创建和销毁的次数,从而降低了系统资源的消耗,提高了系统的整体性能。

4.2 实战

以下是实现一个简易线程池(包含 3 个线程)的 C 语言代码示例,该线程池用于处理 10 个 "计算 1 - 100 累加和" 的任务:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define THREAD_POOL_SIZE 3
#define TASK_COUNT 10

// 任务结构体
typedef struct {
    int task_id;
} Task;

// 线程池结构体
typedef struct {
    pthread_t threads[THREAD_POOL_SIZE];
    Task* task_queue[TASK_COUNT];
    int queue_head;
    int queue_tail;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    int stop;
} ThreadPool;

// 线程执行函数
void* worker(void* arg) {
    ThreadPool* pool = (ThreadPool*)arg;
    while (1) {
        pthread_mutex_lock(&pool->mutex);
        // 等待任务到来或停止信号
        while (pool->queue_head == pool->queue_tail &&!pool->stop) {
            pthread_cond_wait(&pool->cond, &pool->mutex);
        }
        if (pool->stop && pool->queue_head == pool->queue_tail) {
            pthread_mutex_unlock(&pool->mutex);
            pthread_exit(NULL);
        }
        Task* task = pool->task_queue[pool->queue_head];
        pool->queue_head = (pool->queue_head + 1) % TASK_COUNT;
        pthread_mutex_unlock(&pool->mutex);

        // 执行任务
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        printf("任务 %d 结果: %d\n", task->task_id, sum);
        free(task);
    }
}

// 初始化线程池
void initThreadPool(ThreadPool* pool) {
    pool->queue_head = 0;
    pool->queue_tail = 0;
    pool->stop = 0;

    pthread_mutex_init(&pool->mutex, NULL);
    pthread_cond_init(&pool->cond, NULL);

    for (int i = 0; i < THREAD_POOL_SIZE; i++) {
        pthread_create(&pool->threads[i], NULL, worker, pool);
    }
}

// 添加任务到线程池
void addTask(ThreadPool* pool, int task_id) {
    Task* task = (Task*)malloc(sizeof(Task));
    task->task_id = task_id;

    pthread_mutex_lock(&pool->mutex);
    pool->task_queue[pool->queue_tail] = task;
    pool->queue_tail = (pool->queue_tail + 1) % TASK_COUNT;
    pthread_cond_signal(&pool->cond);
    pthread_mutex_unlock(&pool->mutex);
}

// 销毁线程池
void destroyThreadPool(ThreadPool* pool) {
    pthread_mutex_lock(&pool->mutex);
    pool->stop = 1;
    pthread_cond_broadcast(&pool->cond);
    pthread_mutex_unlock(&pool->mutex);

    for (int i = 0; i < THREAD_POOL_SIZE; i++) {
        pthread_join(pool->threads[i], NULL);
    }

    pthread_mutex_destroy(&pool->mutex);
    pthread_cond_destroy(&pool->cond);
}

int main() {
    ThreadPool pool;
    initThreadPool(&pool);

    // 添加10个任务
    for (int i = 1; i <= TASK_COUNT; i++) {
        addTask(&pool, i);
    }

    sleep(1);  // 等待任务执行完毕
    destroyThreadPool(&pool);

    return 0;
}

在上述代码中,ThreadPool结构体定义了线程池的相关信息,包括线程数组、任务队列、队列头和尾指针、互斥锁、条件变量以及停止标志。worker函数是线程执行的函数,它从任务队列中获取任务并执行。initThreadPool函数用于初始化线程池,创建线程并初始化相关资源。addTask函数将任务添加到任务队列中,并通过条件变量通知等待的线程有新任务到来。destroyThreadPool函数用于销毁线程池,向所有线程发送停止信号,等待线程结束,并释放相关资源 。

4.3 多线程调试

在多线程编程中,调试是一项至关重要的工作,因为多线程环境下的问题往往更加复杂和难以排查。pstack和htop是两个非常实用的工具,可以帮助我们定位多线程程序中的问题。

pstack是一个用于查看线程调用栈的工具。它通过调用gdb来打印进程运行时的函数调用栈信息。使用pstack的方法很简单,只需在命令行中输入pstack ,其中是要查看的进程 ID。例如,如果要查看进程 ID 为 1234 的线程调用栈,命令为pstack 1234。执行命令后,pstack会输出每个线程的调用栈信息,包括线程 ID、当前执行的函数以及函数调用的层级关系。通过分析这些信息,我们可以了解线程在执行过程中的状态,判断线程是否陷入死循环、函数调用是否正确等。比如,如果某个线程的调用栈中出现了多次相同函数的递归调用,且没有正常的返回路径,那么很可能存在递归深度过大导致栈溢出的问题。

htop是一个交互式的进程监控工具,它可以实时显示系统中各个进程和线程的状态信息,包括 CPU 占用率、内存使用情况等。使用htop监控线程 CPU 占用的步骤如下:首先,确保系统中安装了htop,如果未安装,可以使用包管理器进行安装,如在 Debian 或 Ubuntu 系统中使用sudo apt install htop,在 CentOS 或 RHEL 系统中使用sudo yum install htop。安装完成后,在命令行中输入htop启动工具。在htop界面中,默认情况下可能只显示进程信息。要查看线程信息,需要启用线程显示模式,可以按F2进入显示选项,勾选 "Tree view(树状视图)" 和 "Show custom thread names(显示线程名)";也可以直接按F5切换为树状视图,这样会展开进程下的线程。使用方向键上下移动可以定位目标进程,按F3或/输入进程名或 PID 进行搜索,按F4可以过滤进程。找到目标进程后,按+或-可以展开或折叠子线程。线程的 CPU 利用率会直接显示在 "CPU%" 列中。通过观察线程的 CPU 占用率,我们可以发现线程性能瓶颈或异常情况。如果某个线程的 CPU 占用率长时间接近 100%,可能表示该线程执行了大量的计算任务或者存在死循环等问题,需要进一步分析和优化。

五、总结

通过本文的学习,我们深入了解了 C 语言基于 POSIX 线程的多线程编程。从基础概念出发,明确了线程与进程的区别,掌握了 POSIX 线程库中核心函数的使用方法,认识到多线程在提高 CPU 利用率和优化 IO 密集型程序方面的显著优势。

在基础实战部分,我们学会了使用pthread_create和pthread_join函数进行线程的创建与销毁,通过互斥锁解决多线程环境下的资源竞争问题,并掌握了避免死锁的原则和方法。

在进阶实战中,我们探讨了线程池的工作原理,实现了一个简易线程池来处理多个任务,同时介绍了pstack和htop等多线程调试工具,帮助我们在开发过程中快速定位和解决问题。

多线程编程是一项强大而复杂的技术,在实际项目中,我们需要根据具体需求和场景,合理运用多线程技术,充分发挥其优势,同时要注意避免多线程带来的各种问题。希望读者能够将所学知识运用到实际项目中,不断实践和探索,进一步提升自己在多线程编程领域的能力。

相关推荐
再睡一夏就好7 小时前
【C++闯关笔记】使用红黑树简单模拟实现map与set
java·c语言·数据结构·c++·笔记·语法·1024程序员节
mifengxing7 小时前
力扣每日一题——接雨水
c语言·数据结构·算法·leetcode·动态规划·
小龙报8 小时前
《算法通关指南:数据结构和算法篇 --- 顺序表相关算法题》--- 询问学号,寄包柜,合并两个有序数组
c语言·开发语言·数据结构·c++·算法·学习方法·visual studio
序属秋秋秋8 小时前
《Linux系统编程之开发工具》【编译器 + 自动化构建器】
linux·运维·服务器·c语言·c++·自动化·编译器
71-38 小时前
C语言——函数声明、定义、调用
c语言·笔记·学习·其他
晨非辰10 小时前
《数据结构风云》递归算法:二叉树遍历的精髓实现
c语言·数据结构·c++·人工智能·算法·leetcode·面试
人邮异步社区13 小时前
推荐几本学习计算机语言的书
java·c语言·c++·python·学习·golang
A-code16 小时前
C/C++ 中 void* 深度解析:从概念到实战
c语言·开发语言·c++·经验分享·嵌入式
编程之路,妙趣横生20 小时前
详解C语言操作符
c语言