C/C++ pthread线程库使用示例详解
目录
在进入代码实践之前,我们应该搞清楚。
线程是成语的最小执行单位,进程是操作系统中最小的资源分配单位。
这样的话我们可以理解以下两点:
关于线程个数的确定:
文件IO操作:文件IO对CPU是使用率不高, 因此可以分时复用CPU时间片, 线程的个数 = 2 * CPU核心数 (效率最高)处理复杂的算法(主要是CPU进行运算, 压力大),线程的个数 = CPU的核心数 (效率最高)
1.线程创建
#include
int pthread_create(
pthread_t *thread
, const pthread_attr_t *attr
, void *(*start_routine) (void *)
, void *arg);
我们主要用到的就是第一个和第三个、第四个参数。
代码练习
#include
#include
#include
void* working(void* arg) {
std::cout << "子线程" << pthread_self() << std::endl;
for (int i = 0; i < 3; i++) {
std::cout << "chiled say: " << i << std::endl;
}
}
int main () {
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
sleep(1); //为啥这里一定要睡一会儿?
std::cout << "parent say:" << tid << std::endl;
return 0;
}
//输出:
子线程140470444414528
chiled say: 0
chiled say: 1
chiled say: 2
parent say:140470444414528
为什么主线程要sleep(1)呢?
因为主线程和子线程都是在抢CPU时间片,谁抢到谁干活,所以完全有可能子线程还没有抢到资源,主线程结束,那么整个进程就结束了,子线程根本就来不及干活。
我们这里也可以使用信号量,等子线程执行结束了,通知主线程,这里就涉及到线程间通信,后面会进行详细讲解。
2.线程退出
#include
void pthread_exit(void *retval);
参数表示线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为NULL(这是重点,因为我们C++中的没有这个功能)
主线程可以调用退出函数退出,但是地址空间不会被释放。
子线程调用退出函数退出,一般目的是带出一些有价值的数据。
主线程调用退出函数
#include
#include
#include
void* child_thread(void* arg) {
sleep(1);
printf("Child thread is running.
");
// 子线程执行一些工作
pthread_exit(NULL); // 正常退出子线程
}
int main() {
pthread_t tid;
// 创建子线程
if (pthread_create(&tid, NULL, child_thread, NULL) != 0) {
perror("Failed to create thread");
return 1;
}
// 主线程立即退出,子线程继续运行
printf("Main thread is exiting.
");
pthread_exit(NULL);
return 0; // 这行代码不会执行,因为主线程已经退出
}
在这里我们可以发现主线程在创建子线程后立即退出,而子线程在继续执行。
但是我们一般不会这样调用函数,因为一般认为主线程的退出就代表程序执行结束。
要注意的是:
即使主线程通过调用 pthread_exit 退出,子线程也不会变成新的主线程。在 POSIX 线程(pthread)模型中,当主线程退出时,它创建的所有子线程仍然继续执行,直到它们自己结束或被其他线程终止。
子线程调用退出函数
如果子线程退出想往外面传递什么参数,也是配合一起使用,它的作用是等待子线程结束,并且获取返回状态:
#include
#include
#include
void* child_thread(void* arg) {
int* data = (int*)arg;
printf("Child thread is processing data.
");
// 模拟计算
*data = 42;
pthread_exit(data); // 子线程结束,并返回数据指针
}
int main() {
pthread_t tid;
int result;
// 分配内存用于存储子线程的结果,该数据位于堆上
int* data = (int*)malloc(sizeof(int));
// 创建子线程
pthread_create(&tid, NULL, child_thread, data);
//主线程在干自己的任务,把修改data数据的任务交给了子线程
// 等待子线程结束,并获取返回状态
pthread_join(tid, (void**)&data);
// 检查子线程的返回值
if (data != NULL) {
printf("Child thread returned: %d
", *data);
free(data);
} else {
printf("Child thread failed to return data.
");
free(data);
}
return 0;
}
3.线程回收
在刚才我们已经初步认识了线程回收函数:,这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。
#include
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);
pthread_join(tid, (void**)&data);
thread: 要被回收的子线程的线程IDretval: 二级指针, 指向一级指针的地址, 这个地址中存储了pthread_exit() 传递出的数据,如果不需要这个参数,可以指定为NULL
现在我们来系统描述一下针对回收子线程数据的线程回收技术吧!
使用主线程栈
在上面子线程调用退出函数部分,我们就是使用的主线程栈上的数据,传递给子线程处理该数据,然后我们主线程在干自己的任务,把修改data数据的任务交给了子线程,最后阻塞在检查子线程活干的咋样。
使用子线程堆区
你觉得可以使用子线程栈区的数据然后回传吗?肯定是不行的,因为栈区数据在线程退出后会被销毁。子线程返回的指针将指向一个无效的内存地址,导致未定义行为。所以我们可以在子线程上堆区分配内存,然后把数据交给主线程:
#include
#include
#include
#include
#include
void* child_thread(void* arg) {
std::string* str = new std::string("hello world"); // 在堆上分配内存
pthread_exit((void*)str); // 返回指向堆上字符串的指针
}
int main() {
pthread_t tid;
// 创建子线程
pthread_create(&tid, NULL, child_thread, NULL);
void* ptr = nullptr;
//主线程执行自己的业务逻辑,把写一个hello world字符串的任务交给子线程
// 等待子线程结束,并获取返回状态
pthread_join(tid, &ptr);
// 将void*指针转换为std::string*指针,并打印字符串
std::string* str_ptr = static_cast
std::cout << *str_ptr << std::endl;
// 释放堆上分配的内存
delete str_ptr;
return 0;
}
使用全局变量
在文章开篇我们就说过,主线程和子线程是共享.text、.rodata、.data、.heap、.bss和文件描述符的。所以子线程操作全局变量,然后把修改好的值传回给主线程当然也是允许的,具体实验请读者自己设计一个吧
4.线程分离
之前我们说过 pthread_join() 是一个阻塞函数,只要子线程不退出主线程会被一直阻塞,但是主线程有自己的业务逻辑要去执行,那应该怎么办呢?
这就涉及到我们的线程分离函数上场了。
调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用pthread_join()就回收不到子线程资源了。
其实也就是父子线程各干各的了:
#include
#include
#include
void* working(void *arg) {
for (int i = 0; i < 10; i ++) {
std::cout << "child say: " << i << std::endl;
}
}
int main () {
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
//子线程与主线程分离
pthread_detach(tid);
//主线程执行自己的逻辑
for (int i = 100; i < 110; i++) {
std::cout << "parent say: " << i << std::endl;
}
std::cout << "task done!!!" << std::endl;
return 0;
}
线程分离技术一般用在什么情况下?简单的后台任务
当子线程执行的是一个简单的、短暂的后台任务,而主线程不需要等待该子线程完成,也不需要获取子线程的返回值时,线程分离技术可以很方便地使用。长期运行的任务
当子线程需要执行一个长期运行的任务,而主线程不需要等待它完成,这种情况下也可以使用线程分离。这样主线程可以继续执行其他任务,而不必被子线程的运行时间所阻碍。不可预测的结束时间
当子线程的结束时间不可预测,主线程不能在合理的时间内使用pthread_join等待子线程结束时,线程分离技术也很有用。这样可以避免主线程长时间等待,导致资源
5.线程同步(或者叫线程间通信?)
由于线程的运行顺序是由操作系统的调度算法决定的,谁也不知道哪个线程先执行哪个后执行,所以我们必须使用线程同步技术来管理相关的资源。
所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。
每一个环节我都会给定一个题目,先给出实现代码,随后讲解相关的知识。
互斥锁
互斥锁就不赘述了,主要就是对于一个共享资源必须加锁,不然有可能出现资源错乱的问题。
#include
#include
#include
// 定义一个互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 共享数据
int shared_data = 0;
// 线程函数
void* thread_function(void* arg) {
// 锁定互斥锁
pthread_mutex_lock(&mutex);
// 对共享数据进行操作
shared_data++;
// 打印共享数据
printf("Thread %ld - shared_data: %d
", pthread_self(), shared_data);
// 解锁互斥锁
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t tid1, tid2;
// 创建两个线程
pthread_create(&tid1, NULL, thread_function, NULL);
pthread_create(&tid2, NULL, thread_function, NULL);
// 等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
它的用法也比较简单,首先想要使用互斥锁必须先完成初始化,
的第二个参数表示互斥锁属性,一般写NULL。
使用完之后记得销毁,销毁时传入的是互斥锁所在的地址,在调用的时候也是传入地址。
读写锁
读写锁允许多个线程同时获取读锁(只要没有线程持有写锁),但写锁是排他的,其他线程必须等待写锁释放后才能获取读锁或写锁。
示例代码如下:我们定义两个读线程,一个写线程。
#include
#include
#include
// 定义一个读写锁
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 共享数据
int shared_data = 0;
// 读取共享数据的线程函数
void* reader(void* arg) {
(void)arg; // 未使用的参数
// 读取锁
pthread_rwlock_rdlock(&rwlock);
printf("Reader: shared_data = %d
", shared_data);
// 释放读取锁
pthread_rwlock_unlock(&rwlock);
return NULL;
}
// 写入共享数据的线程函数
void* writer(void* arg) {
(void)arg; // 未使用的参数
// 写入锁
pthread_rwlock_wrlock(&rwlock);
// 修改共享数据
shared_data++;
printf("Writer: updated shared_data to %d
", shared_data);
// 释放写入锁
pthread_rwlock_unlock(&rwlock);
return NULL;
}
int main() {
pthread_t r1, r2, w1;
// 创建读者线程
pthread_create(&r1, NULL, reader, NULL);
// 创建另一个读者线程
pthread_create(&r2, NULL, reader, NULL);
// 等待读者线程完成
pthread_join(r1, NULL);
pthread_join(r2, NULL);
// 创建写入者线程
pthread_create(&w1, NULL, writer, NULL);
// 等待写入者线程完成
pthread_join(w1, NULL);
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
它的使用和互斥锁是一模一样的,值不过多了读取锁和写入锁的调用,释放锁都是一样的:
// 读取锁
pthread_rwlock_rdlock(&rwlock);
// 写入锁
pthread_rwlock_wrlock(&rwlock);
//释放读取锁或者写入锁
pthread_rwlock_unlock(&rwlock);
条件变量
学完条件变量,我们就可以实现所谓的“线程依次执行”。
整个使用方法如下:
#include
//定义条件变量类型变量
pthread_cond_t cond;
//初始化
//第一个传参&cond
//第二个参数为条件变量属性,一般使用默认属性,指定为NULL
int pthread_cond_init(pthread_cond_t *cond, NULL)
//释放资源
int pthread_cond_destroy(pthread_cond_t *cond);
//线程阻塞函数:它的工作流程如下
//1. 释放与条件变量cond关联的互斥锁mutex
//2. 之后,调用线程会被阻塞,并从运行状态中移除,进入等待条件变量的状态。
//3. 直到另一个线程执行了对应的 pthread_cond_signal 或 pthread_cond_broadcast 操作来唤醒它
//4. 被唤醒后重新获取互斥锁
//5.解除阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond
, pthread_mutex_t *restrict mutex);
//有超时时间的线程阻塞函数,时间到达之后,解除阻塞
int pthread_cond_timedwait(pthread_cond_t *restrict cond
, pthread_mutex_t *restrict mutex
, const struct timespec *restrict abstime);
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);
这里的案例就使用我们经典的生产者单消费者模型
这里有三个生产者、三个消费者,生产者只生产50个商品,如果当前生产者发现任务队列有超过10个商品,生产者休息,如果消费者消费完了,消费者阻塞,通知生产者生产,生产者生产
#include
#include
#include
#include
#include
// 链表的节点
struct Node
{
int number;
struct Node* next;
};
// 定义条件变量, 控制消费者线程
pthread_cond_t cond;
// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;
void* producer(void *arg) {
while(1) {
//模拟生产时间
sleep(rand() % 3);
pthread_mutex_lock(&mutex);
Node* pnew = (struct Node*)malloc(sizeof(Node));
pnew->number = rand() % 1000;
pnew->next = head;
head = pnew;
printf("producer, number = %d, tid=%ld
"
, pnew->number
, pthread_self());
pthread_mutex_unlock(&mutex);
//生产了任务,通知消费者消费
pthread_cond_broadcast(&cond);
}
return nullptr;
}
void* consumer(void *arg) {
while(1) {
pthread_mutex_lock(&mutex);
while(head == nullptr) {
pthread_cond_wait(&cond, &mutex);
}
//消费过程
Node* pnode = head;
printf("consumer, number = %d, tid = %ld
"
, pnode->number
, pthread_self());
head = pnode->next;
free(pnode);
pthread_mutex_unlock(&mutex);
//模拟消费时间
sleep(rand() % 3);
}
return nullptr;
}
int main()
{
pthread_cond_init(&cond, nullptr);
pthread_mutex_init(&mutex, nullptr);
//创建5个生产者,5个消费者
pthread_t ptid[5];
pthread_t ctid[5];
//启动线程
for (int i = 0; i < 5; i++) {
pthread_create(&ptid[i], nullptr, producer, nullptr);
}
for (int i = 0; i < 5; i++) {
pthread_create(&ptid[i], nullptr, consumer, nullptr);
}
//释放资源
for (int i = 0; i < 5; i++) {
pthread_join(ptid[i], nullptr);
}
for (int i = 0; i < 5; i++) {
pthread_join(ctid[i], nullptr);
}
//销毁互斥锁和条件变量
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
}
信号量
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
强调!!!
信号量主要用来阻塞线程,不能保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用!
如果五个线程同时被阻塞在,有一个线程调用了,很可能多个线程同时解除阻塞!
#include
//定义变量
sem_t sem;
//初始化
// pshared = 0 线程同步
// pshared 非 0 进程同步
// value:初始化当前信号量拥有的资源数(>=0),如果资源数为0,线程就会被阻塞了。
int sem_init(sem_t *sem, int pshared, unsighed int val);
//释放资源
int sem_destroy(sem_t *sem);
//线程阻塞函数:如果资源数被耗尽,则函数阻塞
// 函数被调用, sem中的资源就会被消耗1个, 资源数-1
int sem_wait(sem_t *sem);
//如果资源被耗尽,直接返回错误号,用于处理获取资源失败之后的情况
int sem_trywait(sem_t *sem);
//超时阻塞:就算被阻塞了,超过某时间解除阻塞
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
//调用该函数给sem中的资源数+1
int sem_post(sem_t *sem);
这里给一个简单的使用案例:
该代码可以清晰查看sem_wait和sem_post的行为
#include
#include
#include
#include
#define MAXNUM 2
sem_t semPtr;
pthread_t a_thread, b_thread, c_thread;
int g_phreadNum = 1;
void *func1(void *arg) {
sem_wait(&semPtr);
printf("a_thread get a semaphore
");
sleep(5);
sem_post(&semPtr);
printf("a_thread release semaphore
");
}
void *func2(void *arg) {
sem_wait(&semPtr);
printf("b_thread get a semaphore
");
sleep(5);
sem_post(&semPtr);
printf("b_thread release semaphore
");
}
void *func3(void *arg) {
sem_wait(&semPtr);
printf("c_thread get a semaphore
");
sleep(5);
sem_post(&semPtr);
printf("c_thread release semaphore
");
}
int main() {
int taskNum;
// 创建2个信号量
sem_init(&semPtr, 0, MAXNUM);
//线程1获取1个信号量,5秒后释放
pthread_create(&a_thread, NULL, func1, NULL);
//线程2获取1个信号量,5秒后释放
pthread_create(&b_thread, NULL, func2, NULL);
sleep(1);
//线程3获取信号量,只有线程1或者线程2释放后,才能获取到
pthread_create(&c_thread, NULL, func3, NULL);
sleep(10);
//销毁信号量
sem_destroy(&semPtr);
return 0;
}
互斥锁:防止多个线程同时访问某个特定的资源或代码段。同步:协调多个线程的执行顺序,确保它们按正确的顺序执行。限制资源的并发访问数量:控制同时访问某些资源(如数据库连接、文件句柄等)的线程数量。线程池管理:管理线程池中的线程数量,以及任务队列中的待处理任务数量。
信号量实现生产者、消费者模型
场景描述:使用信号量实现生产者和消费者模型,生产者有5个,往链表头部添加节点,消费者也有5个,删除链表头部的节点。
总资源数为1
如果生产者和消费者使用的信号量总资源数为1,那么不会出现生产者线程和消费者线程同时访问共享资源的情况,不管生产者和消费者线程有多少个,它们都是顺序执行的。
主要执行的逻辑就是,定义生产者信号量和消费者信号量两个信号量,他们一共只持有1个资源。在生产者生产完之后,给消费者增加一个资源,消费者消费完了给生产者增加一个资源
所以本节完全可以不使用互斥锁
#include
#include
#include
#include
#include
#include
// 链表的节点
struct Node
{
int number;
struct Node* next;
};
// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;
// 指向头结点的指针
struct Node * head = NULL;
// 生产者的回调函数
void* producer(void* arg)
{
// 一直生产
while(1)
{
// 生产者拿一个信号量
sem_wait(&psem);
//生产过程
struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
pnew->number = rand() % 1000;
pnew->next = head;
head = pnew;
printf("+++producer, number = %d, tid = %ld
", pnew->number, pthread_self());
// 通知消费者消费, 给消费者加一个信号量
sem_post(&csem);
// 生产慢一点
sleep(rand() % 3);
}
return NULL;
}
// 消费者的回调函数
void* consumer(void* arg)
{
while(1)
{
sem_wait(&csem);
// 取出链表的头结点, 将其删除
struct Node* pnode = head;
printf("--consumer: number: %d, tid = %ld
", pnode->number, pthread_self());
head = pnode->next;
free(pnode);
// 通知生产者生成, 给生产者加信号灯
sem_post(&psem);
sleep(rand() % 3);
}
return NULL;
}
int main()
{
// 初始化信号量
// 生产者和消费者拥有的信号灯的总和为1
sem_init(&psem, 0, 1); // 生产者线程一共有1个信号灯
sem_init(&csem, 0, 0); // 消费者线程一共有0个信号灯
// 创建5个生产者, 5个消费者
pthread_t ptid[5];
pthread_t ctid[5];
for(int i=0; i<5; ++i)
{
pthread_create(&ptid[i], NULL, producer, NULL);
}
for(int i=0; i<5; ++i)
{
pthread_create(&ctid[i], NULL, consumer, NULL);
}
// 释放资源
for(int i=0; i<5; ++i)
{
pthread_join(ptid[i], NULL);
}
for(int i=0; i<5; ++i)
{
pthread_join(ctid[i], NULL);
}
sem_destroy(&psem);
sem_destroy(&csem);
return 0;
}
该代码有一个很大的问题,就是可能出现连续多个生产者生产,这是不应该发生的。这是为什么呢?百思不得其解。
总资源数大于1
如果生产者和消费者线程使用的信号量对应的总资源数为大于1,这种场景下出现的情况就比较多了:
所以说这个时候就会产生数据竞争了
#include
#include
#include
#include
#include
#include
// 链表的节点
struct Node
{
int number;
struct Node* next;
};
// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;
// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;
// 生产者的回调函数
void* producer(void* arg)
{
// 一直生产
while(1)
{
// 生产者拿一个信号灯
sem_wait(&psem);
// 加锁, 这句代码放到 sem_wait()上边, 有可能会造成死锁
pthread_mutex_lock(&mutex);
// 创建一个链表的新节点
struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
// 节点初始化
pnew->number = rand() % 1000;
// 节点的连接, 添加到链表的头部, 新节点就新的头结点
pnew->next = head;
// head指针前移
head = pnew;
printf("+++producer, number = %d, tid = %ld
", pnew->number, pthread_self());
pthread_mutex_unlock(&mutex);
// 通知消费者消费
sem_post(&csem);
// 生产慢一点
sleep(rand() % 3);
}
return NULL;
}
// 消费者的回调函数
void* consumer(void* arg)
{
while(1)
{
sem_wait(&csem);
pthread_mutex_lock(&mutex);
struct Node* pnode = head;
printf("--consumer: number: %d, tid = %ld
", pnode->number, pthread_self());
head = pnode->next;
// 取出链表的头结点, 将其删除
free(pnode);
pthread_mutex_unlock(&mutex);
// 通知生产者生成, 给生产者加信号灯
sem_post(&psem);
sleep(rand() % 3);
}
return NULL;
}
int main()
{
// 初始化信号量
sem_init(&psem, 0, 5); // 生成者线程一共有5个信号灯
sem_init(&csem, 0, 0); // 消费者线程一共有0个信号灯
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建5个生产者, 5个消费者
pthread_t ptid[5];
pthread_t ctid[5];
for(int i=0; i<5; ++i)
{
pthread_create(&ptid[i], NULL, producer, NULL);
}
for(int i=0; i<5; ++i)
{
pthread_create(&ctid[i], NULL, consumer, NULL);
}
// 释放资源
for(int i=0; i<5; ++i)
{
pthread_join(ptid[i], NULL);
}
for(int i=0; i<5; ++i)
{
pthread_join(ctid[i], NULL);
}
sem_destroy(&psem);
sem_destroy(&csem);
pthread_mutex_destroy(&mutex);
return 0;
}
到此这篇关于C/C++ pthread线程库 使用的文章就介绍到这了,更多相关C++ pthread线程库内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
您可能感兴趣的文章: