[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,从而保证了数据的一致性。

相关推荐
希忘auto3 天前
详解Redis的常用命令
redis·1024程序员节
yaosheng_VALVE3 天前
探究全金属硬密封蝶阀的奥秘-耀圣控制
运维·eclipse·自动化·pyqt·1024程序员节
dami_king3 天前
SSH特性|组成|SSH是什么?
运维·ssh·1024程序员节
一个通信老学姐8 天前
专业125+总分400+南京理工大学818考研经验南理工电子信息与通信工程,真题,大纲,参考书。
考研·信息与通信·信号处理·1024程序员节
sheng12345678rui8 天前
mfc140.dll文件缺失的修复方法分享,全面分析mfc140.dll的几种解决方法
游戏·电脑·dll文件·dll修复工具·1024程序员节
huipeng9269 天前
第十章 类和对象(二)
java·开发语言·学习·1024程序员节
earthzhang20219 天前
《深入浅出HTTPS》读书笔记(19):密钥
开发语言·网络协议·算法·https·1024程序员节
爱吃生蚝的于勒10 天前
计算机基础 原码反码补码问题
经验分享·笔记·计算机网络·其他·1024程序员节
earthzhang202110 天前
《深入浅出HTTPS》读书笔记(20):口令和PEB算法
开发语言·网络协议·算法·https·1024程序员节
一个通信老学姐10 天前
专业140+总分410+浙江大学842信号系统与数字电路考研经验浙大电子信息与通信工程,真题,大纲,参考书。
考研·信息与通信·信号处理·1024程序员节