Linux线程同步方法

多线程已经成为服务器开发不可或缺的重要知识点了,那么怎样协调各个线程之间的工作就变得至关重要,于是这篇文章就来总结一下线程同步的方法。

什么是线程同步?

“同”字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行。线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。

“同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。

因此, 所有“多个控制流,共同操作一个共享资源” 的情况,都需要同步。

那么怎样达到线程同步就需要一些方法,这里讲解了互斥量,条件变量,信号量线程同步方法。

互斥锁mutex

互斥锁的原理很容易理解,我们给一个共享资源分配一把锁,每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。

通过这样:资源还是共享的,线程间也还是竞争的,但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。但,应注意:同一时刻,只能有一个线程持有该锁。

我们先用一个用互斥量使用步骤开始讲解:

1,pthread_mutex_t 函数 ,创造一个互斥锁。

2,pthread_mutex_init 函数,初始化。

3,pthread_mutex_lock / pthread_mutex_trylock 函数,加锁。

4,访问共享数据

5,pthread_mutex_unlock 函数,解锁
6,pthread_mutex_destroy 函数,销毁锁


pthread_mutex_t  mutex;

没啥好说的,就是创造一个互斥锁mutex。

pthread_mutex_init 函数

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);  函数作用是:初始化一个互斥锁(互斥量) ---> 初值可看作 1

参 1:传出参数,调用时应传 &mutex。

restrict 关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改。

参 2:互斥量属性。是一个传入参数,通常传 NULL,选用默认属性(线程间共享)。 参 APUE.12.4 同步属性

pthread_mutex_lock 函数
int pthread_mutex_lock(pthread_mutex_t *mutex);  函数作用是:加锁。可理解为将 mutex--(或 -1),操作后 mutex 的值为 0。

另外,lock与trylock的区别是:lock加锁失败会导致阻塞,trylock加锁失败不会阻塞会返回-1 。

pthread_mutex_unlock 函数

int pthread_mutex_unlock(pthread_mutex_t *mutex);  解锁。可理解为将 mutex ++(或 +1),操作后 mutex 的值为 1。

pthread_mutex_destroy 函数
int pthread_mutex_destroy(pthread_mutex_t *mutex);  销毁一个互斥锁

lock 与 unlock :

lock 尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。

unlock 主动解锁函数, 同时将阻塞在该锁上的所有线程 全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。

 1 #include<stdio.h>
 2 #include<stdlib.h>
 3 #include<pthread.h>
 4 #include<stdlib.h>
 5 #include<unistd.h>
 6 
 7 //互斥锁设为全局变量
 8 pthread_mutex_t mutex;
 9 
10 //子线程
11 void *tfn(void *arg) {
12     srand(time(NULL));
13     while (1) {
14         pthread_mutex_lock(&mutex);    //加锁,不行则阻塞
15         printf("hello ");
16         printf("world
");
17         pthread_mutex_unlock(&mutex);
18         sleep(rand()%3);
19     }
20     return NULL;
21 }
22 
23 int main()
24 {
25     pthread_t tid;
26     srand(time(NULL));
27     
28     pthread_mutex_init(&mutex,NULL);    //初始化互斥锁
29 
30     pthread_create(&tid,NULL,tfn,NULL);    //创造子线程
31 
32     //主线程
33     while (1) {
34         pthread_mutex_lock(&mutex);    //互斥锁加锁,不行则阻塞
35         printf("HELLO ");
36         printf("WORLD
");
37         pthread_mutex_unlock(&mutex);    //互斥锁解锁
38         sleep(rand()%3);
39     }
40     
41     //最后记得回收子线程,销毁互斥锁
42     pthread_join(tid,NULL);
43     pthread_mutex_destroy(&mutex);
44     return 0;
45 }
互斥锁测试

互斥锁读写锁

条件变量

条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。

函数类似互斥锁,我们也从条件变量使用步骤开始:

1,pthread_cond_t 函数, 用于定义条件变量

2,pthread_cond_init 函数

3,pthread_cond_wait  /  pthread_cond_timedwait 函数

4,pthread_cond_signal  /  pthread_cond_broadcast 函数

5,唤醒之后,重新申请互斥锁,然后操作共享资源

6,pthread_cond_destroy 函数

pthread_cond_init 函数

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);  初始化一个条件变量

参1:创造的条件变量  参2:attr 表条件变量属性,通常为默认值,传 NULL 即可

 

pthread_cond_wait 函数
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_timedwait 函数

int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec*restrict abstime);  限时等待一个条件变量

与pthread_cond_wait函数相比就是多了struct timespec这个结构体:

struct timespec {
time_t tv_sec;   /* seconds */ 秒
long tv_nsec;   /* nanosecondes*/ 纳秒 ,注意这里是纳秒,timeval这个结构体这里是微秒
}

并且要注意这里的abstime是绝对时间,所以正确用法是:

time_t cur = time(NULL);   获取当前时间。
struct timespec t;   定义 timespec 结构体变量 t
t.tv_sec = cur+1;   定时 1 秒
pthread_cond_timedwait (&cond, &mutex, &t);   传参

pthread_cond_signal 函数
int pthread_cond_signal(pthread_cond_t *cond);  唤醒至少一个阻塞在条件变量上的线程

tpthread_cond_broadcast 函数
int pthread_cond_broadcast(pthread_cond_t *cond);  唤醒全部阻塞在条件变量上的线程

 1 #include<stdio.h>
 2 #include<stdlib.h>
 3 #include<string.h>
 4 #include<unistd.h>
 5 #include<errno.h>
 6 #include<pthread.h>
 7 
 8 //线程不能perror,有自己独特的错误处理方式
 9 void error_thread(int ret,char *str) {
10     if (ret!=0) {
11         fprintf(stderr,"%s:%s
",str,strerror(ret));
12         pthread_exit(NULL);
13     }
14 }
15 
16 //把生产的物品做成链表
17 struct msg{
18     int num;
19     struct msg *next;
20 };
21 struct msg *head;
22 
23 pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;    //互斥量
24 pthread_cond_t has_data=PTHREAD_COND_INITIALIZER;    //条件变量
25 
26 //生产者线程
27 void *producer(void *arg) {
28     while (1) {
29         //生产者生产一个数据
30         struct msg *mp=malloc(sizeof(struct msg));
31         mp->num = rand()%1000+1;
32         printf("--produce %d
",mp->num);
33         
34         //利用互斥量对公共区域做互斥
35         pthread_mutex_lock(&mutex);    //加互斥锁
36         mp->next=head;            //写公共区域
37         head=mp;
38         pthread_mutex_unlock(&mutex);    //解互斥锁
39 
40         //条件变量:唤醒阻塞在条件变量has_data的线程
41         pthread_cond_signal(&has_data);
42 
43         sleep(rand()%3);
44     }
45     return NULL;
46 }
47 
48 //消费者线程
49 void *consumer(void *arg) {
50     while (1) {
51         struct msg *mp;
52 
53         //
54         pthread_mutex_lock(&mutex);
55         while (head==NULL) 
56             pthread_cond_wait(&has_data,&mutex);
57 
58         //消费生产品,读写公共区域
59         mp=head;
60         head=mp->next;
61         printf("-------------consumer id %lu :%d
",pthread_self(),mp->num);
62         free(mp);
63         
64         sleep(rand()%3);
65     }
66     return NULL;
67 }
68 
69 int main(int argc,char *argv[]) {
70     int ret;
71     pthread_t pid,cid;
72 
73     srand(time(NULL));
74 
75     ret=pthread_create(&pid,NULL,producer,NULL);        //生产者
76     if (ret!=0)
77         err_thread(ret,"pthread_create producer error");
78 
79     ret=pthread_create(&cid,NULL,consumer,NULL);        //消费者
80     if (ret!=0)
81         err_thread(ret,"pthread_create producer error");
82 
83     return 0;
84 }
条件变量解决生产者消费者问题

从生产者消费者问题我们可以看出条件变量配合使用的一点好处:

相较于 mutex 而言,条件变量可以减少竞争。

如直接使用 mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。

信号量

信号量可以看作是进化版的互斥锁(1 --> N),由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。

信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。

同样的,我们以使用步骤开始讲解:

1,sem_t sem;   规定信号量 sem 不能 < 0。

2,sem_init 函数  初始化

3,sem_wait  /  sem_trywait  /  sem_timedwait 函数  ,信号量尝试减一,准备访问共享资源

4,访问操作共享资源

5,sem_post 函数  ,访问完成,信号量加一

6,sem_destroy 函数  销毁信号量

sem_init 函数
int sem_init(sem_t *sem, int pshared, unsigned int value);  初始化一个信号量
参 1:sem 信号量
参 2:pshared 取 0 用于线程间;取非 0(一般为 1)用于进程间
参 3:value 指定信号量初值>=0

sem_wait 函数
int sem_wait(sem_t *sem);  给信号量加锁 --

sem_tryt wait 函数
int sem_trywait(sem_t *sem);  尝试对信号量加锁 -- (与 sem_wait 的区别类比 lock 和 trylock,即阻塞与非阻塞)

sem_timedwait 函数
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);  限时尝试对信号量加锁 --
参 2:abs_timeout 采用的是绝对时间。  注意这里也是timespec,参考上面条件变量,也是绝对时间+纳秒。

sem_post 函数
int sem_post(sem_t *sem);  给信号量解锁 ++

sem_destroy 函数
int sem_destroy(sem_t *sem);  销毁一个信号量

 1 #include<stdio.h>
 2 #include<stdlib.h>
 3 #include<unistd.h>
 4 #include<pthread.h>
 5 #include<semaphore.h>
 6 
 7 #define NUM 5
 8 
 9 int queue[NUM];
10 sem_t blank_number,product_number;
11 
12 void *producer(void *arg) {
13     int i=0;
14     while (1) {
15         //这里是信号量处理生产过程
16         sem_wait(&blank_number);    //空位数信号量--
17         queue[i]=rand()%1000+1;    //开始生产
18         printf("----Produce----%d
",queue[i]);
19         sem_post(&product_number);    //产品数信号量++
20 
21         i=(i+1)%NUM;
22         sleep(rand()%1);
23     }
24 }
25 
26 void *consumer(void *arg) {
27     int i=0;
28     while (1) {
29         //这里是信号量处理消费过程
30         sem_wait(&product_number);    //产品数信号量--
31         //开始消费
32         printf("---consume--%d
",queue[i]);
33         queue[i]=0;
34         sem_post(&blank_number);    //空位数信号量++
35 
36         i=(i+1)%NUM;
37         sleep(rand()%3);
38     }
39 }
40 
41 int main(int argc,char *argv[])
42 {
43     pthread_t pid,cid;
44 
45     //参二0为线程1为进程,参三可以理解为信号量初始值
46     sem_init(&blank_number,0,NUM);    //空位数
47     sem_init(&product_number,0,0);    //产品数
48 
49     //创建生产者消费者线程
50     pthread_create(&pid,NULL,producer,NULL);    
51     pthread_create(&cid,NULL,consumer,NULL);
52 
53     //回收线程
54     pthread_join(pid,NULL);
55     pthread_join(cid,NULL);
56 
57     //销毁信号量
58     sem_destroy(&blank_number);
59     sem_destroy(&product_number);
60     
61     return 0;
62 }
信号量解决生产者消费者问题
原文地址:https://www.cnblogs.com/clno1/p/12942972.html