Linux下多线程同步的方式主要有互斥量(互斥锁)、条件变量、信号量以及读写锁等,更详细的可以看《UNIX环境高级编程第11章》。
线程同步的作用是什么?为什么要线程同步?
所谓同步,一般是指:
多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用, 从而保证了该变量的唯一性和准确性。
也就是说线程同步的作用是:保证共享数据的唯一性和安全性。线程同步属于多个线程协同步调,按预定的先后次序进行运行,避免同时使用导致共享数据的紊乱。
参考:Linux-线程同步_wengmq的博客-CSDN博客,以下内容基本都来自《UNIX环境高级编程第11章》和该参考文章,仅做个人学习记录。
互斥量(mutex)
互斥量从本质上说是一把锁,访问共享资源之前对互斥量进行加锁,访问完成之后则解锁互斥量。
互斥量的初始化:
Linux下,线程的互斥量数据类型是pthread_mutex_t
,使用前需要进行初始化。
1、静态分配:设置为常量PTHREAD_MUTEX_INITIALIZER
;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2、动态分配:调用pthread_mutex_init()
函数进行初始化,mutexattr
用于指定互斥锁属性。如果用默认属性初始化互斥量,那么就将mutexattr
置位NULL
即可。如果动态分配互斥量(比如使用malloc函数),那么释放内存前需要调用pthread_mutex_destroy
。
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr);
pthread_mutex_init()
函数的参数之前可能会是这样的:pthread_mutex_t *restrict mutex
,多了一个restrict
,它的作用是:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改。
两个函数的返回值都是:
- 成功:返回0
- 失败:返回错误编号
加锁、解锁、销毁锁
- 普通加锁:如果互斥量已经上锁,则加锁失败,调用线程将阻塞直到互斥量被解锁。
int pthread_mutex_lock(pthread_mutex *mutex);
- 测试加锁:相当于加锁之前会事先检查是否可以加锁:如果可以加锁,则会锁住互斥量;如果不可以加锁,也不会出现线程阻塞,直接返回
EBUSY
。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
对应的解锁函数都是:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
动态分配的互斥量对应的销毁锁函数:
int pthread_mutex_destroy(pthread_mutex *mutex);
以上几个函数的返回值都是:
- 成功:返回0
- 失败:返回错误编号
示例:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
const int NLOOP = 50;//每个线程的累加次数
int counter;//全局变量
//静态分配 一个默认锁
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
void *doit(void *vptr) {
int val;
//累加NLOOP次
for (int i = 0; i < NLOOP; i++) {
pthread_mutex_lock(&counter_mutex);//加锁
val = counter;
//pthread_self()获取自身的线程id
printf("%x: %d\n", (unsigned int)pthread_self(), val + 1);
counter = val + 1;
pthread_mutex_unlock(&counter_mutex);//解锁
}
return NULL;
}
int main(int argc, char **argv) {
//创建两个线程
pthread_t tidA, tidB;
pthread_create(&tidA, NULL, &doit, NULL);
pthread_create(&tidB, NULL, &doit, NULL);
//等待两个线程结束
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
linux下运行需要注意:Linux下C++多线程的问题_zj-CSDN博客
条件变量(cond)
参考:【Linux】多线程同步的四种方式 - Y先森0.0 - 博客园
互斥量不是万能的,比如某个线程正在等待共享数据内某个条件出现,可能需要重复对数据对象加锁和解锁(轮询),但是这样轮询非常耗费时间和资源,而且效率非常低,所以互斥锁不太适合这种情况。
我们需要这样一种方法:当线程在等待满足某些条件时使线程进入睡眠状态,一旦条件满足,就唤醒因等待满足特定条件而睡眠的线程。
条件变量是利用线程间共享全局变量进行同步的一种机制,给多个线程提供了一个会合的场所。条件本身是由互斥量保护的。
条件变量上的基本操作有:
- 触发条件(当条件变为 true 时);
- 等待条件,挂起线程直到其他线程触发条件。
条件变量初始化
同样在使用条件变量前,必须先对它进行初始化。由pthread_cond_t
的类型表示条件变量,初始化方式也是分两种:静态和动态。
1、静态初始化:设置为常量PTHREAD_COND_INITIALIZER
。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
2、动态初始化:使用函数pthread_cond_init()
,attr
参数通常设置为NULL
即可
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);
这儿也可能会有restrict
关键字,作用和互斥量里面是一致的。
同样会对应一个条件变量销毁函数pthread_cond_destroy
:
int pthread_cond_destroy(pthread_cond_t *cond);
以上两个函数的返回值:
- 成功:返回0
- 失败:返回错误编号
等待与激活函数
1、无条件等待
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
调用者把锁住的互斥量传递给函数,函数然后自动把调用线程放到等待条件的线程列表上,对互斥量解锁。这样就关闭了条件检查和进入休眠状态等待条件这两个操作之间的时间通道,线程就不会错过条件的任何改变。
2、计时等待
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
与无条件等待的函数类似,但是多了一个超时。超时值指的是我们愿意等待多长时间,abstime与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。如果超时到期时条件还没有出现,将会返回错误ETIMEOUT
。
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求(用 pthread_cond_wait()
或 pthread_cond_timedwait()
)竞争条件 。mutex互斥锁必须是普通锁或者适应锁。也就是使用互斥量把条件变量保护起来。
且在调用pthread_cond_wait()
前必须由本线程加锁(pthread_mutex_lock()
),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()
之前,mutex将被重新加锁,以与进入pthread_cond_wait()
前的加锁动作对应。
3、激活条件
- 激活一个等待该条件的线程(存在多个等待线程时按入队顺序激活其中一个)
int pthread_cond_signal(pthread_cond_t *cond);
- 激活所有等待该条件的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
4、销毁条件变量
只有在没有线程在该条件变量上等待的时候才能销毁这个条件变量,否则返回EBUSY
int pthread_cond_destroy(pthread_cond_t *cond);
示例:生产消费者模型(条件变量与互斥量),该示例来自:Linux-线程同步_wengmq的博客-CSDN博客
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
//生产者消费者模型
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//互斥量
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;//条件变量
struct msg{
int num;
msg* next;
};
msg *head;
void *customer(void *p){//消费者
//假如消费者先执行
msg *mp;
for( ; ; ){
pthread_mutex_lock(&lock);//上锁
while(head == NULL)//没有食物,阻塞等待
pthread_cond_wait(&has_product, &lock);
mp = head;
head = mp->next;//删除这个节点(消费掉食物)
pthread_mutex_unlock(&lock);//解锁
cout << " 消费掉:" << mp->num << endl;
free(mp);
sleep(10);//执行挂起一段时间
}
}
void *product(void *p){//生产者
msg *mp;
for( ; ; ){
mp =(msg*) malloc(sizeof(struct msg));
mp->num = rand()%1000+1;//模拟一张饼
cout << "生产出:" << mp->num << endl;
//向链表加入节点
pthread_mutex_lock(&lock);//上锁
mp->next = head;
head = mp;//头插法
pthread_cond_signal(&has_product);//唤醒阻塞在条件上的进程
pthread_mutex_unlock(&lock);//解锁
sleep(1);
}
}
int main(){
pthread_t pid ,cid;
pthread_create(&pid, NULL, &customer, NULL);
pthread_create(&cid, NULL, &product, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
可以通过修改睡眠时间来观察运行情况,也可以添加多个消费者,以及设置存储队列控制生产量。
信号量
信号量的操作在这里面说的蛮详细的:关于多线程同步与互斥_zj-CSDN博客
信号量(sem)和互斥锁的区别:互斥锁只允许一个线程进入临界区,而信号量允许多个线程进入临界区(只要资源数量足够分配)。
进程可以通过信号量进行通信,线程同样也是可以的。
更详细的资料可以看:《UNIX环境高级编程》469页。
头文件:
#include <semaphore.h>
1、初始化信号量
int sem_init (sem_t *sem , int pshared, unsigned int value);
sem - 指定要初始化的信号量;
pshared - 信号量 sem 的共享选项,linux只支持0,表示它是当前进程的局部信号量;
value - 信号量 sem 的初始值。
2、信号量值加1
给参数sem指定的信号量值加1。
int sem_post(sem_t *sem);
3、信号量值减1
给参数sem指定的信号量值减1。如果sem所指的信号量的数值为0,函数将会等待直到有其它线程使它不再是0为止。
int sem_wait(sem_t *sem);
4、销毁信号量
int sem_destroy(sem_t *sem);
以上函数的返回值:
- 成功:返回0
- 失败:返回-1
示例:还是生产消费者模型(信号量)
#include <semaphore.h>
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
const int NUM = 5;//最大产品数 5
int queue[NUM];//存放产品的数组
sem_t blank_number, product_number;//定义空格数 产品数
//参数里面的void *arg必须有
void *producer(void *arg) {//生产者操作
int p = 0;
while (1) {
sem_wait(&blank_number);//空格数-1
queue[p] = rand() % 1000 + 1;//给产品随机赋一个值
cout << "Produce:" << queue[p] << endl;
sem_post(&product_number);//产品数+1
p = (p+1)%NUM;//数组到底的时候重新回到开头
sleep(rand()%5);
}
}
void *consumer(void *arg) {//消费者操作
int c = 0;
while (1) {
sem_wait(&product_number);//产品数-1
cout << "Consume:" << queue[c] << endl;
queue[c] = 0;
sem_post(&blank_number);//空格数+1
c = (c+1)%NUM;
sleep(rand()%5);
}
}
int main(int argc, char *argv[]) {
pthread_t pid, cid;
//初始化时,空格数为NUM,产品为0个
sem_init(&blank_number, 0, NUM);
sem_init(&product_number, 0, 0);
//创建两个进程
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
//等待两个进程结束
pthread_join(pid, NULL);
pthread_join(cid, NULL);
//销毁两个进程,并释放资源
sem_destroy(&blank_number);
sem_destroy(&product_number);
return 0;
}
除此之外线程同步还可以通过读写锁,自旋锁,屏障等方式实现,具体的还是建议看书。。。