目录
一,C++线程与系统线程
二,分离线程
三,线程结构
四,__thread关键字
五,Linux线程互斥
1,线程互斥相关的背景概念
2,互斥锁
3,死锁
4,互斥锁的弊端
六,线程同步
1,线程互斥与线程同步的区别
2,条件变量
3,生产者消费者模型
一,C++线程与系统线程
Linux中的原生线程库(pthread库)是操作系统级别的调用,提供了较为底层的线程控制接口。C++的多线程是对底层原生线程的封装,其底层调用的是操作系统提供的线程。C++将其底层封装,提供了更高层次的抽象和更便捷的接口,使其在不同平台上对应不同的动静态库,实现跨平台。
注意:C++多线程底层由于封装了Linux系统的原生线程,其底层调用的还是操作系统所提供的线程,所以当g++编译时,必须使用 -lpthread 选项说明系统要使用原生线程库,若编译C++线程程序没有链接系统库,那么将会出现链接错误,如下。
二,分离线程
这里先来认识两种线程分离状态——可连接状态和分离状态。在多线程编程中,线程可以被创建为分离状态或可连接状态。
可连接状态:这是线程默认的分离状态。在这种状态下,线程的终止状态(返回值和退出码)必须被其他线程通过 pthread_join
回收。如果线程终止时,没有任何线程调用 pthread_join
来回收它,那么它的资源(如栈内存)将不会被自动释放,可能会导致资源泄漏。
分离状态:在这种状态下,线程的终止状态不会被保存,线程终止时系统会自动回收其资源。因此,对于分离状态的线程,不需要也不能调用 pthread_join
,因为线程与其它线程分离。
下面来分析下 pthread_join
函数,此函数用于回收指定线程,但它有一个缺点,当线程调用此函数时必须要阻塞等待指定线程结束时才能继续往下运行,若我们想让线程各自运行各自的,这里就需要让线程进入分离状态。
pthread_detach
是原生线程(pthread)库中的一个函数,用于改变线程的分离状态,将线程从可连接状态设置为分离状态。
头文件:
#include <pthread.h>
格式:
int pthread_detach(pthread_t thread);
参数说明:
thread
:分离线程的ID。返回值:
成功时返回
0;
失败时返回一个非零的错误码。
注意:线程进入分离状态后,它们之间仍然共享进程资源。要明白,线程的分离状态主要影响线程的终止和资源回收方式,指其终止后的资源清理工作将由系统自动完成,无需程序员显式干预,并不是将线程完全从进程中分离,进程仍是一个进程,一个线程出异常了程序照样崩溃。也就是说,无论线程处于何种分离状态,它们之间的进程资源共享特性都不会改变,线程的其它性质照样也不变。
线程的分离状态既然不是进程分离,那么这就意味着主线程(main函数)一旦退出,进程资源全部被回收,其它线程无论是否处于分离状态照样会被强行终止,所以,无论线程是否分离,一般情况下要保证主线程最后一个退出,常规操作是使主线程进入死循环,需要时创建线程来完成对应的任务,因为很多情况下难以保证主线程最后一个退出。
三,线程结构
每创建一个线程,系统地址空间内部都会创建对应的一个线程控制块tcb(tcb很少听说是因为系统不提供tcb,而CUP将其看作执行流,也就是说系统中没有线程概念,只有轻量级进程),而线程控制块结构的起始地址其实就是线程的PID。
从上图中可看出,线程具有独立栈结构,下面来进行代码验证。
#include <iostream>
#include <string>
#include <pthread.h>
using namespace std;
void* threadrun(void *args)
{
string name = static_cast<const char*>(args);
//设置局部变量证明线程栈结构是独立的
//注意:不能使用全局变量,因为全局变量没有存储在栈结构中,在全局区中,数据是共享的
int g_val = 100;
while(true)
{
sleep(1);
cout << name << endl;
cout << "g_val: " << g_val << " " << "&g_val: " << &g_val << endl;
cout << endl;
}
return nullptr;
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1, nullptr, threadrun, (void*)"new thread1");
pthread_create(&tid2, nullptr, threadrun, (void*)"new thread2");
//分离新线程
pthread_detach(tid1);
pthread_detach(tid2);
sleep(3);
return 0;
}
上面代码说明,同一个局部变量输出的地址不一样,证明了线程的栈结构是各自独立的。
四,__thread
关键字
在Linux系统中,__thread
是一个GCC特定的关键字,用于声明线程局部存储变量。这些变量在每个线程中都有自己独立的实例,这意味着每个线程可以独立地修改其副本,而不会影响到其他线程中的值。
从根本上将,__thread
关键字相当于把一个变量的值拷贝了几份,放入每个线程TCB的线程局部存储中,也就是说这个关键字可使每个线程拥有自己的一份独立数据。最后需说明下,__thread
关键字只能局部存储内置类型以及那些只包含内置类型成员,且无自定义构造、拷贝、赋值、析构函数的结构体。
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
__thread int tid = 1;
void *threadrun1(void *args)
{
while (true)
{
cout << "new thread1, tid: " << tid << " " << "&tid: " << &tid << endl;
tid++;
sleep(1);
}
return nullptr;
}
void *threadrun2(void *args)
{
while (true)
{
sleep(1);
cout << "new thread2, tid: " << tid << " " << "&tid: " << &tid << endl;
tid++;
}
return nullptr;
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1, nullptr, threadrun1, nullptr);
pthread_create(&tid2, nullptr, threadrun2, nullptr);
pthread_detach(tid1);
pthread_detach(tid2);
for (int i = 0; i < 2; i++)
{
sleep(2);
cout << "main thread, tid: " << tid << " " << "&tid: " << &tid << endl;
}
sleep(2);
return 0;
}
上面代码的全局变量tid 经__thread
修饰后可发现,三个线程(两个新建线程和一个主线程)输出的数值及地址不同,这足以说明tid在每个线程内部都存储了一份,它们之间互不影响。
五,Linux线程互斥
1,线程互斥相关的背景概念
临界资源:多线程执行流共享的资源就叫做临界资源。
临界区:每个线程内部访问临界资源的代码。
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个 线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互,而多个线程并发的操作共享变量,会带来数据不一致或发生竞争关系等问题。
Linux线程互斥是确保在多线程环境中,某一资源同时只允许一个访问者(线程或进程)对其访问的机制,以防止多个线程同时访问共享资源,从而避免数据竞争和不一致的问题。下面来说明线程互斥中一种机制——互斥锁。
2,互斥锁
定义:互斥锁(也叫互斥量)是用一种简单的加锁方法来控制对资源的共享访问。它是同步原语,允许多个线程协调工作,确保在任何时刻只有一个线程可以访问临界区或共享资源,防止多个线程同时尝试访问相同资源时发生冲突和数据损坏,保证线程安全。可以把互斥锁看成某种意义的全局变量,在同一时刻只能有一个线程掌握这个互斥锁。其中,互斥锁有两种状态——上锁和解锁。
上锁:上锁操作是指一个线程获取互斥锁的控制权,从而阻止其他线程同时访问受保护的共享资源,一般会在临界区上锁且上锁的代码粒度要越细越好(只在临界区上加锁)。
注意:如果互斥锁当前未被任何线程持有(即处于解锁状态),则调用上锁线程会成功获取锁,并继续执行后续代码;如果锁已被其他线程持有,则调用上锁线程会被阻塞,直到锁被释放(即其他线程执行了相应的解锁操作)为止。原生线程库中pthread_mutex_lock
函数用来上锁。
解锁:解锁操作是指一个线程释放之前获取的互斥锁,从而允许其他被阻塞的线程访问受保护的共享资源。原生线程库中pthread_mutex_unlock
函数用来上锁。
注意:如果解锁的线程是当前持有互斥锁的线程,则锁会被释放,并且之前因尝试获取该锁而被阻塞的线程之一(如果有的话)将有机会获取锁;如果解锁线程未持有锁,则行为是未定义的,通常会导致程序崩溃或产生不可预测的结果。原生线程库中pthread_mutex_unlock
函数用来解锁。
互斥锁类型:pthread_mutex_t是原生线程库中的互斥锁(mutex)类型(本质是一个联合体)。
初始化互斥锁:一般由主线程来初始化一个互斥锁,初始化时有动态和静态两种方式。重点说明下,使用 PTHREAD_MUTEX_INITIALIZER
初始化(静态初始化)的互斥量不需要使用函数销毁。
- 动态初始化:使用
pthread_mutex_init
函数初始化。这是最常见和灵活的方式,允许在运行时根据需要初始化互斥锁。 - 静态初始化:使用
PTHREAD_MUTEX_INITIALIZER
宏,即pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
。这种方式在编译时初始化互斥锁,适用于全局或静态互斥锁的初始化。
销毁互斥锁:使用pthread_mutex_destroy
函数。销毁之前必须确保锁已经被释放,即没有被任何线程持有。
函数说明:互斥锁中常用的四大函数说明如下:
头文件:
#include <pthread.h>
格式和相关说明:
//用于初始化互斥锁的函数(下面代码演示会全部说明)
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
//上锁操作,即线程获取互斥锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//解锁操作,即线程取消对锁的持有权
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//用于销毁一个互斥锁,释放相关资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数说明:
mutex
:指向对应功能(初始化、上锁、解锁、销毁)互斥锁的指针。attr:指向互斥锁属性的指针,如果传递
NULL
,则使用默认的互斥锁属性。返回值:
成功时返回
0
。失败时返回一个非零的错误码。
注意:首先要说明的是一个持有锁且正在访问临界区的线程是可以被操作系统(OS)切换调度的,这是,其它正在被锁阻塞的线程将继续阻塞,不会出现任何安全问题。其次,互斥锁的作用是用来保护临界区的,非临界区最好不要使用。互斥锁运用的区域对应如下图。
代码示例请进入此链接查看:Linux线程互斥锁代码示例。程序运行图如下:
3,死锁
死锁涉及到多线程或多进程在竞争资源时而处于的一种永久等待状态。死锁是指两个或两个以上的进程(或线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。此时,若无外力作用,这些进程(或线程)都将无法继续执行下去,系统处于死锁状态。这些永远在互相等待的进程(或线程)被称为死锁进程。
死锁的发生通常需要满足以下四个必要条件,也被称为死锁的四个基本条件:
- 互斥条件:至少有一个资源必须是非共享的,即一次只能被一个进程(或线程)使用。如果其他进程(或线程)请求该资源,则请求者必须等待,直到资源被释放。
- 占有并等待条件:一个进程(或线程)已经占有了至少一个资源,并正在等待获取另一个资源,而该资源被其他进程(或线程)所占有。即该进程(或线程)在保持已有资源的同时,又在等待其他资源。
- 不可抢占条件:资源不能被抢占,即资源只能由持有它的进程(或线程)显式地释放。即使该进程(或线程)当前不再需要该资源,它也不会被其他进程(或线程)抢占。
- 循环等待条件:存在一个进程(或线程)等待链,其中每个进程(或线程)都在等待链中下一个进程(或线程)持有的资源,而最后一个进程(或线程)又在等待第一个进程(或线程)持有的资源。这样形成了一个闭环等待链。
为了避免死锁,通常,我们会破坏死锁的四个必要条件或进行死锁检测。下面用代码来模拟死锁情况。
#include <iostream>
#include <pthread.h>
using namespace std;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int num = 1;
void* Thread(void* arg)
{
pthread_mutex_lock(&lock);
cout << "Thread enter" << endl;
if (num)
{
pthread_exit(nullptr); //没有释放互斥锁直接退出,导致死锁
}
return nullptr;
}
int main()
{
pthread_t thread[5];
for (int i = 0; i < 5; i++)
{
pthread_create(&thread[i], nullptr, Thread, nullptr);
}
for (int i = 0; i < 5; i++)
{
pthread_join(thread[i], nullptr);
}
return 0;
}
4,互斥锁的弊端
1,增加竞态条件的风险
竞态条件是指多个线程在没有正确同步的情况下同时访问共享资源,导致不可预测的行为,虽然互斥锁本身是为了避免竞态条件而设计的,但线程竞争锁是自由的,导致有些情况下无法控制它们之间的执行顺序,增加竞态条件的风险。
2,可能导致线程饥饿
线程饥饿是指某些线程长时间无法获得执行机会的情况。当一个线程持续地获取互斥锁,而其他线程无法获得锁时,就可能导致其他线程饥饿。这意味着某些线程可能长时间无法访问共享资源,从而降低了整体的公平性。
3,占用CPU缓存和内存空间
互斥锁对象的结构通常较大,会占用更多的CPU缓存和内存空间,从而大大增加系统的开销
4,可能导致死锁
当两个或多个线程相互等待对方释放锁时,会导致死锁。死锁是一种严重的并发问题,不仅资源浪费,还会使整块程序性能下降,使其无法继续执行,严重时可能导致系统崩溃。
综上所述,互斥锁虽然能够有效地保护共享资源和避免数据竞争,但也存在一些弊端。为了克服这些弊端,开发者往往需要结合同步机制(如条件变量等)。
六,线程同步
1,线程互斥与线程同步的区别
线程互斥和线程同步是两个不同的概念,尽管在某些情况下可能会用到相同的机制。
线程互斥:
线程互斥主要用于防止多个线程同时访问共享资源(如全局变量、数据结构等),以避免数据竞争和数据不一致的问题,但是线程互斥并不能保证线程执行顺序。线程互斥的典型实现机制就是互斥锁(Mutex)。
线程同步:
线程同步则是一个更广泛的概念,它涵盖了多个线程之间如何协调它们的执行顺序,以确保程序能够按照预期的方式运行。线程同步不仅关注于避免数据竞争,还关注于确保线程之间的正确协作,以实现复杂的并发控制逻辑。
总的来说线程互斥只保证线程安全,而线程互斥在保证安全的基础上又确保了各个线程之间的执行顺序。
2,条件变量
条件变量是多线程编程中一种重要的线程同步机制。它允许一个或多个线程在某个条件满足时进行等待,并在条件满足时被唤醒,以此协调它们的执行顺序,即当一个线程的行为依赖于另外一个线程对共享数据状态的改变时,就可以使用条件变量。条件变量还通常与互斥锁(Mutex)结合使用,以确保线程安全。
条件变量与互斥锁互补,它们之间的运用极为相似。其中,pthread_cond_t
是原生线程库中条件变量的类型,而条件变量的常用函数接口和相关说明如下(可参考互斥锁的类型和函数接口):
头文件:
#include <pthread.h>
函数格式与说明:
//初始化条件变量。条件变量与互斥锁一样,有静态初始化和动态初始化。
静态初始化:使用
PTHREAD_COND_INITIALIZER
宏,即:pthread_cond_t cond = PTHREAD_COND_INITIALIZER。这种方式在编译时初始化条件变量适用于全局或静态 条件变量的初始化,且这种方式与互斥锁一样,不需要调用pthread_cond_destroy
函数 来销毁条件变量,系统会自动进行管理。动态初始化:使用
pthread_cond_init
函数。这是最常见和灵活的方式,允许在运行时 根据需要初始化条件变量。这种方式需要调用pthread_cond_destroy
函数来销毁它,以 避免资源泄漏。具体函数形式如下:int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
//销毁条件变量
注意,只有在没有任何线程在等待该条件变量,且没有任何线程会尝试使用该条件变量 时,才能安全地销毁它。
int pthread_cond_destroy(pthread_cond_t *cond);
//实现了线程的等待和阻塞功能,直到与条件变量相关联的条件满足。
该函数使当前线程等待条件变量
cond
对应的条件满足。在等待期间,线程将被阻塞,无 法继续执行。通过调用pthread_cond_signal
或pthread_cond_broadcast
函数向条件 变量发送信号通知,使其被唤醒。注意:调用线程必须在调用
pthread_cond_wait
之前已经获得这个互斥锁。在调 用pthread_cond_wait
时,该函数会自动释放这个互斥锁(此时,其他卡 在pthread_mutex_lock
上的线程是有机会申请并获得这个互斥锁的,即其他线程能够获 得锁访问受保护的共享资源),并且在条件变量被触发时重新获得它。int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
//用于唤醒(pthread_cond_wait使其休眠)所有等待在特定条件变量
cond
上的线程当某个线程调用此函数时,它会通知所有等待在该条件变量上的线程,使它们从等待状态 转换为可运行状态。这些线程可以继续执行它们的任务,但具体的执行顺序由操作系统决 定。
int pthread_cond_broadcast(pthread_cond_t *cond);
//用于唤醒等待在特定条件变量
cond
上的至少一个线程(如果有多个线程在等待,则唤醒 哪一个是不确定的,由调度策略决定)int pthread_cond_signal(pthread_cond_t *cond);
参数说明:
cond:指向对应功能(初始化、销毁、阻塞等待、唤醒)条件变量的指针。
attr:指向条件变量属性的指针。如果设置为
NULL
,则使用默认属性。
mutex
:指向互斥锁的指针。互斥锁用于保护共享数据,一般会与条件变量一起使用。返回值:
成功时返回
0;
出错时返回错误码。
注意:虽然多线程是一个一个创建的,但多处理器处理多个线程时,决定谁先被处理我们无法预测,这里强调的是不能以为创建线程的顺序就是线程被调度执行的顺序,调度顺序由操作系统的调度策略决定。单个线程直接被处理。
代码运用示例一(函数接口的运用):
#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;
const int threadnum = 5;
int wake = 1;
// 静态初始化条件变量和互斥锁
//pthread_cond_t gcond = PTHREAD_COND_INITIALIZER; // 条件变量
//pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER; // 互斥锁
pthread_cond_t gcond;
pthread_mutex_t gmutex;
void *thread_func(void *args)
{
string name = static_cast<const char *>(args);
// 1. 加锁。一个线程持有锁后其它线程都会在此被阻塞等待
pthread_mutex_lock(&gmutex);
// 2. 使用条件变量(一般条件变量是在加锁和解锁之间使用的)
if (wake)
{
pthread_cond_wait(&gcond, &gmutex);
}
cout << "当前被叫醒的线程是: " << name << endl;
// 3. 解锁
pthread_mutex_unlock(&gmutex);
return nullptr;
}
int main()
{
// 动态初始化条件变量和互斥锁
pthread_mutex_init(&gmutex, nullptr);
pthread_cond_init(&gcond, nullptr);
//创建一批线程
vector<pthread_t> tids;
for (int i = 0; i < threadnum; i++)
{
char *name = new char[64];
snprintf(name, 64, "thread-%d", i + 1);
pthread_t tid;
int n = pthread_create(&tid, nullptr, thread_func, name);
if (n == 0)
{
cout << "create success: " << name << endl;
tids.emplace_back(tid);
}
// sleep休眠是为了保证线程调度的顺序按照线程创建的顺序进行
sleep(1);
}
// 依次唤醒一个线程
for (int i = 0; i < threadnum; i++)
{
// 唤醒一个线程(唤醒是随机的)
pthread_cond_signal(&gcond);
cout << "唤醒线程" << endl;
sleep(1);
}
// 唤醒所有的线程
// pthread_cond_broadcast(&gcond);
// wake = 0;
// 释放线程资源
for (auto &tid : tids)
{
pthread_join(tid, nullptr);
}
// 动态初始化条件变量和互斥锁时需使用下面函数销毁
pthread_mutex_destroy(&gmutex);
pthread_cond_destroy(&gcond);
return 0;
}
依次唤醒一个线程(pthread_cond_signal)的运行图:
一次唤醒全部线程(pthread_cond_broadcast)的运行图:
代码运用示例二(pthread_cond_wait函数
释放持有的互斥锁):#include <iostream>
#include <iostream>
#include <pthread.h>
using namespace std;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* Thread(void* arg)
{
char* name = static_cast<char*>(arg);
pthread_mutex_lock(&lock);
cout << "Thread enter: " << name << endl;
pthread_cond_wait(&cond, &lock);
cout << "Thread wait end: " << name << endl;
pthread_mutex_unlock(&lock);
return nullptr;
}
int main()
{
pthread_t thread[5];
for (int i = 0; i < 5; i++)
{
string name = to_string(i + 1);
pthread_create(&thread[i], nullptr, Thread, (void*)name.c_str());
}
pthread_cond_signal(&cond);
for (int i = 0; i < 5; i++)
{
pthread_join(thread[i], nullptr);
}
return 0;
}
3,生产者消费者模型
生产者-消费者模型:这是线程同步中的一个经典案例。生产者线程负责生成数据并将其放入共享缓冲区(通常是一个队列),而消费者线程则从缓冲区中取出数据进行处理。通过使用条件变量,可以确保生产者和消费者线程在适当的时间进行交互,避免缓冲区溢出或空闲等待的情况。此模型的设计是用于描述多线程编程中协作关系的经典模型,具有解耦、支持并发和忙闲不均等特点,实现高效、可靠的生产消费系统。
注意:生产者负责生产数据,显然它们之间存在数据供应的竞争,因此,它们之间通常要设计为互斥关系(生产复杂的物品可能具有同步关系,这里不研究同步关系);消费者负责消费数据,它们之间存在数据的争夺,因此,它们之间通常也要设计为互斥关系(复杂场景也有同步关系,这里不做研究);生产者与消费者之间的工作是协同的,它们之间是同步关系。
下面基于阻塞队列BlockQueue为共享缓冲区来简单实现生产消费者模型。具体说明如下:
1,首先,要设计一种阻塞队列。在多线程编程中,阻塞队列是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出。
2、阻塞队列是会被生产者和消费者同时访问的临界资源,因此,我们需要使用一把互斥锁 _mutex 将其保护起来。
3、生产者与生产者之间是互斥关系,这里需要使用互斥锁,而生产者线程向阻塞队列当中Push数据时,前提是阻塞队列里面有有可存放数据的空间,若阻塞队列已经满了,那么此时该生产者线程就需要进行等待,直到阻塞队列中有空间时再将其唤醒。
4、消费者与消费者之间是互斥关系,这里也需要使用互斥锁,而消费者线程从阻塞队列当中Pop数据时,前提是阻塞队列里面有数据,若阻塞队列为空,那么此时该消费者线程就需要进行等待,直到阻塞队列中有新的数据时再将其唤醒。
5、生产者与消费者之间是同步关系,这里我们需要用到两个条件变量,一个条件变量 _product 专门给生产者提供,另一个条件变量 _comsume 专门给消费者提供。当阻塞队列满了的时候,要进行生产的生产者线程就应该在 _product 条件变量下进行等待,直到消费线程消费数据后,使用pthread_cond_signal(&_product)将其唤醒(当消费者消费完一个数据后,意味着阻塞队列当中至少留有一个空间单元);当阻塞队列为空的时候,要进行消费的消费者线程就应该在 _comsume 条件变量下进行等待,直到生产线程生产数据后,使用pthread_cond_signal(&_comsume)将其唤醒(当生产者生产完一个数据后,意味着阻塞队列当中至少有一个数据)。
6、不论是生产者线程还是消费者线程,它们都是先申请到锁进入临界区后再判断是否满足生产或消费条件的。当对应的条件不满足时,对应的线程就会被挂起,其它线程照样在阻塞等待互斥锁;当对应的条件满足时,对应的线程将执行后续功能,直到该线程释放互斥锁,而拿到锁的线程将不断轮流按照上面的思路执行。
7,注意:生产线程与消费线程使用的是一个互斥锁,这就可能会导致生产者影响消费者或消费者影响生产者,还有,pthread_cond_wait等待时会自动释放互斥锁,因此,在多线程中,生产者线程或消费者线程可能会全部阻塞在pthread_cond_wait中,而不论是生产者线程还是消费者线程,一般情况下它们都是pthread_mutex_lock先申请到锁后才进入的临界区,若所有线程在pthread_cond_wait阻塞等待,那么当线程被唤醒时就可能已经处于临界区了,所以一般情况下最好把pthread_cond_wait阻塞等待设计为循环条件等待(如while)。比如,pthread_cond_wait等待的消费者线程可能有两个,但此时却只有一个生产者线程生产了一个数据,解锁后,一个消费者线程竞争到锁后消费完数据,此时若另一个消费者线程也竞争到锁了且下面的代码无消费条件判断,那么这个消费者线程将继续进行消费,此时就出问题了。说白了,这种情况的根本原因还是因为唤醒的线程是不确定的,使其可能出现消费者线程唤醒消费者线程,生产者线程唤醒生产者线程,因此在使用pthread_cond_wait时还需要将临界区保护起来。条件变量的使用规范如以下:
等待条件代码:
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发送信号代码:
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
这里再来设计一个任务类Task。其中,负责生产计算的线程向阻塞队列中Push任务,而负责消费的线程则从阻塞队列中Pop获取任务进行对应功能的计算。
总程序分为三大步:主函数实现生产消费线程、Task类实现数据计算功能、BlockQueue阻塞队列实现共享缓冲区。代码请在此链接下观看:Linux生产消费者模型