《Linux应用多线程(一) — 线程的同步与互斥机制》
互斥锁和条件变量配合使用:
https://www.ibm.com/developerworks/cn/linux/thread/posix_thread1/index.html
https://www.ibm.com/developerworks/cn/linux/thread/posix_thread2/index.html
https://www.ibm.com/developerworks/cn/linux/thread/posix_thread3/index.html
https://blog.csdn.net/u022812849/article/details/109225200
1.互斥锁
互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后会释放(解锁)互斥量。
#include<pthread.h> int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); int pthread_mutex_destroy(pthread_mutex_t *mutex); 返回值: 成功:0; 失败:返回错误编号 attr: PTHREAD_MUTEX_INITIALIZER创建快速互斥锁
PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP创建递归互斥锁
PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP创建检错互斥锁要用默认的属性初始化互斥锁(快速锁),只需把attr设置为NULL。
在销毁互斥量的时候需要注意:
· 使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量无须销毁。
· 不要销毁一个已加锁的互斥量,或者是真正配合条件变量使用的互斥量。
· 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
这三种锁的区别:主要在于其他未占有互斥锁的线程在希望得到互斥锁时是否需要阻塞等待。
快速锁:是指调用线程会阻塞直至拥有互斥锁的线程解锁为止。
递归互斥锁:能够成功地返回并且增加调用线程在互斥上加锁的次数。
检错互斥锁:则为快速互斥锁的非阻塞版本,它会立即返回并返回一个错误信息。
三种锁分别适用于哪些场景?后期需要补充。
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); 返回值:成功:0; 失败:错误编号
线程调用pthread_mutex_lock时候,如果互斥量已经上锁,则线程会被阻塞直到互斥量解锁。
如果不希望线程阻塞,可以调用pthread_mutex_unlock。如果互斥量已经上锁,会立即返回EBUSY。
#include <pthread.h> #include <time.h> int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr); 返回值:成功:0;失败:错误编号
这里的时间是绝对时间,而不是相对时间。后面补充一个使用这个函数的例子
避免死锁
死锁(deallocks): 是指两个或两个以上的进程(线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或 系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。 由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程(线程)在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象 死锁。
http://blog.51cto.com/ab3813/1765422 这里对死锁进行了详细的说明。后面再详细学习。
银行家算法
1.当使用的是快速锁的时候,也就是默认锁。如果程序试图对一个已经加了锁的互斥量调用pthread_mutex_lock,程序就会被阻塞,而又因为拥有互斥量的这个线程正是现在被阻塞的线程,所以互斥量就永远也不会解锁了。就是一个线程出现多重加锁的情况。
解决方法:可以通过改变互斥量的属性来解决。用递归或检错互斥锁。
2.读写锁
读写有三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。而且一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
与互斥锁相比的优点:
当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权。但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有的线程释放它们的读锁为止。但当读写锁处于读模式锁住的状态,而这时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式请求一直得不到满足。
读写锁非常适合用于对数据结构读的次数远大于写的情况。
#include <pthread.h> int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); 返回值: 成功:0; 失败:错误编号
#include <pthread.h> int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); 返回值: 成功:0;失败:错误编号
#include <pthread.h> int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); 返回值: 成功:0;失败:错误编号
#include <pthread.h> #include <time.h> int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr); int pthread_rwlock_timedwrlock(pthread_rwlock_t restrict rwlock, const struct timespec *restrict tsptr); 返回值:成功:0;失败:错误编号 tsptr参数指向timespec结构 指定线程应该停止阻塞的时间。
注意:超时指定的是绝对时间,而不是相对时间。
实例:
#include <stdio.h> #include <unistd.h> #include <pthread.h> int counter; pthread_rwlock_t rwlock; /* 3个线程不定时写同一全局资源,5个线程不定时读同一全局资源 */ void *th_write(void *arg) { int t; int 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; }
3.自旋锁
自旋锁与互斥锁功能一样,唯一一点不同的就是互斥量阻塞后休眠让出cpu,而自旋锁阻塞后不会让出cpu,会一直忙等待,直到得到锁。因此会过多的占用CPU资源。
自旋锁在用户态使用的比较少,在内核使用的比较多。
自旋锁比较少用到,因此在这边就不进行介绍了。
4.条件变量
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。条件变量分 为两部分: 条件和变量。条件本身是由互斥量保护的。线程在改变条件状态前先要锁住互斥量。条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局 变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。条件的检测是在 互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
条件变量用来阻塞线程等待某个事件的发生,并且当等待的事件发生时,阻塞线程会被通知。互斥锁的缺点是只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。
条件变量的初始化:由pthread_cond_t数据类型表示的条件变量可以用两种方式进行初始化,可以用常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量。也可以动态的使用pthread_cond_init函数对它进行初始化。
#include <pthread.h> int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict arrt); int pthread_cond_destroy(pthread_cond_t *cond); 返回值: 成功:0;错误:错误编号使用默认属性的时候,将arrt设置为NULL。
尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr
值通常为NULL,且被忽略。
pthread_cond_destroy只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。
#include <pthread.h> int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict tspir); 返回值: 成功:0; 失败:错误编号
time的时间是绝对时间,不是相对时间
#include <pthread.h> int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond); 返回值: 成功:0;失败:错误编号
pthread_cond_signal函数至少能唤醒一个等待该条件的线程,而pthread_cond_broadcast函数则能唤醒等待该条件的所有线程。需要注意的是:一定要在改变条件状态以后再给线程发信号。
条件变量的使用可以分为两部分:
等待线程:
使用pthread_cond_wait前要先加锁;
pthread_cond_wait内部会解锁,然后等待条件变量被其它线程激活;
pthread_cond_wait被激活后会再自动加锁;
激活线程:
加锁(和等待线程用同一个锁);
pthread_cond_signal发送信号;
解锁;
应用实例:https://www.cnblogs.com/studystudent/p/3333882.html
实例:
/*借助条件变量模拟 生产者-消费者 问题*/
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.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) { //头指针为空,说明没有节点
pthread_cond_wait(&has_product, &lock);
}
mp = head;
head = mp->next; //模拟消费掉一个产品
pthread_mutex_unlock(&lock);
printf("-Consume %lu---%d\n", pthread_self(), mp->num);
free(mp);
sleep(rand() % 4);
}
}
void *producer(void *p)
{
struct msg *mp;
for (;;) {
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() % 4);
}
}
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_create(&cid, NULL, consumer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
条件变量的优点:
相较于mutex而言,条件变量可以减少竞争。如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。
有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。
红字1:
需要在条件变量进行判断时,将变量锁住,让其他线程不能修改此变量,这样就可以保证在判断的时候条件的变量的值是正确的。即互斥锁的作用不是为了保护条件变量,而是为了保护条件判断时共享变量的值不会被修改。
条件变量判断为什么使用while而不是if?
因为唤醒中存在虚假唤醒( spurious wakeup ),换言之,条件尚未满足, pthread_cond_wait 就返回了。在一些实现中,即使没有其他线程向条件变量发送信号,等待此条件变量的线程也有可能会醒来。
为什么还要把互斥锁作为参数传给 pthread_cond_wait 呢?
pthread_mutex_lock(&m) while(condition_is_false) { pthread_mutex_unlock(&m); // 解锁之后,等待之前,可能条件已经满足,信号已经发出,但是该信号可能会被错过 cond_wait(&cv); pthread_mutex_lock(&m); }
原因在于,上面的解锁和等待不是原子操作。解锁以后,调用 cond_wait 之前,如果已经有其他线程获取到了互斥量,并且满足了条件,同时发出了通知信号,那么 cond_wait 将错过这个信号,可能会导致线程永远处于阻塞状态。所以解锁加等待必须是一个原子性的操作,以确保已经注册到事件的等待队列之前,不会有其他线程可以获得互斥量。
那先注册等待事件,后释放锁不行吗?注意,条件等待是个阻塞型的接口,不单单是注册在事件的等待队列上,线程也会因此阻塞于此,从而导致互斥量无法释放,其他线程获取不到互斥量,也就无法通过改变共享数据使等待的条件得到满足,因此这就造成了死锁。
5.信号量
信号量是一个特殊类型的变量,这个信号量可以被增加或者减少,当信号量大于0时,代表资源可以被访问。当访问完后,信号量减1。当信号量为0时,要想访问信号量就要等待(阻塞)。根据信号量的值可以分为:二进制信号量和计数信号量。
信号量的创建/销毁:
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); int sem_destroy(sem_t *sem);
- sem:信号量
- pshared:0:线程同步 1:进程同步
- value:信号量初始值
- 返回值:成功返回0,错误返回错误码
信号量的访问/释放:
#include <semaphore.h> int sem_wait(sem_t *sem); int sem_post(sem_t *sem);
wait会对信号量减1,如果信号量大于0,就直接减1。如果信号量等于0,就会等待。
post会对信号量加1。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> #include <semaphore.h> void *thread_function(void *arg); sem_t g_sem; int main() { int ret; pthread_attr_t thread_attr; pthread_t thread; ret = pthread_attr_init(&thread_attr); if(ret != 0) { perror("ptherad attribute init failed"); exit(EXIT_FAILURE); } ret = sem_init(&g_sem, 0, 0); if(ret != 0) { perror("semaphore init failed"); exit(EXIT_FAILURE); } ret = pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED); if(ret != 0) { perror("setting thread detached state failure"); exit(EXIT_FAILURE); } ret = pthread_create(&thread, &thread_attr, thread_function, NULL); if(ret != 0) { perror("thread create failed"); exit(EXIT_FAILURE); } ret = pthread_attr_destroy(&thread_attr); if(ret !=0) { perror("thread attr destory failed"); exit(EXIT_FAILURE); } printf("ready to wait sem \n"); sem_wait(&g_sem); sleep(2); printf("the main thread is finish \n"); exit(EXIT_SUCCESS); } void *thread_function(void *arg) { sleep(3); printf("finish thread function \n"); sem_post(&g_sem); pthread_exit(NULL); }
6. 信号
这里需要注意的一点是:与线程有关的函数绝大多数名字都是以“pthread_”打头的 要使用这些函数,除了要引入相应的头文件,链接这些线程函数库时要使用编译器命令的-lpthread选项