线程(五)线程的同步和互斥——线程信号量
文章目录
- 线程
- 线程的同步和互斥
- 线程的同步和互斥--线程信号量
- 示例--使用线程信号量来控制线程执行的先后顺序
- 示例--使用信号量实现线程之间的互斥
- 示例--使用信号量实现线程之间的同步
- 死锁
- 线程状态转换
线程
线程的同步和互斥
线程的同步和互斥–线程信号量
上边讲了互斥的方式互斥锁、读写锁、自旋锁,同步的方式条件变量,实现线程之间的同步和互斥还有一种方式–信号量。
-
信号量从本质上是一个非负整数计数器,是共享资源的数目,通常被用来控制对共享资源的访问。
-
信号量可以实现线程的同步和互斥
-
通过
sem_post()
和sem_wait()
函数对信号量进行加减操作从而解决线程的同步和互斥。 -
信号量数据类型:
sem_t
案例:现图书馆购入10本Unix环境高级编程供学生借阅,这里的数量10就是共享资源的数量即信号量,如果有10位同学借走了这10本书,那么第11位同学想要再借阅的话就必须等待之前的10位同学中的任意一位将书归还否则它只能等待,这里的借书和还书就对应
sem_wait()
和sem_post()
两个函数,通过控制信号量的值就可以实现线程之间的同步和互斥。
信号量的初始化和销毁
#include <semaphore.h>int sem_init(sem_t *sem, int pshared, unsigned value);
int sem_destroy(sem_t *sem);/*功能:sem_init 对信号量进行初始化sem_destroy 对信号量进行销毁参数:sem 指向信号量的指针pshared 是否在进程间共享的标志,0为不共享(只在当前进程的多个线程中使用),1为共享(可以在多个进程中的多个线程中使用)value 信号量的初始值返回值:成功执行返回0,否则返回错误编码
*/
信号量的加和减操作
#include <semaphore.h>int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);/*功能:sem_post 增加信号量的值sem_wait 减少信号量的值sem_trywait sem_wait()的非阻塞版本参数:sem 指向信号量的指针返回值:成功执行返回0,否则返回错误编码
*/
- 调用
sem_post()
一次信号量作加1操作 - 调用
sem_wait()
一次信号量作减1操作 - 当线程调用
sem_wait()
后,若信号量的值小于0则线程阻塞。只有其他线程在调用sem_post()
对信号量作加1操作后并且其值大于或等于0时,阻塞的线程才能运行(也就是说:当此时信号量的值为0时,若调用sem_wait()
对信号量作减1操作,那么信号量的值小于0线程阻塞,此时必须等待另外的线程调用sem_post()
函数对信号量作加1操作才能够使得被阻塞的线程能够对信号量作减1操作)。
示例–使用线程信号量来控制线程执行的先后顺序
#include "header.h"sem_t sem1;
sem_t sem2;void* exec_func1(void *arg)
{sem_wait(&sem1);printf("[thread id:%lx] [fun:func1] is running\n",pthread_self());pthread_exit(NULL);
}void* exec_func2(void *arg)
{sem_wait(&sem2); //对信号量进行减1操作,如果减1后值小于0就阻塞等待其他线程调用sem_post对信号量加1操作printf("[thread id:%lx] [fun:func2] is running\n",pthread_self());sem_post(&sem1); //唤醒被阻塞的线程1pthread_exit(NULL);
}void* exec_func3(void *arg)
{printf("[thread id:%lx] [fun:func3] is running\n",pthread_self());sem_post(&sem2);//对信号量的值加1,使得被此信号量阻塞的线程能够运行pthread_exit(NULL);
}int main(void)
{int err = -1;pthread_t func1, func2, func3;//初始化信号量,信号量的值为0,只在此进程中的多个线程中共享sem_init(&sem1, 0, 0); sem_init(&sem2, 0, 0); if((err = pthread_create(&func1, NULL, exec_func1, NULL)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}if((err = pthread_create(&func2, NULL, exec_func2, NULL)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}if((err = pthread_create(&func3, NULL, exec_func3, NULL)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}pthread_join(func1, NULL);pthread_join(func2, NULL);pthread_join(func3, NULL);sem_destroy(&sem1); //销毁信号量sem_destroy(&sem2);return 0;
}
通过编译执行可以发现使用线程信号量可以用来控制线程执行的先后顺序,代码在刚开始使用sem_init()
函数将信号量的值初始化为0,所以一旦线程func1
调用sem_wait()
函数对信号量进行减1操作就会被阻塞,线程func2
也是同样的道理,它们都在等待某一个线程通过调用sem_post()
函数对信号量执行加1操作来唤醒被信号量阻塞的线程。通过线程func3
调用sem_post()
函数来唤醒线程func2
,线程func2
调用sem_post()
函数来唤醒线程func1
来控制线程之间的执行顺序。
示例–使用信号量实现线程之间的互斥
//account.c#include "account.h"
#include "header.h"Account* create_account(int acc_num, double balance) //创建账户
{Account *a = (Account*)malloc(sizeof(Account));assert(a != NULL);a->acc_num = acc_num;a->balance = balance;//pthread_mutex_init(&a->mutex, NULL);sem_init(&a->sem, 0, 1); //初始化线程信号量,信号量值为1return a;
}double withdrawal(Account *a, double amount)
{assert(a != NULL);//P(1)操作,对信号量作减1操作 sem_wait(&a->sem);
// pthread_mutex_lock(&a->mutex);if(amount <= 0 || amount > a->balance){//V(1)操作,对信号量作加1操作sem_post(&a->sem);//pthread_mutex_unlock(&a->mutex);return 0.0;}double balance = a->balance;sleep(1); //模拟ATM机延迟balance -= amount;a->balance = balance; //将余额balance取出amount后再存放回a账户 //V(1)操作,对信号量作加1操作sem_post(&a->sem);// pthread_mutex_unlock(&a->mutex);return amount;
}double deposit(Account *a, double amount)
{assert(a != NULL);sem_wait(&a->sem);//pthread_mutex_lock(&a->mutex);if(amount <= 0){sem_post(&a->sem);//pthread_mutex_unlock(&a->mutex);return 0.0;}double balance = a->balance;sleep(1);balance += amount;a->balance = balance;sem_post(&a->sem);//pthread_mutex_unlock(&a->mutex);return amount;
}double get_balance(Account *a)
{assert(a != NULL);sem_wait(&a->sem);//pthread_mutex_lock(&a->mutex);double balance = a->balance;sem_post(&a->sem);//pthread_mutex_unlock(&a->mutex);return balance;
}void destroy_account(Account *a)
{assert(a != NULL);//pthread_mutex_destroy(&a->mutex);sem_destroy(&a->sem); //销毁线程信号量free(a); //将在堆上开辟出来的空间释放a = NULL;
}
通过编译执行可以发现通过信号量也能够像之前的互斥锁那样实现线程之间的互斥,在执行对应的函数时对信号量进行减1操作,然后再执行完后对信号量进行加1操作。这样如果中间被别的线程打断执行,那么由于再次执行减1操作后信号量的值小于1,那么对应的线程就会被阻塞,直到前一个线程对信号量执行加1操作后才会继续执行。这样不论哪一个线程先执行,都能够保证同一时间只能有一个线程去操作账户,从而保证了共享资源的安全性。
示例–使用信号量实现线程之间的同步
#include "header.h"typedef struct
{int result;sem_t sem;
}OperArg;void* cal_func(void *arg)
{OperArg *s = (OperArg*)arg;int i = 1;for(; i<= 100; i++){s->result += i;}printf("[cal thread id:%lx] write %d to the structure\n",pthread_self(),s->result);sem_post(&s->sem); //对信号量作加1操作,唤醒被此信号量阻塞的线程pthread_exit(NULL);
}void* get_func(void *arg)
{OperArg *s = (OperArg*)arg;sem_wait(&s->sem); //由于将信号量初始化为0,所以当执行此线程时会被阻塞直到另外一个线程调用sem_post将信号量的值加1printf("[get thread id:%lx] read %d from the structure\n",pthread_self(),s->result);pthread_exit(NULL);
}int main(void)
{int err = -1;pthread_t cal, get; OperArg arg;memset(&arg, 0, sizeof(arg)); //初始化结构体sem_init(&arg.sem, 0, 0); //初始化信号量,信号量的值为0只用于当前进程的若干线程if((err = pthread_create(&cal, NULL, cal_func, (void*)&arg)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}if((err = pthread_create(&get, NULL, get_func, (void*)&arg)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}pthread_join(cal, NULL);pthread_join(get, NULL);sem_destroy(&arg.sem); //销毁信号量return 0;
}
通过编译执行可以发现使用信号量可以用来实现线程之间的同步。在代码中要cal_func
线程先计算出结果存放到result
中才轮到get_func
线程获取result
的值。具体的做法就是阻塞get_func
线程直到cal_func
线程将结果计算出来,也就是说刚开始的信号量的值为0,那么当get_func
线程调用sem_wait()
去对信号量作减1操作的时候就会被阻塞直到cal_func
线程调用sem_post()
函数对信号量作加1操作才会执行,那么此时也就意味着已经将结果计算出来并放入到结构体中可以由get_func
线程获取了,由此实现了线程之间的同步。
死锁
死锁的产生原因:
- 资源竞争
- 多个线程需要竞争有限的资源(如锁、信号量、内存等),并且它们以不同的顺序请求这些资源,可能导致死锁,例如:线程A持有资源1并请求资源2,而线程B持有资源2并请求资源1,形成循环等待
- 持有并等待
- 一个线程在已持有某个资源的同时请求其他的资源,这种情况很容易导致死锁,因为其他线程可能会占用这些请求的资源,形成等待状态。例如:线程A已经获得了资源X,并请求资源Y,而线程B已经获得了资源Y,并请求资源X。
- 不可剥夺性
- 线程持有的资源不能被强制剥夺,只有当线程释放它们时,其他线程才能获取这些资源。这样的情况下,如果多个线程相互等待彼此持有的资源,就可能导致死锁。
- 循环等待
- 一组线程形成一个等待环,其中每个线程都在等待下一个线程所持有的资源。这种条件是死锁的必要条件。例如:线程1等待线程2持有的资源,线程2等待线程3持有的资源,而线程3又在等待线程1持有的资源。
示例–死锁的产生
#include "header.h"typedef struct
{int value;pthread_mutex_t mutex;
}ResourceA;typedef struct
{int value;pthread_mutex_t mutex;
}ResourceB;typedef struct
{ResourceA *a;ResourceB *b;
}Resource;void* exec_func1(void *arg)
{Resource *s = (Resource*)arg;//对共享资源A进行上锁pthread_mutex_lock(&s->a->mutex);sleep(1);printf("func1 thread id:%lx waiting for resourceB....\n",pthread_self());//对共享资源B进行上锁pthread_mutex_lock(&s->b->mutex); printf("ResourceA value = %d\n",s->a->value);printf("ResourceB value = %d\n",s->b->value);pthread_mutex_unlock(&s->b->mutex);pthread_mutex_unlock(&s->a->mutex);pthread_exit(NULL);
}void* exec_func2(void *arg)
{Resource *s = (Resource*)arg;//对共享资源B进行上锁pthread_mutex_lock(&s->b->mutex);sleep(1);printf("func2 thread id:%lx waiting for resourceA....\n",pthread_self());//对共享资源A进行上锁pthread_mutex_lock(&s->a->mutex);printf("ResourceA value = %d\n",s->a->value);printf("ResourceB value = %d\n",s->b->value);pthread_mutex_unlock(&s->a->mutex);pthread_mutex_unlock(&s->b->mutex);pthread_exit(NULL);
}int main(void)
{int err = -1;pthread_t func1, func2;ResourceA a;ResourceB b;Resource arg = {&a, &b};a.value = 100;b.value = 200;pthread_mutex_init(&a.mutex, NULL);pthread_mutex_init(&b.mutex, NULL);if((err = pthread_create(&func1, NULL, exec_func1, (void*)&arg)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}if((err = pthread_create(&func2, NULL, exec_func2, (void*)&arg)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}pthread_join(func1, NULL);pthread_join(func2, NULL);pthread_mutex_destroy(&a.mutex);pthread_mutex_destroy(&b.mutex);return 0;
}
通过编译执行可以发现线程陷入了死循环,分析其代码造成死循环的主要原因是:线程func1
对资源A进行上锁后延时1s,此时会轮到线程func2
执行,然后线程func2
会对资源B进行上锁然后延时1s,当再次轮到线程func1
运行的时候,线程func1
尝试去获取资源B的锁,但是资源B的锁已经被func2
所获取,所以此时线程func1
就会被阻塞,而线程func2
同理想要获取资源A的锁也会被阻塞,所以两个线程在已经持有一个锁的情况下想要获取对方的锁就造成了死锁。
解决死锁的办法:
- 按相同的次序锁定相应的共享资源
- 使用函数
pthread_mutex_trylock()
,当它首次上锁的时候,如果该互斥锁没有被别的线程锁定,则调用成功,锁会被获取。如果再次调用的时候,如果该互斥锁已经被其他的线程锁定,pthread_mutex_trylock()
会立即返回,并不会阻塞。此时,函数返回的错误码是EBUSY
,表示互斥锁正在被使用,当前线程无法获取互斥锁。
示例–使用相同次序锁定解决死锁
#include "header.h"typedef struct
{int value;pthread_mutex_t mutex;
}ResourceA;typedef struct
{int value;pthread_mutex_t mutex;
}ResourceB;typedef struct
{ResourceA *a;ResourceB *b;
}Resource;void* exec_func1(void *arg)
{Resource *s = (Resource*)arg;//对共享资源A进行上锁pthread_mutex_lock(&s->a->mutex);sleep(1);printf("func1 thread id:%lx waiting for resourceB....\n",pthread_self());//对共享资源B进行上锁pthread_mutex_lock(&s->b->mutex); printf("ResourceA value = %d\n",s->a->value);printf("ResourceB value = %d\n",s->b->value);pthread_mutex_unlock(&s->b->mutex);pthread_mutex_unlock(&s->a->mutex);pthread_exit(NULL);
}void* exec_func2(void *arg)
{Resource *s = (Resource*)arg;pthread_mutex_lock(&s->a->mutex);sleep(1);printf("func2 thread id:%lx waiting for ResourceB\n",pthread_self());pthread_mutex_lock(&s->b->mutex);printf("ResourceA value = %d\n",s->a->value);printf("ResourceB value = %d\n",s->b->value);pthread_mutex_unlock(&s->b->mutex);pthread_mutex_unlock(&s->a->mutex);/*//对共享资源B进行上锁pthread_mutex_lock(&s->b->mutex);sleep(1);printf("func2 thread id:%lx waiting for resourceA....\n",pthread_self());//对共享资源A进行上锁pthread_mutex_lock(&s->a->mutex);printf("ResourceA value = %d\n",s->a->value);printf("ResourceB value = %d\n",s->b->value);pthread_mutex_unlock(&s->a->mutex);pthread_mutex_unlock(&s->b->mutex);
*/pthread_exit(NULL);
}int main(void)
{int err = -1;pthread_t func1, func2;ResourceA a;ResourceB b;Resource arg = {&a, &b};a.value = 100;b.value = 200;pthread_mutex_init(&a.mutex, NULL);pthread_mutex_init(&b.mutex, NULL);if((err = pthread_create(&func1, NULL, exec_func1, (void*)&arg)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}if((err = pthread_create(&func2, NULL, exec_func2, (void*)&arg)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}pthread_join(func1, NULL);pthread_join(func2, NULL);pthread_mutex_destroy(&a.mutex);pthread_mutex_destroy(&b.mutex);return 0;
}
通过编译执行可以看出通过修改锁定的次序后线程没有再次进入到死锁状态了。分析代码:当线程func1
开始执行的时候,先对资源A进行上锁,然后进入到睡眠状态轮到线程func2
执行,此时线程func2
想要获取互斥锁,但是互斥锁已经被线程func1
获取并锁定,所以线程func2
没有获取到互斥锁进入到阻塞状态,直到线程func1
执行完释放锁以后线程func2
才能够获取锁并执行。经过这个操作以后就能够实现避免死锁现象,并且保证线程的安全性。