和小男生做的网站,在哪建企业网站好,wordpress html调用php,长沙网站seo诊断笔记主要内容来自 爱编程的大柄–线程 爱编程的大柄–线程同步 在进入代码实践之前#xff0c;我们应该搞清楚。
线程是成语的最小执行单位#xff0c;进程是操作系统中最小的资源分配单位。
这样的话我们可以理解以下两点#xff1a;
同一地址空间中的多个线程独有的是我们应该搞清楚。
线程是成语的最小执行单位进程是操作系统中最小的资源分配单位。
这样的话我们可以理解以下两点
同一地址空间中的多个线程独有的是每个线程都有属于自己的栈区和寄存器内核中管理的寄存器主要记录的就是上下文共享的是.text、.rodata、.data、.heap、.bss、文件描述符
关于线程个数的确定
文件IO操作文件IO对CPU是使用率不高, 因此可以分时复用CPU时间片, 线程的个数 2 * CPU核心数 (效率最高)处理复杂的算法(主要是CPU进行运算, 压力大)线程的个数 CPU的核心数 (效率最高) 文章目录 1.线程创建代码练习 2.线程退出主线程调用退出函数子线程调用退出函数 3.线程回收使用主线程栈使用子线程堆区使用全局变量 4.线程分离⭐️5.线程同步(或者叫线程间通信)互斥锁读写锁⭐️条件变量⭐️信号量信号量实现生产者、消费者模型总资源数为1总资源数大于1 1.线程创建
#include pthread.h
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);我们主要用到的就是第一个和第三个、第四个参数。
第一个参数如果线程创建成功线程ID写入到该指针指向的内存 pthread_t itd1; pthread_create(tid1, ...)第二个参数是线程属性一般为NULL第三个参数是线程函数创建出的子线程的处理动作也就是该函数在子线程中执行第四个参数作为实参传递到 start_routine指针指向的函数内部。可以传入一个函数指针等等作为线程的回调函数。
代码练习
#include iostream
#include pthread.h#include unistd.hvoid* 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 say140470444414528为什么主线程要sleep(1)呢 因为主线程和子线程都是在抢CPU时间片谁抢到谁干活所以完全有可能子线程还没有抢到资源主线程结束那么整个进程就结束了子线程根本就来不及干活。
我们这里也可以使用信号量等子线程执行结束了通知主线程这里就涉及到线程间通信后面会进行详细讲解。
2.线程退出
#include pthread.h
void pthread_exit(void *retval);参数表示线程退出的时候携带的数据当前子线程的主线程会得到该数据。如果不需要使用指定为NULL这是重点因为我们C中的没有这个功能 主线程可以调用退出函数退出但是地址空间不会被释放。 子线程调用退出函数退出一般目的是带出一些有价值的数据。 主线程调用退出函数
#include pthread.h
#include stdio.h
#include unistd.hvoid* child_thread(void* arg) {sleep(1);printf(Child thread is running.\n);// 子线程执行一些工作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.\n);pthread_exit(NULL);return 0; // 这行代码不会执行因为主线程已经退出
}在这里我们可以发现主线程在创建子线程后立即退出而子线程在继续执行。 但是我们一般不会这样调用函数因为一般认为主线程的退出就代表程序执行结束。 要注意的是 即使主线程通过调用 pthread_exit 退出子线程也不会变成新的主线程。在 POSIX 线程pthread模型中当主线程退出时它创建的所有子线程仍然继续执行直到它们自己结束或被其他线程终止。 子线程调用退出函数
如果子线程退出想往外面传递什么参数也是配合pthread_join()一起使用它的作用是等待子线程结束并且获取返回状态
#include pthread.h
#include stdio.h
#include stdlib.hvoid* child_thread(void* arg) {int* data (int*)arg;printf(Child thread is processing data.\n);// 模拟计算*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\n, *data);free(data);} else {printf(Child thread failed to return data.\n);free(data);}return 0;
}3.线程回收
在刚才我们已经初步认识了线程回收函数pthread_join()这个函数是一个阻塞函数如果还有子线程在运行调用该函数就会阻塞子线程退出函数解除阻塞进行资源的回收函数被调用一次只能回收一个子线程如果有多个子线程则需要循环进行回收。
#include pthread.h
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);
pthread_join(tid, (void**)data);thread: 要被回收的子线程的线程IDretval: 二级指针, 指向一级指针的地址, 这个地址中存储了pthread_exit() 传递出的数据如果不需要这个参数可以指定为NULL
现在我们来系统描述一下针对回收子线程数据的线程回收技术吧
使用主线程栈
在上面子线程调用退出函数部分我们就是使用的主线程栈上的数据传递给子线程处理该数据然后我们主线程在干自己的任务把修改data数据的任务交给了子线程最后阻塞在pthread_join()检查子线程活干的咋样。
使用子线程堆区
你觉得可以使用子线程栈区的数据然后回传吗肯定是不行的因为栈区数据在线程退出后会被销毁。子线程返回的指针将指向一个无效的内存地址导致未定义行为。所以我们可以在子线程上堆区分配内存然后把数据交给主线程
#include pthread.h
#include stdio.h
#include stdlib.h
#include string
#include iostreamvoid* 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_caststd::string*(ptr);std::cout *str_ptr std::endl;// 释放堆上分配的内存delete str_ptr;return 0;
}使用全局变量
在文章开篇我们就说过主线程和子线程是共享.text、.rodata、.data、.heap、.bss和文件描述符的。所以子线程操作全局变量然后把修改好的值传回给主线程当然也是允许的具体实验请读者自己设计一个吧
4.线程分离
之前我们说过 pthread_join() 是一个阻塞函数只要子线程不退出主线程会被一直阻塞但是主线程有自己的业务逻辑要去执行那应该怎么办呢
这就涉及到我们的线程分离函数pthread_detach()上场了。
调用这个函数之后指定的子线程就可以和主线程分离当子线程退出的时候其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用pthread_join()就回收不到子线程资源了。
其实也就是父子线程各干各的了
#include iostream
#include pthread.h
#include unistd.hvoid* 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 pthread.h
#include stdio.h
#include stdlib.h// 定义一个互斥锁
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\n, 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;
}它的用法也比较简单首先想要使用互斥锁必须先完成初始化 pthread_mutex_init()的第二个参数表示互斥锁属性一般写NULL。
使用完之后记得销毁销毁时传入的是互斥锁所在的地址在调用的时候也是传入地址。
读写锁
读写锁允许多个线程同时获取读锁只要没有线程持有写锁但写锁是排他的其他线程必须等待写锁释放后才能获取读锁或写锁。
示例代码如下我们定义两个读线程一个写线程。
#include pthread.h
#include stdio.h
#include stdlib.h// 定义一个读写锁
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\n, 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\n, 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.h
//定义条件变量类型变量
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 stdio.h
#include stdlib.h
#include unistd.h
#include string.h
#include pthread.h// 链表的节点
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\n, 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\n, 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);
}
⭐️信号量
信号量用在多线程多任务同步的一个线程完成了某一个动作就通过信号量告诉别的线程别的线程再进行某些动作。信号量不一定是锁定某一个资源而是流程上的概念比如有AB两个线程B线程要等A线程完成某一任务以后再进行自己下面的步骤这个任务并不一定是锁定某一资源还可以是进行一些计算或者数据处理之类。
强调
信号量主要用来阻塞线程不能保证线程安全如果要保证线程安全需要信号量和互斥锁一起使用
如果五个线程同时被阻塞在sem_wait(sem)有一个线程调用了sem_post(sem)很可能多个线程同时解除阻塞
#include semaphore.h
//定义变量
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 pthread.h
#include semaphore.h
#include stdio.h
#include unistd.h#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 \n);sleep(5);sem_post(semPtr);printf(a_thread release semaphore \n);
}void *func2(void *arg) {sem_wait(semPtr);printf(b_thread get a semaphore \n);sleep(5);sem_post(semPtr);printf(b_thread release semaphore \n);
}void *func3(void *arg) {sem_wait(semPtr);printf(c_thread get a semaphore \n);sleep(5);sem_post(semPtr);printf(c_thread release semaphore \n);
}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 stdio.h
#include stdlib.h
#include unistd.h
#include string.h
#include semaphore.h
#include pthread.h// 链表的节点
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\n, 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\n, pnode-number, pthread_self());head pnode-next;free(pnode);// 通知生产者生成, 给生产者加信号灯sem_post(psem);sleep(rand() % 3);}return NULL;
}int main()
{// 初始化信号量// 生产者和消费者拥有的信号灯的总和为1sem_init(psem, 0, 1); // 生产者线程一共有1个信号灯sem_init(csem, 0, 0); // 消费者线程一共有0个信号灯// 创建5个生产者, 5个消费者pthread_t ptid[5];pthread_t ctid[5];for(int i0; i5; i){pthread_create(ptid[i], NULL, producer, NULL);}for(int i0; i5; i){pthread_create(ctid[i], NULL, consumer, NULL);}// 释放资源for(int i0; i5; i){pthread_join(ptid[i], NULL);}for(int i0; i5; i){pthread_join(ctid[i], NULL);}sem_destroy(psem);sem_destroy(csem);return 0;
}该代码有一个很大的问题就是可能出现连续多个生产者生产这是不应该发生的。这是为什么呢百思不得其解。
总资源数大于1
如果生产者和消费者线程使用的信号量对应的总资源数为大于1这种场景下出现的情况就比较多了
多个生产者线程同时生产多个消费者同时消费生产者线程和消费者线程同时生产和消费
所以说这个时候就会产生数据竞争了
#include stdio.h
#include stdlib.h
#include unistd.h
#include string.h
#include semaphore.h
#include pthread.h// 链表的节点
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\n, 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\n, 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 i0; i5; i){pthread_create(ptid[i], NULL, producer, NULL);}for(int i0; i5; i){pthread_create(ctid[i], NULL, consumer, NULL);}// 释放资源for(int i0; i5; i){pthread_join(ptid[i], NULL);}for(int i0; i5; i){pthread_join(ctid[i], NULL);}sem_destroy(psem);sem_destroy(csem);pthread_mutex_destroy(mutex);return 0;
}