目录
1 同步的概念
这里同步的概念是“协同”的意思,是为了协调步调,让进程/线程按预定的次序运行,即同步的本质就是协调执行顺序。即“同步”的目的,是为了避免数据混乱,解决与时间有关的错误。即有多个控制流,共同操作一个共享资源的时候,都需要同步,同步的实现方法,一般就是给共享资源加锁,使得多个进程只能互斥地访问共享资源。
提到线程同步,就不得不提到线程安全的问题,因为线程同步可以解决线程安全问题。线程安全问题主要是由全局变量以及静态变量引起了,线程执行函数里边的局部变量不会对其它线程造成影响。其实,若每个线程对全局变量、静态变量只有读操作,没有写操作,一般来说,这个全局变量/静态变量是线程安全的。若有多个线程同时执行写操作,那么不同线程对全局变量/静态变量的改变就可能会影响到其它线性,这个时候就需要用线程同步了。
下面看一个多个进程访问共享资源的例子
#include<unistd.h>
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
void *thr1(void *arg){
while(1){
printf("hello");
sleep(rand()%3);
printf("world\n");
sleep(rand()%3);
}
}
void *thr2(void *arg){
while(1){
printf("HELLO");
sleep(rand()%3);
printf("WORLD\n");
sleep(rand()%3);
}
}
int main(){
pthread_t tid[2];
pthread_create(&tid[0], NULL, thr1, NULL);
pthread_create(&tid[1], NULL, thr2, NULL);
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
return 0;
}
可以看到,helloworld并没有大小写分开输出,而是乱七八糟,连输出的长度都不一样。因为两个线程操作的是同一个文件,即标准输出。因为线程中的第一个printf中没有刷新,所以可能数据还在缓冲区然后这个文件就别另一个线程写了,所以就有了helloHELLOWORLD这种输出。
2 mutex互斥量
互斥锁对象
pthread_mutex_t
可以理解为一把锁
锁初始化
int pthread_mutex_init(pthread_mutex_t *restrict_mutex, const pthread_mutexattr_t *restrict_attr);
restrict_mutex:传入一个互斥锁数据类型对象的地址,pthread_mutex_t
restrict_attr:互斥量的属性,不用考虑,直接传NULL就行
除了用pthread_mutex_init函数来初始化互斥锁对象以外,还可以用常量初始化互斥锁对象。
即pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
给共享资源加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
mutex:一个互斥锁对象
用mutex这把锁来给共享资源上锁。
pthread_mutex_lock函数是阻塞等待的。
我们把共享资源当成一扇大门的话,也就是说,如果当前线程想要给大门上一把锁,但是门上已经有锁了,那么该线程就会阻塞等待下去,等待门上这把锁解开以后,当前线程才会给们上锁。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
pthread_mutex_trylock函数是非阻塞的上锁函数,即如果遇到共享资源已经被某线程上锁,则该函数立即返回并设置errno。
线程中一旦调用了上锁函数以后,下边线程访问到的共享资源都是专属的了,其它线程不能访问,直到调用解锁函数,共享资源才可以被另外的线程访问。
给共享资源解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
摧毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥锁例子
加上互斥锁之后,很好地做到了线程同步,解决了线程安全问题,但是可能导致另一个问题,死锁。互斥条件是死锁形成的必要条件之一。
#include<unistd.h>
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// pthread_mutex_init(&mutex, NULL);
void *thr1(void *arg){
while(1){
pthread_mutex_lock(&mutex);
printf("hello");
sleep(rand()%3);
printf("world\n");
// sleep(rand()%3);
pthread_mutex_unlock(&mutex);
sleep(rand()%3); // 注意把sleep移动这里是因为如果解锁之后没有sleep,
// 则thr1会马上再去抢锁,那么可能造成谁第一个抢到以后都是谁的
}
}
void *thr2(void *arg){
while(1){
pthread_mutex_lock(&mutex);
printf("HELLO");
sleep(rand()%3);
printf("WORLD\n");
// sleep(rand()%3);
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
}
int main(){
pthread_t tid[2];
pthread_create(&tid[0], NULL, thr1, NULL);
pthread_create(&tid[1], NULL, thr2, NULL);
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
// pthread_detach(tid[0]);
// pthread_detach(tid[1]);
return 0;
}
死锁
我们要明确一下,线程给某资源上锁的目的,其实是想获得某资源的专属使用权。所以就可以理解什么叫死锁了,就是线程想获取某资源的专属使用权,但是因为各种原因,它永远不可能获得。
因为互斥锁pthread_mutex_t造成死锁一般有两种原因:
一、线程连续两次申请某资源的使用权,即第一次用pthread_mutex_lock给某资源上锁成功后,还没有解锁就又一次用pthread_mutex_lock申请资源使用权,因为pthread_mutex_lock是阻塞等待的。因此第二次申请会等待资源被释放,但是因为线程阻塞在了第二次申请资源这一行,所以线程无法继续向下执行,即资源永远不会被释放。因而造成死锁。这种一般的解决方案是程序员写代码的时候小点心,别写重复了上锁语句。
二、交叉锁。如果申请某资源需要两把锁,线程A和线程B各抢到了一把,显然,它们形成了环路等待条件,最终谁也等不到另一把,造成死锁问题。解决方法可以破话死锁形成的几个必要条件就行。比如破坏环路等待条件,只需要限制访问某资源需要按一定顺序来上锁。破坏请求保持条件,如果一个进程申请到一把锁,当时另一个把锁申请失败了,那么就把申请成功的这把锁也释放掉。
3 读写锁
读写锁概念
互斥量或互斥锁是最简单的锁,一旦上锁则资源只能本线程使用,其它线程不能读写。
读写锁是互斥量的进阶,其特性是:读共享,写独占,写的优先级高。即读写锁有三种状态,读锁,写锁,未加锁。
读共享:首先要明确,读写锁首先是一把锁,如果线程A给某资源上了读锁,虽然读共享,但意思不是说线程A上过锁之后线程B还是可以直接访问该资源,而是说线程B也可以再次给该资源上读锁,从而实现多个线程访问同一个资源的效果。如果线程B尝试上写锁,则会阻塞。
写独占:一旦某个线程给资源上了写锁,则其它线程都不能再读写该资源,即任何尝试给该资源上锁的操作都会阻塞。
写的优先级高:并不是说,如果线程A给某资源上了读锁,这个时候线程B可以强行给该资源上写锁。而是说,如果线程A想给某资源上读锁,线程B想给该资源上写锁,但是这个时候该资源已经被线程C上了读锁,这个时候线程A与B都会阻塞,即这个时候即便线程A上的是读锁也不行,读共享的性质失效了。一直等到线程C给资源解锁,则线程B更优先,给资源上写锁。线程A就继续阻塞吧。即读锁、写锁并行阻塞的时候,写锁的优先级高。
如果读线程比较多用读写锁更好,如果读线程跟写线程差不多的话读写锁跟互斥锁效果也差不多。
读写锁对象
pthread_rwlock_t
读写锁初始化
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
// rwlock:读写锁对象
// attr:传NULL就行
// 也可以用常量初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
加读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 非阻塞读锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
加写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 非阻塞写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
释放锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
销毁锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
读写锁例子
由例子可以发现一件事,那就是一个执行函数可以用于创建很多个指向相同代码的线程。
4 条件变量与生产者消费者模型
生产者消费者模型
有多个生产者线程和多个消费者线程,它们共用一个内存缓冲区,生产者往缓冲区里写数据,消费者从缓冲区取走数据。当缓冲区为空时,消费者线程会阻塞,当缓冲区满时,生产者线程阻塞。
条件变量
条件变量不是锁,需要与锁配合使用,条件变量的作用是让所有线程都阻塞的时候都阻塞在条件变量上,比如对于消费者线程,如果缓冲区空了,它们都阻塞在条件变量上,当缓冲区中来数据了以后,条件变量会唤醒其中的一个消费者线程或者多个(所有)消费者线程。同理对于生产者线程也一样。
条件变量结构体
pthread_cond_t
条件变量初始化
int pthread_cond_init(pthread_cond_t *restrict_cond, const pthread_condattr_t *restrict_attr);
restrict_cond:条件变量结构体对象的地址
restrict_attr:传NULL就行吧
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
条件变量摧毁
int pthread_cond_destroy(pthread_cond_t *cond);
将线程阻塞等待在条件变量上
int pthread_cond_wait(pthread_cond_t *restrict_cond, pthread_mutex_t *restrict_mutex);
restrict_cond:条件变量对象的地址
restrict_mutex:锁对象的地址
先调用pthread_mutex_lock申请上锁,然后调用pthread_cond_wait将锁阻塞在条件变量上,等到条件变量满足条件再去申请上锁,这个时候资源一定是可用的。
其实在调用pthread_cond_wait之后,会先将pthread_mutex_lock加的锁给解锁,然后等待条件变量cond满足条件,满足以后再上锁。
int pthread_cond_timewait(pthread_cond_t *restrict_cond, pthread_mutex_t *restrict_mutex, const struct timespec *restrict_abstime);
struct timespec{
time_t tv_sec; // 秒
long tv_nsec; // 纳秒
}
我们知道pthread_cond_wait是先解锁,然后等条件满足在加锁。而pthread_cond_timewait也是先解锁,然后等待一段时间,如果这段时间内cond条件满足了,那就再加锁,如果不满足,那就算了吧,这次先不申请上锁了。
发送解除阻塞消息
唤醒至少一个阻塞在条件变量cond上的线程
int pthread_cond_signal(pthread_cond_t *cond);
唤醒阻塞在条件变量cond上的全部线程
int pthread_cond_broadcast(pthread_cond_t *cond);
用条件变量解决生产者消费者问题
条件变量阻塞住消费者线程,生产者线程在资源可用的时候发送消息通知生产者线程。
注意这里我们在解决生产者消费者模型的时候只模拟了缓冲区为空时,消费者线程会阻塞,但是没有模拟缓冲区满时,生产者阻塞的情形,这个留给下一小节的信号量来解决。
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int beginnum = 1000;
// 定义临界资源,链表不为空表示可用
typedef struct _ProdInfo{
int num;
struct _ProdInfo *next;
}ProdInfo;
ProdInfo *Head = NULL;
void *thr_producter(void *arg){
// 生产者线程,负责往链表中加数据
while(1){
ProdInfo *prod = malloc(sizeof(ProdInfo));
prod->num = beginnum++;
printf("---%s---self-%lu---%d\n", __FUNCTION__, pthread_self(), prod->num);
pthread_mutex_lock(&mutex);
prod->next = Head;
Head = prod;
pthread_mutex_unlock(&mutex);
// 资源可用,条件变量发送通知
pthread_cond_signal(&cond);
sleep(rand()%2);
}
return NULL;
}
void *thr_customer(void *arg){
ProdInfo *prod = NULL; // 线程栈空间不共享,可以取同名变量
// 其实仔细想想,在Linux内部线程就是进程,只不过多个线程会共享很多区域的数据
// 只是每个线程都有自己独立的用户栈空间罢了
while(1){
pthread_mutex_lock(&mutex);
while(Head == NULL){ // 生产者线程同时给多个消费者发消息,可能接到消息一瞬间,
// 资源就被其它的线程消耗了,即pthread_cond_wait解除阻塞的一瞬间,如果有资源
// 那么Head != NULL,所以就继续往下执行,如果没有,进入while循环继续等待消息
pthread_cond_wait(&cond, &mutex);
}
prod = Head;
Head = Head->next;
printf("---%s---self-%lu---%d\n", __FUNCTION__, pthread_self(), prod->num);
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
free(prod);
}
}
int main(){
// 一个生产者,两个消费者
pthread_t tid[3];
pthread_create(&tid[0], NULL, thr_producter, NULL);
for(int i = 1; i < 3; ++i){
pthread_create(&tid[i], NULL, thr_customer, NULL);
}
for(int j = 0; j < 3; ++j){
pthread_join(tid[j], NULL);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
5 信号量
概念
信号量是加强版的互斥锁。
如果一个资源足够多个线程使用,那么用互斥锁只让一个线程使用是不合适的。
信号量相当于是拥有多个互斥锁,可以同时给多个线程使用。
需要注意的是,在资源内部,各个线程使用的是资源内部不重合的部分。
即信号量内部的可用资源剩多少个,线程就能申请几次信号量。
即这里用信号量来共享资源的意思是整体共享,局部互斥,与读写锁里边的读共享完全不是一个概念。
信号量对象
sem_t
信号量初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem: 信号量对象
pshared: 0代表线程信号量,1代表进程信号量
value: 初始化信号量的个数,在申请与释放信号量的过程中会动态变化
摧毁信号量
int sem_destroy(sem_t *sem);
申请信号量,即上锁
int sem_wait(sem_t *sem);
申请成功,则信号量的value值减1
当信号量value的值为0的时候申请信号零会阻塞
释放信号量,即解锁
int sem_post(sem_t *sem)
释放成功则信号量的值value加1
信号量实现生产者消费者模型
定义两个信号量
xfull:初始值为0,代表缓冲区为空,xfull代表缓冲区有多少产品可供消费。
blank:初始值为5,代表缓冲区有多少空位可以供生产者生产产品。
blank不为0时生产者可以生产产品,xfull不为零时消费者可以消费产品。
blank每申请一个信号量,xfull就要释放一次信号量,反之同理。
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<semaphore.h>
#define _SEM_CNT_ 5
sem_t blank, xfull;
int queue[_SEM_CNT_];
int beginnum = 1000;
void *thr_producter(void *arg){
int i = 0;
while(1){
sem_wait(&blank);
printf("---%s---self=%lu---%d\n", __FUNCTION__, pthread_self(), beginnum);
queue[(i++)%_SEM_CNT_] = beginnum++;
sem_post(&xfull);
sleep(rand()%3);
}
return NULL;
}
void *thr_customer(void *arg){
int i = 0;
int num = 0;
while(1){
sem_wait(&xfull);
num = queue[(i++)%_SEM_CNT_];
printf("---%s---self=%lu---num---%d\n", __FUNCTION__, pthread_self(), num);
sem_post(&blank);
sleep(rand()%3);
}
return NULL;
}
int main(){
sem_init(&blank, 0, _SEM_CNT_);
sem_init(&xfull, 0, 0);
pthread_t tid[2];
pthread_create(&tid[0], NULL, thr_producter, NULL);
pthread_create(&tid[1], NULL, thr_customer, NULL);
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
return 0;
}