一,线程同步概念
同步:同步即协同步调,按预定的先后次序运行。
线程同步:指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。
“同步”的目的:是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。
资源混乱的原因:
1. 资源共享(独享资源则不会)
2. 调度随机(意味着数据访问会出现竞争)
3. 线程间缺乏必要的同步机制。
线程同步的机制有互斥锁,读写锁,条件变量,信号量
二,互斥锁
1,互斥锁用于保护临界区,以保证任何时刻只有一个线程再执行其中的代码。
2,当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。
因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。
互斥锁也可以用于进程间的同步
二值信号灯与互斥量的区别:
二值信号灯强调的是共享资源,只要共享资源可用,其他进程或线程同样可以修改信号灯的值;而互斥锁更强调进程或线程,占用资源的进程或线程使用完资源后,必须由进程或线程本身来解锁
3,相关函数
pthread_mutex_init函数
pthread_mutex_destroy函数
pthread_mutex_lock函数
pthread_mutex_trylock函数
pthread_mutex_unlock函数
以上5个函数的返回值都是:成功返回0, 失败返回错误号。
4,pthread_mutex_init函数(动态初始化)
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;(静态初始化)
功能:初始化一个互斥锁(互斥量) ---> 初值可看作1
mutex:全局定义的互斥变量 也叫互斥锁
attr:属性,一般为NULL
- 静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。e.g. pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
- 动态初始化:局部变量应采用动态初始化。e.g. pthread_mutex_init(&mutex, NULL)
2,pthread_mutex_destroy函数
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:销毁一个互斥锁
3,pthread_mutex_lock函数
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:加锁。可理解为将mutex--(或-1)
4,int pthread_mutex_unlock函数
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:解锁。可理解为将mutex++(或+1)
mutex的值只有1和0。1表示锁可用,0表示锁不可用,被其他线程所占用
5,pthread_mutex_trylock函数
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能:尝试加锁
如果互斥锁被锁住会返回一个EBUSY错误
lock与unlock:
lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。
lock与trylock:
lock加锁失败会阻塞,等待锁释放。
trylock加锁失败直接返回错误号(如:EBUSY),不阻塞。
在访问共享资源前加锁,访问结束后立即解锁(调用unlock)。锁的“粒度”应越小越好。
死锁:
1. 线程试图对同一个互斥量A加锁两次。
第二次加锁不会成功,就会一直阻塞。等到第一把锁解锁,但解锁是该线程解,然而他在阻塞。
2. 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁
对于第二种死锁。自己的锁没有解,去获取其他锁时,用pthread_mutex_trylock.不满足就会返回错误不会阻塞,然后作出让步,把自己的锁解了。
三,读写锁
1,与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。读锁、写锁并行阻塞,写锁优先级高
两种状态:1. 读模式下加锁状态 (读锁)
2. 写模式下加锁状态 (写锁)
三种情况:
1,读写锁是写模式加锁时,解锁前,所有对该锁要加锁的的线程进行阻塞(写独占)
2,读写锁是读模式加锁时,如果想要加锁的线程全是读模式加锁,则加锁成功(读共享)。如果想要加锁的线程全是写模式加锁,则这些线程阻塞
3,读写锁是读模式加锁时,如果要想加锁的线程中既有读模式加锁,又有写模式加锁,则阻塞。下次加锁时则写模式加锁的优先级高(写优先级高)。
2,假设情景描述:
一个线程现在加锁用于读,后来又来了两个用于读的线程则他们加锁成功。后来又来了一个读线程和写线程准备加锁,他们两个则阻塞。前面三个线程读完后,解锁后。写线程加锁,读线程先阻塞。在写线程加锁期间,如果又有其他线程要加锁则阻塞。
3,读写锁非常适合于对数据结构读的次数远大于写的情况。
4,主要应用函数:
pthread_rwlock_init函数
pthread_rwlock_destroy函数
pthread_rwlock_rdlock函数
pthread_rwlock_wrlock函数
pthread_rwlock_tryrdlock函数
pthread_rwlock_trywrlock函数
pthread_rwlock_unlock函数
以上7 个函数的返回值都是:成功返回0, 失败直接返回错误号。
pthread_rwlock_t类型 用于定义一个读写锁变量。
pthread_rwlock_t rwlock;
1,初始化一把读写锁:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
2,销毁一把读写锁:
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
3,以读方式请求读写锁。(常简称为:请求读锁)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
4,以写方式请求读写锁。(常简称为:请求写锁)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
5,解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
6,非阻塞以读方式请求读写锁(非阻塞请求读锁)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
7,非阻塞以写方式请求读写锁(非阻塞请求写锁)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
注:函数具体用法与互斥锁差不多
例子:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int counter;
pthread_rwlock_t rwlock;
/* 3个线程不定时写同一全局资源,5个线程不定时读同一全局资源 */
void *th_write(void *arg)
{
int t, i = (int)arg;
while (1) {
pthread_rwlock_wrlock(&rwlock);
t = counter;
usleep(1000);
printf("=======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
pthread_rwlock_unlock(&rwlock);
usleep(10000);
}
return NULL;
}
void *th_read(void *arg)
{
int i = (int)arg;
while (1) {
pthread_rwlock_rdlock(&rwlock);
printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);
pthread_rwlock_unlock(&rwlock);
usleep(2000);
}
return NULL;
}
int main(void)
{
int i;
pthread_t tid[8];
pthread_rwlock_init(&rwlock, NULL);
for (i = 0; i < 3; i++)
pthread_create(&tid[i], NULL, th_write, (void *)i);
for (i = 0; i < 5; i++)
pthread_create(&tid[i+3], NULL, th_read, (void *)i);
for (i = 0; i < 8; i++)
pthread_join(tid[i], NULL);
pthread_rwlock_destroy(&rwlock);
return 0;
}
四,条件变量
1,为什么需要条件变量
互斥锁用于上锁,条件变量用于等待。互斥锁只有两种状态,锁定和非锁定。多个线程访问共享资源时,不知道何时去使用共享资源。
2,函数
pthread_cond_init函数
pthread_cond_destroy函数
pthread_cond_wait函数
pthread_cond_timedwait函数
pthread_cond_signal函数
pthread_cond_broadcast函数
以上6 个函数的返回值都是:成功返回0, 失败直接返回错误号。
pthread_cond_t类型 用于定义条件变量
pthread_cond_t cond;
1,初始化一个条件变量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
静态初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
2,销毁一个条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
3,阻塞等待一个条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
三个作用:
1. 阻塞等待条件变量cond(参1)满足
2. 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
1.2.两步为一个原子操作。
3,当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);
唤醒是指另外的线程调用pthread_cond_signal或pthread_cond_broadcast
4,限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
这里用的是绝对时间:
struct timespec {
time_t tv_sec; /* seconds */ 秒
long tv_nsec; /* nanosecondes*/ 纳秒
}
time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。
struct timespec t = {1, 0};
pthread_cond_timedwait (&cond, &mutex, &t); 只能定时到 1970年1月1日 00:00:01秒(早已经过去)
正确用法:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1秒
pthread_cond_timedwait (&cond, &mutex, &t); 传参
5,唤醒至少一个阻塞在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
6,唤醒全部阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
互斥锁与条件变量的基本模型
对于生产者:
pthread_mutex_lock(&mutex);
if(设置条件为真)
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
对于消费者:
pthread_mutex_lock(&mutex);
while(条件为假)//while的作用是避免虚假唤醒
pthread_cond_wait(&cond,&mutex);
pthread_mutex_unlock(&mutex);
避免上锁冲突
最坏的情况:当条件变量满足时,signal唤醒等待该条件变量的线程,该线程开始运行但立即停止,因为此线程没有获取互斥锁,是因为signal所在的线程还没有解锁
int dosignal;
pthread_mutex_lock(&mutex);
dosignal=(条件满足);
nready++;
pthread_mutex_unlock(&mutex);
if(dosignal)
pthread_cond_signal(&cond,&cond);
例子:
//此条件变量是head不为空
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
struct msg {
struct msg *next;
int num;
};
struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p)
{
struct msg *mp;
for (;;) {
pthread_mutex_lock(&lock);
while (head == NULL) { //头指针为空,说明没有节点 可以为if吗
pthread_cond_wait(&has_product, &lock);
}
mp = head;
head = mp->next; //模拟消费掉一个产品
pthread_mutex_unlock(&lock);
printf("-Consume ---%d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
}
void *producer(void *p)
{
struct msg *mp;
while (1) {
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1; //模拟生产一个产品
printf("-Produce ---%d\n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product); //将等待在该条件变量上的一个线程唤醒
sleep(rand() % 5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
互斥锁和条件变量的属性
pthread_mutexattr_t attr;
#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr,int *pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int pshared);
pthread_condattr_t attr;
int pthread_condattr_destroy(pthread_condattr_t *attr);
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr,
int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr,int pshared);
get函数:pshared指向整数中这个属性的当前值
set函数:pshared设置这个属性的当前值。PTHREAD_PROCESS_PRIVATE或
PROCESS_SHARED后者是进程间共享的属性
以上函数都是成功返回0,失败返回错误号
五,信号量
1,信号量的分类
三种类型的信号量:
Posix有名信号量:使用Posix IPC名字标识,可用于进程或线程间的同步
Posix基于内存的信号量:存放在共享内存中,可用于进程或线程间的同步
System V信号量:在内核中维护,可用于进程或线程间同步。
Posix信号量不必再内核中维护。可能与文件系统中的路径名对应的名字来标识
互斥锁和信号量区别:
1,互斥锁必须总是由给它上锁的线程解锁,信号量的挂出却不必由执行过它的等待操作的同一线程执行。
2,互斥锁要么被锁住,要么被解开,
3,信号量有一个与之关联的状态,信号量挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么信号将丢失。
Posix信号量分为两种:
进化版的互斥量。互斥量只能取1,0.而信号量可取N。多个对象对同一资源共享,而互斥量只能一个对象对共享资源占用。
加锁:-- 解锁:++。值为0时,阻塞。大于0时才加锁成功
2,Posix基于内存的信号量函数(无名信号量)
主要函数:
sem_init函数
sem_destroy函数
sem_wait函数
sem_trywait函数
sem_timedwait函数
sem_post函数
sem_getvalue函数
以上7 个函数的返回值都是:成功返回0, 失败返回-1,同时设置errno。
sem_t类型:本质是结构体,但我们可以把她看成大于等于0的整数
1,sem_init函数
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:sem_t定义的变量
pshared:为0时作用于线程,大于0(一般为1)作用于进程
value:信号量初值
2,sem_destroy函数
#include <semaphore.h>
int sem_destroy(sem_t *sem);
sem:sem_t定义的变量
功能:销毁一个信号量
3,sem_wait函数
#include <semaphore.h>
int sem_wait(sem_t *sem);
sem:sem_t定义的变量
功能:给信号量加锁 类似于pthread_mutex_lock
4,sem_trywait函数
#include <semaphore.h>
int sem_trywait(sem_t *sem);
sem:sem_t定义的变量
功能:尝试加锁,如果加锁不成功,不阻塞。类似于pthread_mutex_trylock
5,sem_timedwait函数
#include <semaphore.h>
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
sem:sem_t定义的变量
abs_timeout:绝对时间
功能:限时尝试对信号量加锁,类似于thread_cond_timelock
定时1秒:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1秒
sem_timedwait(&sem, &t); 传参
6,sem_post函数
#include <semaphore.h>
int sem_post(sem_t *sem);
sem:sem_t定义的变量
功能:解锁,类似于pthread_mutex_unlock.
7,sem_getvalue函数
#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);
sem:sem_t定义的变量
sval:传出参数,是一个整数取地址。表示信号量的当前值。
如果sval值0,表示已上锁,为某一个负数,其绝对值就是等待该信号解锁的线程数