目录
前言
因为多个线程是共享地址空间的,也就是说很多资源都是共享的。
优点:通信方便。 缺点:缺乏访问控制。
因为一个线程的操作问题,给其他线程造成了不可控,或者引起崩溃,异常,逻辑正确的现象叫做:线程安全。
要想创建一个函数没有线程安全问题,就不要使用stl,malloc,new等会在全局内有效的数据。
因为所有线程都可以访问修改数据。
所以就需要进行对线程的访问进行控制,例如 同步和互斥。
1.线程互斥
1.1. 背景概念
临界资源:多线程执行流共享的资源,且一次只能允许一个执行流访问的资源就叫做临界资源。(多线程、多进程打印数据)
临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么不执行 。
多线程抢票程序代码演示:
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
using namespace std;
const int NUM = 5;
int tickets = 10000;
void* GetTickets(void* arg)
{
int id = *(int*)arg;
delete (int*)arg;
while(true)
{
if(tickets > 0)
{
usleep(1000);
cout<<"我是线程["<<id<<"], 我抢的票是:"<<tickets<<endl;
tickets--;
}
else
{
break;
}
}
}
int main()
{
pthread_t tid[NUM];
for(int i = 0; i < NUM; ++i)
{
int*id = new int(i);
pthread_create(tid+i, NULL, GetTickets, (void*)id); // 创建5个线程同时抢票
}
for(int i = 0; i < NUM; ++i)
{
pthread_join(tid[i], NULL);
}
return 0;
}
从程序中可以看到,票数到0的时候就没有票了,线程就应该退出了。
但是结果中,票数甚至被抢到了负数,这是怎么回事。
这里提一个问题,这里对票(临界资源)的访问是原子的吗?(是安全的吗?) 答案肯定不是!!
可能在一个线程A中,刚刚将tickets加载到内存上,线程A就被切走了,这时线程A的数据和上下文被保存,线程A从CPU上被剥离。
线程B开始抢票,如果他的竞争力非常强,一次运行后抢到了1000张票。
线程B执行完后线程A又来了,他会从上次执行的地方继续执行,但是他上次保存的tickets的数据是10000,所以抢到了一张票后,将剩余的9999张票写回内存,本来线程B执行完后还剩9000张票,但是线程A执行完后剩余的票数反而增多了。
而上面票数减到负数的原因是因为,当一个线程在抢票进行if条件判断时,if条件为真,线程已经进入了抢票环节,线程就被切走了,但是其他线程将票抢完了,再回到当前线程时,就将票数抢到了负数。
1.2. 互斥锁
对于上面的抢票程序,要想使每个线程正确的抢票就要保证:当一个线程在进入到抢票环节时,其他线程不能进行抢票。
所以就可以对抢票环节加互斥锁。
调用接口:
pthread_mutex_init、pthread_mutex_destroy:对线程锁进行初始化和销毁
#include <pthread.h> // pthread_mutex_t mutex: 锁变量,所有线程都可看到 int pthread_mutex_destroy(pthread_mutex_t *mutex);// 销毁锁 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);// 初始化锁 // attr: 锁属性,我们传入空指针就可 // 如果将锁定义为静态或者全局的,可以使用宏直接初始化,且不用销毁 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock、int pthread_mutex_unlock:对线程进行加锁和解锁
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
那么现在就可对抢票程序进行加锁了:
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
using namespace std;
// 对抢票过程进行封装
class Ticket
{
private:
int tickets;
pthread_mutex_t mtx;
public:
Ticket()
:tickets(1000) // 初始化票数为1000
{
pthread_mutex_init(&mtx, nullptr); // 初始化锁
}
~Ticket()
{
pthread_mutex_destroy(&mtx); // 销毁锁
}
bool GetTickets()
{
bool flag = true;
pthread_mutex_lock(&mtx); // 加锁
// 执行这部分代码时就是互斥的
if(tickets > 0)
{
usleep(1000);
cout<<"我是线程["<<pthread_self()<<"], 我抢的票是:"<<tickets<<endl;
tickets--;
}
else
{
cout<<"票被抢空了。。。"<<endl;
flag = false;
}
pthread_mutex_unlock(&mtx); // 解锁
return flag;
}
};
void* GetTickets(void* arg)
{
Ticket* tickets = (Ticket*)arg;
while(true)
{
if(!tickets->GetTickets()) // 票为空退出
{
break;
}
}
}
const int NUM = 5;
int main()
{
Ticket* t = new Ticket;
pthread_t tid[NUM];
for(int i = 0; i < NUM; ++i)
{
pthread_create(tid+i, NULL, GetTickets, (void*)t);
}
for(int i = 0; i < NUM; ++i)
{
pthread_join(tid[i], NULL);
}
return 0;
}
现在就不会将票抢到负数了。
1.3. 互斥锁原理
以抢票程序为例,当线程需要访问临界资源时,需要先访问mtx,为了所有的线程都能看到它,所以锁肯定是全局的。
且锁本身也是临界资源。那么如何保证锁本身是安全的,即获取锁的过程是安全的。
其原理是:加锁(lock)、解锁(unlock)的过程是原子的!
那怎样才算是原子的呢:一行代码被翻译成汇编后只有一条汇编,就是原子的。
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令。
该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。
即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
当线程申请到锁之后,进入到临界区访问临界资源,这时线程也可能被切走,被切走后会保护上下文,而锁数据也在上下文中。
所以锁也被带走了,所以即便是该线程被挂起了,其他线程也不能申请到锁,也不能进入临界区。
必须等待拥有锁的线程释放锁之后才能申请到锁。
2.可重入函数、线程安全和死锁
2.1. 重入和线程安全
线程安全:
线程安全指的是在多线程编程中,多个线程对临界资源进行争抢访问而不会造成数据二义或程序逻辑混乱的情况。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:
同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数
线程安全的实现,通过同步与互斥实现
具体互斥的实现可以通过互斥锁和信号量实现、而同步可以通过条件变量与信号量实现。
常见的线程不安全的情况:
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
常见不可重入的情况:
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
可重入与线程安全联系:
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别:
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数锁还未释放则会产生死锁
2.2. 死锁
2.2.1. 概念
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资 源而处于的一种永久等待状态。
死锁的四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配
3. 线程同步
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问 题,叫做同步 。
3.1. 条件变量
3.1.1. 概念
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。
用自己的话来说,就是当一个线程拥有锁访问临界资源时,其他线程不知道临界资源的状态,但自己又不能进入临界区,所以就需要一种中间媒介来传递临界资源的使用情况,就有了条件变量。
3.1.2. 条件变量对应操作函数
这套接口的使用与互斥锁非常相似
pthread_cond_init、pthread_cond_destroy:初始化、销毁条件变量
#include <pthread.h> int pthread_cond_destroy(pthread_cond_t *cond);// pthread_cond_t:条件变量类型,类似pthread_mutex_t int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); // 如果是静态或全局的条件变量可使用宏初始化: pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_wait、pthread_cond_signal:等待条件、唤醒线程
#include <pthread.h> // 等待条件满足 int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); // 唤醒一个线程,在cond等待队列里的第一个线程 int pthread_cond_signal(pthread_cond_t *cond); // 一次唤醒所有线程 int pthread_cond_broadcast(pthread_cond_t *cond);
演示代码:用一个线程控制另一个线程
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
using namespace std;
pthread_mutex_t mtx;
pthread_cond_t cond;
void *ctrl(void* args)
{
string name = (char*)args;
while(true)
{
// 唤醒在条件变量下队列中等待的第一个线程
pthread_cond_signal(&cond);
cout<< name <<"say: worker begin" << endl;
sleep(1);
}
}
void* work(void* args)
{
int num = *(int*)args;
while(true)
{
pthread_cond_wait(&cond, &mtx); // 等待条件变量
cout<< "worker: "<< num << " is working..."<< endl;
}
}
int main()
{
pthread_mutex_init(&mtx, nullptr);
pthread_cond_init(&cond, nullptr);
pthread_t worker[5];
pthread_t master;
pthread_create(&master, nullptr, ctrl, (void*)"master");
for(int i = 0; i < 5; ++i)
{
int* number = new int(i);
pthread_create(worker+i, nullptr, work, (void*)number);
}
for(int i = 0; i < 5; ++i)
{
pthread_join(worker[i], nullptr);
}
pthread_join(master, nullptr);
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond);
return 0;
}
根据结果可以看到,线程的执行是按照一定的顺序的,所以其实条件变量的内部是存在一个等待队列的。
3.1.3.基于阻塞队列的生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
这种模式得优点是:
解耦 支持并发 支持忙闲不均
生产者消费者模型的321原则:
三种关系:生产者与生产者的关系是竞争和互斥、消费者与消费者的关系是竞争和互斥、生产者和消费者的关系是互斥和同步
两种角色:生产者和消费者
一个交易场所:一段缓冲区(内存空间、stl容器等)
实则之前所讲的进程间通信中的管道通信就是一种生产者消费者模型,管道就是让不同的进程能够看到同一份资源,且管道自带同步和互斥的机制。进程间通信的本质其实就是生产者消费者模型。
代码实现:通过两种线程(生产者和消费者),生产者线程向队列中放数据,消费者线程向队列中拿数据,队列(阻塞队列)就是交易场所。
task.hpp:用于任务计算
#pragma once #include <iostream> #include <pthread.h> using namespace std; class Task { private: int _x; int _y; char _op; public: Task() {} Task(int x, int y, char op) : _x(x), _y(y), _op(op) {} int option() { int ans = 0; switch (_op) { case '+': ans = _x + _y; break; case '-': ans = _x - _y; break; case '*': ans = _x * _y; break; case '/': ans = _x / _y; break; case '%': ans = _x % _y; break; default: cout << "error" << endl; break; } cout << "当前任务正在被:" << pthread_self() << "处理-->" << _x << _op << _y << "=" << ans << endl; return ans; } int operator()() { return option(); } };
BlockQueue.hpp:
#pragma once #include <iostream> #include <queue> #include <pthread.h> #include <unistd.h> #include <time.h> #include <stdlib.h> using namespace std; template<class T> class BlockQueue { private: queue<T> _bq; // 阻塞队列 int _capacity; // 队列容量 pthread_mutex_t _mtx; // 保护资源 pthread_cond_t _full; // _bq 满了, 生产者需在该条件变量下等待 pthread_cond_t _empty; // _bq 空了,消费者需在该条件变量下等待 private: bool IsFull() { return _bq.size() == _capacity; } bool IsEmpty() { return _bq.size() == 0; } void Lock() { pthread_mutex_lock(&_mtx); } void Unlock() { pthread_mutex_unlock(&_mtx); } void ProducterWait() { // 由于自己持有锁,但是自己被挂起了,按照常理,另一个线程也申请不到锁,这时就会导致出现死锁问题。 // 且调用结束之后,自己仍然处于临界区中,如果没有锁,那么违反了互斥 // 所以wait中传入mutex参数的意义就在这里: // 1.调用时,会先释放锁,再挂起自己 // 2.返回的时候,会首先自动去竞争锁,只有获取到锁的时候才会被唤醒 pthread_cond_wait(&_empty, &_mtx); // 等待队列中有空间 } void ConsumerWait() { pthread_cond_wait(&_full, &_mtx); // 等待队列中有数据 } void WakeupConsumer() { pthread_cond_signal(&_full); } void WakeupProducter() { pthread_cond_signal(&_empty); } public: BlockQueue(const int capacity = 4) :_capacity(capacity) { pthread_mutex_init(&_mtx, nullptr); pthread_cond_init(&_full, nullptr); pthread_cond_init(&_empty, nullptr); } ~BlockQueue() { pthread_mutex_destroy(&_mtx); pthread_cond_destroy(&_full); pthread_cond_destroy(&_empty); } void push(const T& in)//生产函数 { Lock(); // 进行需要进行条件检测的时候,需要使用循环判断 // 来保证退出循环一定是因为条件不满足导致的 while(IsFull()) // 队列满了 { ProducterWait(); // 等待队列有空间,因为要访问临界资源,这里自己肯定是持有锁的 } // 如果挂起失败(函数调用失败),或者被伪唤醒(多线程情况下,队列中情况满足时,其他线程先被唤醒且把队列中的数据拿完了),这里条件不满足还会向下执行,所以应该使用循环判断该条件 _bq.push(in); WakeupConsumer(); // 向里面放入数据后,唤醒消费者线程 Unlock(); } void pop(T*out)//消费函数 { Lock(); while(IsEmpty()) { ConsumerWait(); } *out = _bq.front(); _bq.pop(); WakeupProducter(); // 从队列拿出数据后,唤醒生产者线程 Unlock(); } };
cp.cc:
#include "BlockQueue.hpp" #include "task.hpp" void *consumer(void *args) { BlockQueue<Task> *bq = (BlockQueue<Task> *)args; while (true) { Task t; bq->pop(&t); // 获取任务 t(); // 处理任务 } } void *producter(void *args) { BlockQueue<Task> *bq = (BlockQueue<Task> *)args; string str = "+-*/%"; while (true) { int x = rand() % 50 + 1; int y = rand() % 50 + 1; char op = str[rand() % 5]; Task t(x, y, op); // 生产任务 cout << "生产者派发了一个任务: " << x << op << y << "=?" << endl; bq->push(t); sleep(1); } } int main() { srand((long long)time(nullptr)); BlockQueue<Task> *bq = new BlockQueue<Task>; pthread_t c1, c2, c3, c4, c5, p; pthread_create(&c1, nullptr, consumer, (void *)bq); pthread_create(&c2, nullptr, consumer, (void *)bq); pthread_create(&c3, nullptr, consumer, (void *)bq); pthread_create(&c4, nullptr, consumer, (void *)bq); pthread_create(&c5, nullptr, consumer, (void *)bq); pthread_create(&p, nullptr, producter, (void *)bq); pthread_join(c1, nullptr); pthread_join(c2, nullptr); pthread_join(c3, nullptr); pthread_join(c4, nullptr); pthread_join(c5, nullptr); pthread_join(p, nullptr); return 0; }
生产者消费者模型中,不仅仅只是将数据放入缓冲区、将数据拿出缓冲区这么简单,还要进行数据获取,任务处理等。
其中生产者在获取任务时,消费者可以处理任务。
3.2. POSIX信号量
3.2.1. 概念
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于 线程间同步。
信号量本质就是一个计数器,用来描述临界区中临界资源的数目大小。
临界资源如果可以被划分为更小的资源,如果处理得当,我们也有可能让多个线程同时访问临界资源,从而实现并发。
但是每个线程想访问临界资源,都得先申请信号量资源。
3.2.2. 信号量对应操作函数
申请信号量成功时,临界资源的数目会减一;释放信号量时,临界资源的数目会加一。
由于信号量是用来维护临界资源的,首先必须得保证自身是安全的,所以常规的对全局变量的++或--操作肯定是不行的。
操作的大概过程使用一段伪代码表示:
P操作(申请信号量):
start:
lock();
if(count <= 0)
{
// 挂起
unlock();
goto start;
}
else
{
count--;
}
unlock();
V操作(释放信号量):
lock()
count++;
unlock();
调用接口:
sem_init、sem_destroy:初始化销毁信号量(具体用法与mutex和cond十分类似)
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); // pshared: 默认为0, value:信号量的初始值(count) int sem_destroy(sem_t *sem); // sem_t :信号量类型 // Link with -pthread.
sem_wait、sem_signal: 申请、释放信号量
#include <semaphore.h> int sem_wait(sem_t *sem); // P操作 int sem_post(sem_t *sem); // V操作 // Link with -pthread.
3.2.3. 基于环形队列的生产者消费者模型
环形队列:
在数据结构初阶章节,我们学习过环形队列,即一个头尾相连的队列
但是现在的环形队列的判空判满不再使用中的两种方式判断,因为有了信号量可以判定。
队列为空的时候,消费者和生产者指向同一个位置。(生产和消费线程不能同时进行)(生产者执行)
队列为满的时候,消费者和生产者也指向同一个位置。(生产和消费线程不能同时进行)(消费者执行)
当队列不为空不为满的时候,消费者和生产者不指向同一个位置。(生产和消费线程可以并发执行)
根据上面三种情况,基于环形队列的生产者消费者模型应该遵守以下规则:
生产者不能把消费者套一个圈
消费者不能超过生产者
当指向同一个位置的时候,要根据空、满状态,判断让谁先执行
其他情况,消费者和生产者可以并发执行
这里生产者关心的是环形队列中空位置,消费者关心的是环形队列中的数据。
所以生产者对应的信号量描述的是环形队列中空位置的数量(blank),消费者对应的信号量描述的是环形队列中数据的数量(data)。
代码实现:
ring_queue.hpp:
#pragma once #include <iostream> #include <vector> #include <semaphore.h> using namespace std; template <class T> class ring_queue { private: vector<T> _ring_queue; int _capacity; // 容量 int _p_index; // 生产者放数据的位置 int _c_index; // 消费者拿数据的位置 sem_t _blank_sem; // 格子信号量 sem_t _data_sem; // 数据信号量 pthread_mutex_t _p_mtx; // 生产互斥锁,生产者与生产者之间是互斥关系 pthread_mutex_t _c_mtx; // 消费互斥锁,消费者与消费者之间是互斥关系 public: ring_queue(int capacity = 5) :_capacity(capacity),_p_index(0), _c_index(0),_ring_queue(capacity) // 在vector中提前开好空间,不然可能会导致越界访问 { sem_init(&_blank_sem, 0, _capacity); // 初始化信号量 sem_init(&_data_sem, 0, 0); pthread_mutex_init(&_p_mtx, nullptr); // 初始化锁 pthread_mutex_init(&_c_mtx, nullptr); } ~ring_queue() { sem_destroy(&_blank_sem); // 销毁信号量 sem_destroy(&_data_sem); pthread_mutex_destroy(&_p_mtx); // 销毁锁 pthread_mutex_destroy(&_c_mtx); } void push(const T &in) { sem_wait(&_blank_sem); // P操作(申请空位置),如果队列满了,线程会被挂起等待 pthread_mutex_lock(&_p_mtx); // 最好不要将互斥锁放在申请信号量的前面,放在前面就与单生产者没有区别,生产者需要申请到锁再申请信号量,效率较低 // 生产 // 放在后面时,可以保证信号量被所有生产线程提前申请,且只有申请到信号量的线程才有资格进程锁,保证了申请信号量和申请锁并行执行 _ring_queue[_p_index] = in; ++_p_index; _p_index %= _capacity; // 防止越界,队列成环,回到队头 pthread_mutex_unlock(&_p_mtx); // 需要将解锁过程放到后面是因为,数组下标是所有生产者线程共享的,对数组下标的操作不是原子性的,需要保证修改下标时,其他生产者线程不能访问 sem_post(&_data_sem); // V操作(释放数据信号量) } void pop(T *out) // 消费者线程同理 { sem_wait(&_data_sem); pthread_mutex_lock(&_c_mtx); // 消费 *out = _ring_queue[_c_index]; ++_c_index; _c_index %= _capacity; pthread_mutex_unlock(&_c_mtx); sem_post(&_blank_sem); } };
task.hpp:
#pragma once #include <iostream> #include <pthread.h> using namespace std; class Task { private: int _x; int _y; char _op; public: Task() {} Task(int x, int y, char op) : _x(x), _y(y), _op(op) {} int option() { int ans = 0; switch (_op) { case '+': ans = _x + _y; break; case '-': ans = _x - _y; break; case '*': ans = _x * _y; break; case '/': ans = _x / _y; break; case '%': ans = _x % _y; break; default: cout << "error" << endl; break; } cout << "当前任务正在被消费者线程:" << pthread_self() << "处理-->" << _x << _op << _y << "=" << ans << endl; return ans; } int operator()() { return option(); } };
ring_cp.cc:
#include "ring_queue.hpp" #include "task.hpp" #include <time.h> #include <pthread.h> #include <unistd.h> void *consumer(void *args) { ring_queue<Task> *rq = (ring_queue<Task> *)args; while (true) { Task t; rq->pop(&t); t(); sleep(1); } } void *producter(void *args) { ring_queue<Task> *rq = (ring_queue<Task> *)args; string str = "+-*/%"; while (true) { int x = rand() % 20 + 1; int y = rand() % 20 + 1; char op = str[rand() % 5]; Task t(x, y, op); cout << "生产者线程:" << pthread_self() << "--> 生产数据:" << x << op << y << "=?" << endl; rq->push(t); } } int main() { srand((long long)time(nullptr)); pthread_t c1, c2, c3, p1, p2, p3; ring_queue<Task> *rq = new ring_queue<Task>; pthread_create(&c1, nullptr, consumer, (void *)rq); pthread_create(&c2, nullptr, consumer, (void *)rq); pthread_create(&c3, nullptr, consumer, (void *)rq); pthread_create(&p1, nullptr, producter, (void *)rq); pthread_create(&p2, nullptr, producter, (void *)rq); pthread_create(&p3, nullptr, producter, (void *)rq); pthread_join(c1, nullptr); pthread_join(c2, nullptr); pthread_join(c3, nullptr); pthread_join(p1, nullptr); pthread_join(p2, nullptr); pthread_join(p3, nullptr); return 0; }
多线程的优势在于,并发的获取和处理任务。
通过这里的实验就可以很好的验证多生产者多消费者线程的优势。
3.3. 总结
互斥锁与信号量的异同
互斥锁由同一线程加放锁,信号量可以由不同线程进行PV操作。
计数信号量允许多个线程,且值为剩余可用资源数量。互斥锁保证多个线程对一个共享资源的互斥访问,信号量用于协调多个线程对一系列资源的访问条。
件变量与信号量的异同
信号量利用条件变量、互斥锁、计数器实现,计数器就是信号量的核心,信号量可以说是是条件变量的高级抽象。
使用条件变量可以一次唤醒所有等待者,而这个信号量没有的功能。
信号量是有一个值,而条件变量是没有的。从实现上来说一个信号量可以是用mutex + count + cond实现的。因为信号量有一个状态,可以精准的同步,信号量可以解决条件变量中存在的唤醒丢失问题。
条件变量一般需要配合互斥锁使用,而信号量可根据情况而定。
有了互斥锁和条件变量还提供信号量的原因是:尽管信号量的意图在于进程间同步,互斥锁和条件变量的意图在于线程间同步,但是信号量也可用于线程间,互斥锁和条件变量也可用于进程间。信号量最有用的场景是用以指明可用资源的数量。