01 概述
线程同步,是协调步调,按预定先后次序执行,解决与时间相关的错误。
线程不同步,产生的现象就是数据混乱:
1、资源共享(系统层面)
2、调度随机(系统层面)
3、缺乏必要的同步机制(用户层面,我们可以在这里做限制)
多个控制流访问同一共享资源时,必须同步。
02 互斥锁
2.1 概述
当多个线程并发的访问同一个共享资源的时候,可能导致数据异常。可以通过互斥锁确保一个共享资源每次只能被一个线程访问。
互斥锁属于重量级锁,因为存在用户态和内核态的切换。
2.2 操作
1、初始化
静态初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITILIZER;
动态初始化:
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
2、pthread_mutex_lock
阻塞加锁。
3、pthread_mutex_trylock
非阻塞加锁。
4、pthread_mutex_timelock
5、pthread_mutex_unlock
解锁,同时会将阻塞在该锁上的所有线程全部唤醒。
6、pthread_mutex_destroy
2.3 死锁
产生原因:
1、对同一互斥量重复加锁;
2、持有锁A的线程1请求锁B,持有锁B的线程请求锁A。
避免方法:
1、保证资源的获取顺序,要求每个线程获取资源的顺序一致;
2、当得不到所有所需要的资源时,放弃已经获得的资源,等待。
03 读写锁
3.1 概述
pthread读写锁把对共享资源的访问者分为读者和写者,读者只对共享资源进行读访问,写者只对共享资源进行写操作。在互斥机制,读者和写者都需要独立独占互斥量以独占共享资源,在读写锁机制下,允许同时有多个读者访问共享资源,只有写者才需要独占资源(读共享,写独占)。
相比互斥锁,读写机制由于允许多个读者同时访问共享资源,进一步提高了多线程的并发度。
3.2 记录上锁
记录上锁是读写锁的一种拓展(进程间的锁),可以用于有亲缘关系或者无亲缘关系的进程之间共享某个文件的读写。被锁住的文件通过其文件描述符访问,执行上锁操作的函数是fcntl,这种类型的锁通常在内核中维护,其属主是由属主的进程ID标识的。
这意味着这些锁用于不同进程间的上锁,而不是用于同一进程内不同线程的上锁。
3.3 操作
1、初始化
pthread_rwlock_init
2、pthread_ rwlock _lock
3、pthread_ rwlock _timelock
4、pthread_ rwlock _rdlock
5、pthread_ rwlock _wrlock
6、pthread_ rwlock _unlock
7、pthread_ rwlock _destroy
3.4 使用
适用于读操作远大于写操作的场景。
04 条件变量
4.1 概述
条件锁就是所谓的条件变量,某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态(主要用于访问公共资源)。一旦条件满足以“信号量”的方式唤醒一个因为条件而被阻塞的线程。
最为常见的就是在线程池中,起初没有任何时刻任务队列为空,此时线程池中的线程以为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。这个过程中就使用到了条件变量pthread_cond_t。
与互斥锁相比较,互斥锁用于上锁,条件变量(条件锁)用于等待。
与读写锁相比较,更加先进,因为可以使用条件变量判断这个共享区域是否存在数据是否可以访问,不需要读写锁再去判断了,减少不必要的竞争。
条件变量的特性:
1、条件变量不是锁;
2、可以造成线程阻塞;
3、与mutex配合使用。
4.2 操作
1、初始化:
静态初始化:pthread_cond_t condition = PTHREAD_COND_INITILIZER;
动态初始化:pthread_cond_init
2、pthread_cond_destroy
3、pthread_cond_wait
其语义相当于:首先解锁互斥锁,然后以阻塞方式等待条件变量的信号,收到信号后又会对互斥锁加锁。
为了防止“虚假唤醒”,该函数一般放在while循环体中。
while(当前线程中条件不成立){
pthread_cond_wait(cond, mutex);
//解锁,其他线程使条件成立发送信号,加锁
}
4、pthread_cond_unlock
释放互斥锁
5、pthread_cond_timedwait
以阻塞方式等待,如果时间到了条件还没有满足还是会结束。
6、pthread_cond_broadcast
唤醒全部阻塞在条件变量上的线程(一般不推荐使用,使用pthread_cond_signal)。
7、pthread_cond_signal
在另一个线程中改变线程,条件满足发送信号。唤醒一个等待的线程(可能有多个线程处于阻塞状态),唤醒哪个线程由具体的线程调度策略决定。
05 文件加锁
5.1 概述
应用程序经常需要读取文件中的数据,修改数据,然后回写数据,如果某一时刻只有一个进程执行文件操作不存在任何问题,但是如果同时多个进程执行操作就会出现问题。这就需要多进程之间实现同步,可以使用信号量来完成所需的同步,但通常文件锁更好一些,因为内核能够将锁和文件关联起来。
5.2 操作
1、flock
flock[file lock],建议性锁,不具备强制性。一个进程使用flock将文件锁住,另一个进程可以直接操作正在被锁的文件,修改文件中的数据。
flock系统调用是在整个文件中加锁,通过对传入的fd所指向的文件进行操作,然后再通过operation参数所设置的值来确定做什么样的操作,operation的值如下:
在默认情况下,如果另外一个进程已经持有了文件上的一个不兼容的锁,那么flock会阻塞。如果需要防止这种情况的出现,可以在operation参数中对这些值取OR(|)操作。在这种情况下,如果一个进程已经有了一个文件上的一个不兼容锁,那么flock就会阻塞。相反,它会返回-1,并将errno设置成EWOULDBLOCK。
flock放置的锁有如下限制:
1、粗粒度,只能对整个文件进行加锁;
2、通过flock只能放置劝告式锁。
3、很多NFS实现不识别flock放置的锁。
2、fcntl
原型:int flock(int fd, int operation);
fcntl函数覆盖了flock的功能,且提供了比flock更加强大的功能,但是在某些应用中仍然使用flock函数,并且在继承和释放锁方面的一些语义中flock与fcntl还是有所不同的。
3、ftruncate
原型:int ftruncate(int fd, off_t length);
功能:改变文件大小
说明:ftruncate会将参数fd指定的文件大小改为参数length指定的大小。如果原来的文件大小比参数length大,则超过的部分被删除。
06 信号量
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。
从信号量的描述来看,其功能类似互斥锁,但是二者存在诸多区别:
1、互斥量用于线程间互斥,信号量用于线程间同步(本质区别);
2、互斥量值只能为0/1,信号量值可以为非负整数;
3、互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。