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_JOINABLE
或PTHREAD_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); // 主线程正常退出
}
解释
-
全局变量
sum
:int sum
用于存储累加结果,因为两个线程都需要访问它,所以将它定义为全局变量。
-
线程函数
add1
和add2
:add1()
负责累加从 0 到 4。add2()
负责累加从 5 到 9。- 每个线程函数完成后都调用
pthread_exit(NULL)
,表示线程正常结束。
-
主函数
main
:- 使用
pthread_create()
创建两个线程,分别执行add1
和add2
。 - 调用
pthread_join(ptid1, NULL)
和pthread_join(ptid2, NULL)
来等待两个线程完成。这就保证了在主线程打印sum
的时候,两个子线程的计算都已经完成。 - 最后调用
pthread_exit(NULL)
让主线程正常退出。
- 使用
输出分析
总结
-
如果
pthread_join()
被注释掉(如示例中所示):cppsum = 0
-
主线程没有等待子线程完成,直接打印
sum
的值,这时两个线程的计算还未完成,因此sum
还是初始化的值 0。 -
如果
pthread_join()
没有被注释掉:cppsum = 45
-
在调用
pthread_join()
后,主线程会等待两个子线程完成计算,这样sum
的值就是 0 到 9 的累加和,即 45。
通俗解释
在多线程编程中,如果主线程不等待子线程完成,就会直接继续执行剩余的代码,这时可能导致子线程的结果还没有计算出来,主线程就已经输出了错误的结果。因此,我们使用 pthread_join()
来实现"同步",也就是让主线程等到子线程完成工作之后再继续执行。
可以把它理解为:主线程是老板,子线程是两个员工。老板需要等两个员工把工作做完,然后再去汇总结果。如果老板不等员工完成,就直接看结果,那么结果肯定是不完整的。
使用 pthread_join()
的推荐
- 同步线程 :
pthread_join()
是最常用的线程同步方式之一,它确保线程按顺序完成,以避免数据错误。 - 分离和连接 :
- 分离线程(Detached Thread) :线程一旦启动,就不再受主线程的管理,其资源在结束时自动释放,无法再用
pthread_join()
等待。 - 连接线程(Joinable Thread) :可以使用
pthread_join()
等待其结束,手动管理线程的生命周期,常用于需要获得线程返回值的情况。
- 分离线程(Detached Thread) :线程一旦启动,就不再受主线程的管理,其资源在结束时自动释放,无法再用
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
,确保计数结果正确,线程之间的操作顺序得到了保证。
代码解释
-
全局变量
counter
:- 这个变量被多个线程共享,因此需要互斥锁来保护,避免数据竞争。
-
互斥锁的声明和初始化:
- 使用
pthread_mutex_t lock
声明互斥锁。 - 在主函数中,使用
pthread_mutex_init(&lock, NULL)
初始化互斥锁。
- 使用
-
线程函数
trythis
:- 线程首先调用
pthread_mutex_lock(&lock)
来获取锁。 - 当锁被获取后,线程可以安全地访问和修改
counter
。 - 在完成任务后,线程调用
pthread_mutex_unlock(&lock)
释放锁。
- 线程首先调用
-
主函数
main
:- 创建两个线程执行
trythis
函数。 - 使用
pthread_join()
等待两个线程完成。 - 最后销毁互斥锁,释放资源。
- 创建两个线程执行
输出分析
-
如果不使用互斥锁(如在代码中注释掉的部分):
cppJob 1 has started Job 2 has started Job 2 has finished Job 2 has finished
在没有互斥锁保护的情况下,多个线程会同时访问和修改
counter
,导致计数错误,输出结果不符合预期。 -
使用互斥锁:
cppJob 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
,从而保证了数据的一致性。