线程同步
一、线程为什么要同步
1.线程的最大特点是资源的共享性
,多个线程都可对共享资源操作;
2.线程操作共享资源的先后顺序不确定;
3.处理器对存储器的操作一般不是原子操作。
二、多线程之间有几个特殊的临界资源:
全局数据
、堆区数据
、文件描述符
多线程之间共用。
三、处理方法
Linux下提供了多种方式来处理线程同步,最常用的是互斥锁
、条件变量
和信号量
。
3.1临界区
3.1.1概述
通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。是保证在某一时刻只有一个线程能访问数据
的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么 在有一个线程进入后其他所有试图访问此临界区的线程将被挂起
,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
3.1.2 操作原语:
EnterCriticalSection()
进入临界区
LeaveCriticalSection()
离开临界区
EnterCriticalSection()语句执行后代码将进入临界区以后无论发生什么,必须确保与之匹配的LeaveCriticalSection()都能够被执行到。否则临界区保护的共享资源将永远不会被释放。虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
3.1.3临界区的选定
临界区的选定因尽可能小,如果选定太大会影响程序的并行处理性能。
3.2互斥锁
3.2.1概述
互斥锁和临界区有些相似,完全控制临界资源,如果一个线程完成加锁操作,则其他线程无论如何都无法再完成加锁,也就无法对临界资源进行访问。
3.2.2锁机制
通过锁机制实现线程间的同步。
3.2.3操作原语
#include <pthread.h>
//1、初始化锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //静态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);//动态分配
//2、加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//3、解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//4、销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
3.2.4举例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000
//全局变量 可设置成静态
int counter; //本例创建两个线程,各自把counter增加5000次
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;//静态分配
void *doit(void *);
int main(int argc, char **argv)
{
pthread_t tidA, tidB;
//创建两个线程
pthread_create(&tidA, NULL, doit, NULL);
pthread_create(&tidB, NULL, doit, NULL);
//等待两个线程都终止
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
void *doit(void *vptr)
{
int i, val;
for (i = 0; i < NLOOP; i++)
{
//谁要操作全局变量谁拿锁 锁只有一把
//加锁
pthread_mutex_lock(&counter_mutex);
val = counter;
printf("%x: %d\n", (unsigned int)pthread_self(), val + 1);
counter = val + 1;
//解锁
pthread_mutex_unlock(&counter_mutex);
}
return NULL;
}
3.2死锁
1.同一个线程在拥有A锁的情况下再次请求获得A锁
2.线程一拥有A锁,请求获得B锁;线程二拥有B锁,请求获得A锁
3.3条件变量
3.3.1概述
1.与互斥锁不同,条件变量是用来等待
而不是用来上锁的。
2.条件变量用来自动阻塞一个线程
,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
3.条件变量分为两部分: 条件
和变量
。条件本身是由互斥量保护的。线程在改变条件状态前先要锁住互斥量。条件变量使我们可以睡眠等待某种条件出现。
4.条件变量是利用线程间共享的全局变量进行同步的一种机制
,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
3.3.2操作原语
#include<pthread.h>
//1、初始化条件变量。
pthread_cond_t cond = PTHREAD_COND_INITIALIER;//静态初始化
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);//动态初始化
/*2、等待条件成立。释放锁,同时阻塞等待条件变量为真才行。timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait)*/
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
//3、激活条件变量。
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond); //(激活所有等待线程)解除所有线程的阻塞
//4、清除条件变量。无线程等待,否则返回EBUSY
int pthread_cond_destroy(pthread_cond_t *cond)
3.3.3举例(生产者消费者模型)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_produce = PTHREAD_COND_INITIALIZER;
struct msg{
struct msg *next;
int num;
};
struct msg *head;//头指针
void *consumer(void *p)
{
struct msg *mp;
for(;;)
{
pthread_mutex_lock(&lock);
while(NULL==head)
{
pthread_cond_wait(&has_produce,&lock);
}
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock);
//消费掉
printf("consumer %d \n",mp->num);
free(mp);
sleep(rand()%5);
}
}
void *producer(void *p)
{
struct msg *mp;
for(;;)
{
mp = malloc(sizeof(struct msg));
mp->num = rand()%1000 + 1;
printf("Producer %d\n",mp->num);
pthread_mutex_lock(&lock);
//头插法
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
//唤醒阻塞线程
pthread_cond_signal(&has_produce);
sleep(rand()%2);
}
}
int main()
{
pthread_t pid,cid;
pthread_create(&pid, NULL, producer, NULL);//生产者
pthread_create(&cid, NULL, consumer, NULL);//消费者
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
3.3信号量
3.3.1概述
1.“信号量”的作用就象是代码周围的门卫
2.二进制信号量:一种最简单的一种信号量,只有“0”,“1”两种取值
3.计数信号量:有更大的取值范围,一般用于希望有限个线程去执行一段给定的代码
4.如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。信号量函数的名字都以"sem_
"开头。
5.头文件<semaphore.h>
3.3.2操作原语
1.信号量的初始化
作用:对给定的信号量对象进行初始化
int sem_init(sem_t *sem,int pshared,unsigned value);
/*
sem: 要进行初始化的信号量对象
pshared:控制着信号量的类型,如果值为0,表示它是当前进程的局部信号量;否则,其他进程就能够共享这个信号量
value:赋给信号量对象的一个整数类型的初始值
调用成功时 返回 0;
*/
2.释放信号量
作用:给信号量的值加上一个“1”,并通知其他等待线程。
int sem_post(sem_t *sem);
/*
sem: 初始化的信号量对象的指针作为参数,用来改变该对象的值,调用成功时返回 0;
这是一个“原子操作”-即同时对同一个信号量做加“1”操作的两个线程是不会冲突的。信号量的值永远会正确地加上一个“2”,因为有两个线程试图改变它
*/
3.等待信号量
作用:从信号量的值减去一个“1”,但它永远会先等待该信号量为一个非零值才开始做减法
int sem_wait(sem_t *sem);
/*
sem: 初始化的信号量对象的指针作为参数,用来改变该对象的值,调用成功时 返回 0;
也是一个“原子操作”。
*/
4.销毁信号量
作用:用完信号量后,对该信号量进行清理
int sem_destroy(sem_t *sem);
/*
sem: 初始化的信号量对象的指针作为参数,用来改变该对象的值,调用成功时 返回 0;
归还自己占有的一切资源,在清理信号量的时候如果还有线程在等待它,用户就会收到一个错误
*/
3.3.3举例
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
#define NUM 5
int queue[NUM];
sem_t blank_number, product_number;
void *producer(void *arg)
{
int p = 0;
while (1)
{
sem_wait(&blank_number);
queue[p] = rand() % 1000 + 1;
printf("Produce %d\n", queue[p]);
sem_post(&product_number);
p = (p+1)%NUM;//控制数组不越界
sleep(rand()%5);
}
}
void *consumer(void *arg)
{
int c = 0;
while (1)
{
sem_wait(&product_number);
printf("Consume %d\n", queue[c]);
queue[c] = 0;
sem_post(&blank_number);
c = (c+1)%NUM;
sleep(rand()%5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
//初始化信号量
sem_init(&blank_number, 0, NUM);
sem_init(&product_number, 0, 0);
//创建线程
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
//回收线程
pthread_join(pid, NULL);
pthread_join(cid, NULL);
//销毁信号量
sem_destroy(&blank_number);
sem_destroy(&product_number);
return 0;
}
参考:https://blog.csdn.net/xipiaoyouzi/article/details/52453274