线程同步
- 临界资源:一次仅允许一个进程使用的共享资源
- 临界区:每个进程中访问临界资源的那段程序。
进程进入临界区的调度原则是:
- 如果有若干进程要求进程空闲的临界区,每次只允许一个进程进入临界区,进入后不允许其他进程进入。
- 任何时候,处于临界区内的进程不可多余一个。如已有进程进入自己的临界区,则其他所有试图进入临界区的进程必须等待。
- 进入临界区的进程要在有限的时间内退出,以便其他进程能及时进程进入自己的临界区
- 如果进程不能进入自己的临界区,则、应该让出CPU,避免进程出现“忙等”现象。
- 当一个进程可以修改的变量,其他线程也可以读取或者修改的时候,我们需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。
互斥量
- 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <error.h>
5
6 int ticket=100;
7
8 void *thread(void *arg)
9 {
10 while(ticket>0)
11 {
12 usleep(100);
13 printf("child get ticket\n");
14 ticket--;
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <error.h>
5
6 int ticket=100;
7
8 void *thread(void *arg)
9 {
10 while(ticket>0)
11 {
12 usleep(100);
13 printf("child get ticket\n");
14 ticket--;
15 printf("sell ticket = %d\n",ticket);
16 }
17 }
18
19 int main()
20 {
21 pthread_t tid;
22 int ret=pthread_create(&tid,NULL,thread,(void *)tid);
23 if(ret!=0)
24 perror("pthread_create"),exit(1);
25 while(ticket>0)
26 {
27 usleep(100);
28 printf("main get ticket\n");
29 ticket--;
30 printf("sell ticket = %d\n",ticket);
31 }
32 return 0;
33 }
我们可以看到票数ticket出现-1的情况,why???
- if 语句判断条件为真以后,代码可以并发的切换到其他线程
- usleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
- ticket操作本身不是一个原子操作,其中包括:
- load:将共享变量ticket从内存加载到寄存器中
- update: 更新寄存器⾥里⾯面的值,执行-1操作
- store:将新值,从寄存器写回共享变量ticket的内存地址
要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
互斥量(mutex)本质上说是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。
- 对互斥量加锁后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。
- 如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态。
- 第一个变为运行的线程就可以对互斥量加锁,其他线程就会看到互斥量依然是锁着的,只能回去再次等待它重新变为可用。
互斥量接口
互斥量初始化
- 方法1,静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
- 方法2,动态分配
原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr _t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
返回值:
若成功,返回0;否则,返回错误编号
互斥量销毁
如果使用动态分配互斥量,通过调用malloc函数,所以在释放内存前要调用销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保面不会有线程再尝试加锁
- 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用pthread_ lock 时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞,等待互斥量解锁
如果不希望被阻塞,可以使用 pthread_mutex_trylock 尝试对互斥量进行加锁。如果调用 pthread_mutex_trylock 时互斥量处于未锁住状态,那么 pthread_mutex_trylock 将锁住互斥量,不会出现阻塞直接返回0,否则 pthread_mutex_trylock 就会失败,不能锁住互斥量,返回EBUSY.
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <error.h>
5
6 int ticket=100;
7 pthread_mutex_t mutex;
8
9 void *thread(void *arg)
10 {
11 pthread_mutex_lock(&mutex);
12 while(ticket>0)
13 {
14 usleep(100);
15 printf("child get ticket\n");
16 ticket--;
17 printf("sell ticket = %d\n",ticket);
18 }
19 pthread_mutex_unlock(&mutex);
20 }
21
22 int main()
23 {
24 pthread_t tid;
25 pthread_mutex_init(&mutex,NULL);
26 int ret=pthread_create(&tid,NULL,thread,(void *)tid);
27 if(ret!=0)
28 perror("pthread_create"),exit(1);
29 pthread_mutex_lock(&mutex);
30 while(ticket>0)
31 {
32
33 usleep(100);
34 printf("main get ticket\n");
35 ticket--;
36 printf("sell ticket = %d\n",ticket);
37 }
38 pthread_mutex_unlock(&mutex);
39 return 0;
40 }
优化:
- 在票数ticket<=0的时候,直接(主动)退出;
- 这里要注意,在任何退出地方都需要解锁;