[OS] Pthread Join & Pthread Mutex

3. Pthread Join

pthread_join() 是用于实现线程同步的一种方法,它使得主线程(或调用线程)能够等待指定的子线程完成后再继续执行。它的函数声明如下:

cpp 复制代码
int pthread_join(pthread_t thread, void *value_ptr);
  • 参数

    • thread:需要等待的线程 ID。
    • value_ptr:可以用来获取目标线程的返回状态。如果线程通过 pthread_exit() 返回值,这个指针可以用来接收这个值。
  • 返回值

    • 成功时,pthread_join() 返回 0
    • 如果失败,则返回错误号。
  • 线程的属性

    • 线程在创建时可以被定义为"可连接的(joinable)"或"分离的(detached)"。
    • 如果一个线程是"分离的",它就不能被 pthread_join() 连接(即等待)。
    • 线程属性通过 PTHREAD_CREATE_JOINABLEPTHREAD_CREATE_DETACHED 设置。

示例代码

cpp 复制代码
#include <pthread.h>  // 包含 Pthread 库
#include <stdio.h>    // 标准输入输出库
#include <stdlib.h>   // 标准库
#include <unistd.h>   // 包含 sleep() 函数

int sum;  // 全局变量,用于累加结果

// 第一个线程函数,计算从 0 到 4 的累加和
void *add1(void *cnt) {
    for (int i = 0; i < 5; i++) {
        sum += i;
    }
    pthread_exit(NULL);  // 线程正常退出
    return 0;
}

// 第二个线程函数,计算从 5 到 9 的累加和
void *add2(void *cnt) {
    for (int i = 5; i < 10; i++) {
        sum += i;
    }
    pthread_exit(NULL);  // 线程正常退出
    return 0;
}

int main(int argc, char *argv[]) {
    pthread_t ptid1, ptid2;  // 声明两个线程
    sum = 0;  // 初始化 sum 为 0

    // 创建两个线程
    pthread_create(&ptid1, NULL, add1, &sum);
    pthread_create(&ptid2, NULL, add2, &sum);

    // 等待两个线程完成
    pthread_join(ptid1, NULL);
    pthread_join(ptid2, NULL);

    // 打印累加和
    printf("sum = %d\n", sum);

    pthread_exit(NULL);  // 主线程正常退出
}
解释
  1. 全局变量 sum

    • int sum 用于存储累加结果,因为两个线程都需要访问它,所以将它定义为全局变量。
  2. 线程函数 add1add2

    • add1() 负责累加从 0 到 4。
    • add2() 负责累加从 5 到 9。
    • 每个线程函数完成后都调用 pthread_exit(NULL),表示线程正常结束。
  3. 主函数 main

    • 使用 pthread_create() 创建两个线程,分别执行 add1add2
    • 调用 pthread_join(ptid1, NULL)pthread_join(ptid2, NULL) 来等待两个线程完成。这就保证了在主线程打印 sum 的时候,两个子线程的计算都已经完成。
    • 最后调用 pthread_exit(NULL) 让主线程正常退出。
输出分析

总结

  • 如果 pthread_join() 被注释掉(如示例中所示)

    cpp 复制代码
    sum = 0
  • 主线程没有等待子线程完成,直接打印 sum 的值,这时两个线程的计算还未完成,因此 sum 还是初始化的值 0。

  • 如果 pthread_join() 没有被注释掉

    cpp 复制代码
    sum = 45
  • 在调用 pthread_join() 后,主线程会等待两个子线程完成计算,这样 sum 的值就是 0 到 9 的累加和,即 45。

通俗解释

在多线程编程中,如果主线程不等待子线程完成,就会直接继续执行剩余的代码,这时可能导致子线程的结果还没有计算出来,主线程就已经输出了错误的结果。因此,我们使用 pthread_join() 来实现"同步",也就是让主线程等到子线程完成工作之后再继续执行。

可以把它理解为:主线程是老板,子线程是两个员工。老板需要等两个员工把工作做完,然后再去汇总结果。如果老板不等员工完成,就直接看结果,那么结果肯定是不完整的。

使用 pthread_join() 的推荐
  • 同步线程pthread_join() 是最常用的线程同步方式之一,它确保线程按顺序完成,以避免数据错误。
  • 分离和连接
    • 分离线程(Detached Thread) :线程一旦启动,就不再受主线程的管理,其资源在结束时自动释放,无法再用 pthread_join() 等待。
    • 连接线程(Joinable Thread) :可以使用 pthread_join() 等待其结束,手动管理线程的生命周期,常用于需要获得线程返回值的情况。
  • pthread_join() 用于等待指定线程结束,是实现多线程同步的有效方法。
  • 在需要确保所有子线程完成后再执行主线程的后续代码时,pthread_join() 是必不可少的。
  • 如果不使用 pthread_join() 而主线程直接退出,则所有正在执行的子线程将被强制终止,可能导致不完整的结果或数据错误。

4. Pthread Mutex

互斥锁(Mutex)用于实现线程同步,防止多个线程同时修改数据,造成数据不一致。

4.1. Pthread Mutex 的声明

互斥锁通过 pthread_mutex_t 类型来声明。

复制代码
pthread_mutex_t lock;  // 声明一个互斥锁
4.2. Pthread Mutex 的初始化

在使用互斥锁之前,需要对其进行初始化,通常使用 pthread_mutex_init() 函数。

复制代码
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • mutex:指向需要初始化的互斥锁的指针。

  • attr:互斥锁的属性,如果为 NULL,则使用默认属性。

    pthread_mutex_init(&lock, NULL); // 使用默认属性初始化互斥锁

4.3. Pthread Mutex 的销毁

在互斥锁不再需要使用时,应该将其销毁,以释放系统资源。

复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);

pthread_mutex_destroy(&lock);  // 销毁互斥锁
4.4. Pthread Mutex 的锁定与解锁
  • pthread_mutex_lock(pthread_mutex_t *mutex):用于锁定互斥锁。如果该锁已经被其他线程持有,则调用线程会阻塞,直到锁被释放。
  • pthread_mutex_trylock(pthread_mutex_t *mutex):尝试锁定互斥锁。如果锁已经被其他线程持有,该函数会立即返回一个"忙"错误代码,而不会阻塞调用线程。
  • pthread_mutex_unlock(pthread_mutex_t *mutex):用于解锁互斥锁,通常在线程完成对共享数据的访问后调用。如果锁未被当前线程持有,则会返回错误。
4.5 示例代码:使用 Mutex 来保护共享数据

下面是一个使用互斥锁来保护共享数据的例子。

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

int counter = 0;             // 全局变量,用于计数
pthread_mutex_t lock;        // 声明一个互斥锁

// 线程函数
void *trythis(void *arg) {
    pthread_mutex_lock(&lock);  // 锁定互斥锁,确保只有一个线程能进入这段代码

    unsigned long i = 0;
    counter += 1;
    printf("Job %d has started\n", counter);

    // 模拟一些计算任务
    for (i = 0; i < (0x0FFFFFFF); i++)
        ;

    printf("Job %d has finished\n", counter);

    pthread_mutex_unlock(&lock);  // 解锁互斥锁,其他线程可以继续访问共享资源
    return NULL;
}

int main(int argc, char *argv[]) {
    pthread_t tid[2];  // 声明两个线程
    int error;

    // 初始化互斥锁
    if (pthread_mutex_init(&lock, NULL) != 0) {
        printf("\nMutex init has failed\n");
        return 1;
    }

    // 创建两个线程
    for (int i = 0; i < 2; i++) {
        error = pthread_create(&(tid[i]), NULL, &trythis, NULL);
        if (error != 0) {
            printf("\nThread can't be created\n");
        }
    }

    // 等待两个线程完成
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);

    // 销毁互斥锁
    pthread_mutex_destroy(&lock);

    return 0;
}

在这个例子中,全局变量 counter 被多个线程共享,因此需要互斥锁来保护,避免数据竞争。线程函数 trythis 首先调用 pthread_mutex_lock(&lock) 来获取锁,然后安全地访问和修改 counter。在完成任务后,线程调用 pthread_mutex_unlock(&lock) 释放锁。主函数 main 创建两个线程执行 trythis 函数,使用 pthread_join() 等待两个线程完成,最后销毁互斥锁,释放资源。

如果不使用互斥锁,多个线程可能会同时访问和修改 counter,导致计数错误。使用互斥锁后,每次只有一个线程能访问 counter,确保计数结果正确,线程之间的操作顺序得到了保证。

代码解释
  1. 全局变量 counter

    • 这个变量被多个线程共享,因此需要互斥锁来保护,避免数据竞争。
  2. 互斥锁的声明和初始化

    • 使用 pthread_mutex_t lock 声明互斥锁。
    • 在主函数中,使用 pthread_mutex_init(&lock, NULL) 初始化互斥锁。
  3. 线程函数 trythis

    • 线程首先调用 pthread_mutex_lock(&lock) 来获取锁。
    • 当锁被获取后,线程可以安全地访问和修改 counter
    • 在完成任务后,线程调用 pthread_mutex_unlock(&lock) 释放锁。
  4. 主函数 main

    • 创建两个线程执行 trythis 函数。
    • 使用 pthread_join() 等待两个线程完成。
    • 最后销毁互斥锁,释放资源。
输出分析
  • 如果不使用互斥锁(如在代码中注释掉的部分):

    cpp 复制代码
    Job 1 has started
    Job 2 has started
    Job 2 has finished
    Job 2 has finished

    在没有互斥锁保护的情况下,多个线程会同时访问和修改 counter,导致计数错误,输出结果不符合预期。

  • 使用互斥锁

    cpp 复制代码
    Job 1 has started
    Job 1 has finished
    Job 2 has started
    Job 2 has finished
  • 在使用互斥锁后,每次只有一个线程能访问 counter,确保计数结果正确,线程之间的操作顺序得到了保证。

  • 互斥锁(Mutex) 用于保护共享资源,防止多个线程同时修改导致数据竞争。

  • 初始化和销毁 :使用 pthread_mutex_init() 来初始化互斥锁,用 pthread_mutex_destroy() 来销毁。

  • 锁定和解锁 :通过 pthread_mutex_lock()pthread_mutex_unlock() 来锁定和解锁互斥锁,确保线程安全访问共享数据。

通俗解释

互斥锁就像是一个门锁,只有持有钥匙(获得锁)的线程才能进入共享资源的房间(访问 counter)。如果其他线程也想访问共享资源,必须等待这个线程释放锁(开门)之后,才能进去。这样可以避免两个线程同时修改同一个变量导致的数据不一致问题。

在例子中,pthread_mutex_lock() 就是上锁,pthread_mutex_unlock() 就是开锁。通过互斥锁保护,确保每次只有一个线程能修改 counter,从而保证了数据的一致性。

相关推荐
学传打活2 天前
【边打字.边学昆仑正义文化】_进阶篇5 宇宙星系的运转(续1)
微信公众平台·1024程序员节·汉字·昆仑正义文化
fangzhanpeng16810 天前
快速掌握C#语言基础知识点(1.基础数据类型)
开发语言·c#·1024程序员节
无己心13 天前
面向多平台 AI 搜索引擎的适配层架构设计
前端·人工智能·搜索引擎·ai·1024程序员节
u01030552718 天前
AI驱动的安卓UI自动生成功能实现
人工智能·1024程序员节
u01030552719 天前
长株潭智能体赋能腾讯元器链接共享
1024程序员节
云贝贝贝20 天前
OceanBase认证体系介绍与备考指南
运维·服务器·oceanbase·1024程序员节
u01030552720 天前
Java图像处理实战指南
人工智能·1024程序员节
u01030552721 天前
Java AWT鼠标事件全解析
人工智能·1024程序员节
u01030552723 天前
Java实用类应用精讲
人工智能·1024程序员节
u01030552724 天前
Java I/O核心操作实战
人工智能·1024程序员节