Bootstrap

线程同步笔记

线程同步

1. 为什么需要线程同步

①线程同步为了对共享资源的访问进行保护(多个线程操作一个全局变量)
②保护的目的是为了解决数据一致性的问题。
如果其他线程不会对变量进行修改和读取那么不存在数据一致性问题
如果其他线程对变量只能读那么也不存在数据一致性问题
如果其他线程可以对变量进行修改那么一定存在数据一致性问题
③出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问(同时访问)

2. 如何解决线程数据一致性问题?

在这里插入图片描述

互斥锁、条件变量、自旋锁以及读写锁等

3. 互斥锁

互斥锁(mutex)又叫互斥量,从本质上说是一把锁,在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁);

3.1 初始化

①使用 PTHREAD_MUTEX_INITIALIZER 宏初始化互斥锁

# define PTHREAD_MUTEX_INITIALIZER \
 { { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

②使用 pthread_mutex_init()函数初始化互斥锁
但需要先定义互斥锁再初始化时,需要动态分配互斥锁时需要用到pthread_mutex_init()

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

在这里插入图片描述

3.2 互斥锁加锁和解锁

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

调用 pthread_mutex_lock()函数对互斥锁进行上锁,如果互斥锁处于未锁定状态,则此次调用会上锁成功,函数调用将立马返回;如果互斥锁此时已经被其它线程锁定了,那么调用pthread_mutex_lock()会一直阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。

3.3 pthread_mutex_trylock()函数

尝试上锁,当准备上锁时,如果已经被其他线程上锁,那么不会阻塞而是返回错误码

#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//成功返回0,失败返回EBUSY
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 10

int number;

pthread_mutex_t mutex;

void *callback1(void *arg){

	for(int i = 0;i<MAX;i++){
        while(pthread_mutex_trylock(&mutex));
		//pthread_mutex_lock(&mutex);	
		int cur = number;
		cur++;
		usleep(10);
		number = cur;
		printf("pthread A,id = %ld,number = %d\n",pthread_self(),number);
		pthread_mutex_unlock(&mutex);
	}
    return (void *)0;
}

void *callback2(void *arg){

	for(int i = 0;i<MAX;i++){
        while(pthread_mutex_trylock(&mutex));
		//pthread_mutex_lock(&mutex);	
		int cur = number;
		cur++;
		number = cur;
		printf("pthread B,id = %ld,number = %d\n",pthread_self(),number);
		pthread_mutex_unlock(&mutex);
		usleep(2);
	}
    return (void *)0;
}

int main(){

	pthread_t p1,p2;
	
	pthread_mutex_init(&mutex,NULL);

	pthread_create(&p1,NULL,callback1,NULL);
	pthread_create(&p2,NULL,callback2,NULL);

	pthread_join(p1,NULL);
	pthread_join(p2,NULL);

	pthread_mutex_destroy(&mutex);

	exit(0);
}

3.4 销毁互斥锁

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);

6.5 死锁的所有情况和解决办法

线程死锁的必要条件:

互斥条件:
资源只能被一个线程占用,如果其他线程请求获取该资源,则请求者只能等待,直到占用资源的线程释放该资源
请求并持有条件:
指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新的资源已被其他线程占用,所以当前线程会被阻塞,但阻塞的同时不释放自己获取的资源
不可剥夺条件:
获取到的资源在自己使用完之前不能被其他线程抢占,只能在使用完之后释放
环路等待条件:
发生死锁的时候必然存在一个线程-资源的环形链,即线程集合{T0,T1,T2,…Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,…Tn正在等待T1占用的资源

一直阻塞出不来,会造成所有线程被阻塞,并且线程无法解开

  • 加锁之后忘记解锁
//场景一
pthread_mutex_lock(&mutex);
.......
//忘记解锁

//场景二
pthread_mutex_lock(&mutex);

if1{
	return //使用return会导致死锁
}

pthread_mutex_unlock(&mutex);


  • 重复加锁造成死锁
//场景一
void *funA(){
	pthread_mutex_lock(&mutex);
	...
	pthread_mutex_lock(&mutex);
	//重复加锁
}


//场景二
void *funA(){
	pthread_mutex_lock(&mutex);
	........
	pthread_mutex_unlock(&mutex);
}

void *funB(){
	pthread_mutex_lock(&mutex);
	funA();
	........
	pthread_mutex_unlock(&mutex);
}
//调用B时,A阻塞,调用funA会导致重复加锁

在这里插入图片描述

  • 在程序中有多个共享资源,因此有很多把锁,随意加锁,导致相互被阻塞

    场景描述:
    1,有两个共享资源:X,Y,x对应锁A,Y对应锁B
    -线程A访问资源X,加锁A
    -线程B访问资源Y,加锁Y
    2,线程A要访问资源Y,线程B要访问资源X,因为资源X和Y已经被对应的锁锁住了,因此两个线程被阻塞
    - 线程A被锁B阻塞了,无法打开A锁
    - 线程B被锁A阻塞了,无法打开B锁

    如何避免:

    • 避免多次锁定,多检查
    • 对共享资源访问完毕后,一定要解锁,或者加锁的使用trylock
    • 如果有多把锁,可以控制对锁的顺序
    • 死锁检测模块(线程属性)
	pthread_mutex_t mutex;
	pthread_mutexattr_t attr;
	/* 初始化互斥锁属性对象 */
	pthread_mutexattr_init(&attr);
	/* 将类型属性设置为 PTHREAD_MUTEX_NORMAL */
	pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
	/* 初始化互斥锁 */
	pthread_mutex_init(&mutex, &attr);
	......
	/* 使用完之后 */
	pthread_mutexattr_destroy(&attr);
	pthread_mutex_destroy(&mutex);

4. 条件变量

条件变量用于自动阻塞线程,知道某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。

两个动作:
⚫ 一个线程等待某个条件满足而被阻塞;
⚫ 另一个线程中,条件满足时发出“信号”。

没有条件变量测试(新线程会一直判断全局变量是否大于0,对CPU资源造成很大浪费):

#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


static pthread_mutex_t mutex;
static int g_count=0;

static void *callback(void *arg){
    for(;;){
        pthread_mutex_lock(&mutex);
        while(g_count>0)
            g_count--;
        pthread_mutex_unlock(&mutex);
    }
    return (void *)0;
}

int main(){
    pthread_t tid;
    int ret;

    pthread_mutex_init(&mutex, NULL);

    ret = pthread_create(&tid, NULL, callback, NULL);
    if(ret){
        perror("creat error\n");
        exit(-1);
    }

    for (;;)
    {
        pthread_mutex_lock(&mutex);
        g_count++;
        pthread_mutex_unlock(&mutex);
    }
    exit(0);
}

在这里插入图片描述

4.1 条件变量初始化

初始化方法有两种:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

对于初始化与销毁操作,有以下问题需要注意:
⚫ 在使用条件变量之前必须对条件变量进行初始化操作,使用 PTHREAD_COND_INITIALIZER 宏或者函数 pthread_cond_init()都行;
⚫ 对已经初始化的条件变量再次进行初始化,将可能会导致未定义行为;
⚫ 对没有进行初始化的条件变量进行销毁,也将可能会导致未定义行为;
⚫ 对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的;
⚫ 经 pthread_cond_destroy()销毁的条件变量,可以再次调用 pthread_cond_init()对其进行重新初始化。

4.2 通知和等待条件变量

条件变量的主要操作便是发送信号(signal)和等待。发送信号操作即是通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检查条件是否满足。等待操作是指在收到一个通知前一直处于阻塞状态。

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

区别在于pthread_cond_broadcast可以唤醒所有等待的线程,而pthread_cond_signal只需确保至少唤醒一个线程即可

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
//cond :指向需要等待的条件变量
//mutex : pthread_mutex_t类型指针,指向一个互斥锁对象
/*
1.在阻塞线程时,如果线程已经对互斥锁mutex上锁,那么会将这把锁打开,这样做为了防止死锁
2.当线程接触阻塞的时候,函数会帮助这个线程再次将这个mutex互斥锁锁上,继续执行临界区
*/

使用条件变量测试:

#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static pthread_cond_t cond;
static pthread_mutex_t mutex;
static int g_count=0;

static void *callback(void *arg){
    for(;;){
        pthread_mutex_lock(&mutex);
        while(g_count<=0)
            pthread_cond_wait(&cond, &mutex);
        while(g_count>0)
            g_count--;
        pthread_mutex_unlock(&mutex);
    }
    return (void *)0;
}

int main(){
    pthread_t tid;
    int ret;
    
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond,NULL);
    ret = pthread_create(&tid, NULL, callback, NULL);
    if(ret){
        perror("creat error\n");
        exit(-1);
    }

    for (;;)
    {
        pthread_mutex_lock(&mutex);
        g_count++;
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond);
    }
    exit(0);
}

在这里插入图片描述

4.3 条件变量的判断条件

pthread_cond_wait()要配合while使用而不是if,当不满足条件时要立即重新判断条件,不满足就继续休眠等待,当有多个消费者线程时wait被唤醒,其中一个消费者线程会消费资源,可能会消费为空,那么其他的消费者又必须进入阻塞,所以不能用if

⚫ 当有多于一个线程在等待条件变量时,任何线程都有可能会率先醒来获取互斥锁,率先醒来获取到互斥锁的线程可能会对共享变量进行修改,进而改变判断条件的状态。如果有两个或更多个消费者线程,当其中一个消费者线程从 pthread_cond_wait()返回后,它会将全局共享变量 g_avail 的值变成 0,导致判断条件的状态由真变成假。

⚫ 可能会发出虚假的通知。

4.3 条件变量的属性

条件变量包括两个属性:进程共享属性和时钟属性。

5. 读写锁

在这里插入图片描述

一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁

读写锁有如下两个规则:

⚫ 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。

⚫ 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。

意思就是,两个线程要同时访问两个临界区,两个临界区分别被读锁和写锁,写优先级高,那么访问写锁的线程执行,而读锁阻塞;

5.1 读写初始化

pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER;
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

5.2 读写锁上锁和解锁

#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);

如果线程不希望被阻塞,可以调用 pthread_rwlock_tryrdlock()和 pthread_rwlock_trywrlock()来尝试加锁,

#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static pthread_rwlock_t rwlock;
static int g_count=0;
static int num[5]={0,1,2,3,4};
static void *read_callback(void *arg){
    int nums= *(int *)arg;

    for(int j=0; j<10;j++){
        pthread_rwlock_rdlock(&rwlock);
        printf("r线程:%d, g_count=%d\n",nums+1,g_count);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
    return (void *)0;
}

static void *write_callback(void *arg){
    int nums= *(int *)arg;

    for(int j=0; j<10;j++){
        pthread_rwlock_wrlock(&rwlock);
        g_count += 20;
        printf("w线程:%d, g_count=%d\n",nums+1,g_count);
        pthread_rwlock_unlock(&rwlock);
       
    }
    return (void *)0;
}


int main(){
    pthread_t tid_r[5],tid_w[5];
    int ret;

    pthread_rwlock_init(&rwlock,NULL);

    for (int i=0;i<5;i++){
        pthread_create(&tid_r[i], NULL, read_callback, &num[i]);
    }

    for (int i=0;i<5;i++){
        pthread_create(&tid_w[i], NULL, write_callback, &num[i]);
    }
    for(int i =0;i<5;i++){
        pthread_join(tid_r[i], NULL);
        pthread_join(tid_w[i], NULL);
    }

    pthread_rwlock_destroy(&rwlock);

    return 0;
}

在这里插入图片描述

5.3 读写锁的属性

#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
/*
函数 pthread_rwlockattr_getpshared() 用于从pthread_rwlockattr_t 对象中获取共享属性
函数 pthread_rwlockattr_setpshared() 用于设置 pthread_rwlockattr_t对象中的共享属性
*/

//使用方式
pthread_rwlock_t rwlock; //定义读写锁
pthread_rwlockattr_t attr; //定义读写锁属性
/* 初始化读写锁属性对象 */
pthread_rwlockattr_init(&attr);
/* 将进程共享属性设置为 PTHREAD_PROCESS_PRIVATE */
pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE);
/* 初始化读写锁 */
pthread_rwlock_init(&rwlock, &attr);
......
/* 使用完之后 */
pthread_rwlock_destroy(&rwlock); //销毁读写锁
pthread_rwlockattr_destroy(&attr); //销毁读写锁属性对象

6. 信号量

6.1 信号量函数

信号量用在多线程多任务同步,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程在进行某些动作,信号量不一定是锁定某一个资源,而是流程上的概念。

信号量:与互斥锁和条件变量的区别在‘灯’的概念,灯亮意味着资源可用,灯灭则意味着不可用,信号量主要是阻塞线程,不保证线程安全,需要配合互斥锁使用。

#include <semaphore.h>
sem_t sem;
//初始化信号量
//pshared: 0 线程同步
//		   非0 进程同步
//value:   初始化当前信号量拥有的资源数,资源数为0,线程会被阻塞
int sem_init(sem_t *sem,int pshared,unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
/*
函数被调用set中的资源就会被消耗1,资源数-1
*/
int sem_trywait(sem_t *sem);
//sem中资源数+1
int sem_post(sem_t *sem);
//查看信号量sem中的整形数的当前值,这个值会被写入到sval指针对应的内存中
//sval是一个传出参数
int sem_getvalue(sem_t *sem,int *sval);
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>

static sem_t semc;
static sem_t semp; 
static pthread_mutex_t mutex;
static int g_count=0;
struct Node{
    int number;
    struct Node *next;
};
struct Node *head;
static void *consumer(void *arg){
    int nums = *(int *)arg;
    for(int j=0;j<5;j++){
        sem_wait(&semc);
        printf("ctid:%d,g_count=%d\n",nums+1,head->number);
        free(head);
        sem_post(&semp);
    }
    
    return (void *)0;
}

static void *producer(void *arg){
    int nums = *(int *)arg;
    
    for(int j=0;j<5;j++){
        sem_wait(&semp);
        g_count +=20;
        
        struct Node *node = (struct Node*)malloc(sizeof(struct Node));
        node->number = g_count;
        node->next = head;
        head = node;
        printf("ptid:%d,g_count=%d\n",nums+1,node->number);
        sem_post(&semc);
        sleep(1);
    }
    return (void *)0;
}
int main(){
    pthread_t tidc[5];
    pthread_t tidp[5];
    int ret;
    int num[5]={0,1,2,3,4};


    
    pthread_mutex_init(&mutex, NULL);
    sem_init(&semp, 0, 1);
    sem_init(&semc, 0, 0);
    for (int i=0;i<5;i++){
        pthread_create(&tidc[i], NULL, consumer, &num[i]);
    }
    for (int i=0;i<5;i++){
        pthread_create(&tidp[i], NULL, producer, &num[i]);
    }

    for (int i=0;i<5;i++)
    {
       pthread_join(tidc[i],NULL);
       pthread_join(tidp[i],NULL);
    }

    sem_destroy(&semc);
    sem_destroy(&semp);
    exit(0);
}

在这里插入图片描述

sem资源不为1时需要配合互斥锁使用,sem为2时,ptid可以同时有两个写线程进行

#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>

static sem_t semc;
static sem_t semp; 
static pthread_mutex_t mutex;
static int g_count=0;
struct Node{
    int number;
    struct Node *next;
};
struct Node *head;
static void *consumer(void *arg){
    int nums = *(int *)arg;
    for(int j=0;j<5;j++){
        sem_wait(&semc);
        pthread_mutex_lock(&mutex);
        struct Node *temp = head;
        printf("ctid:%d,g_count=%d\n",nums+1,temp->number);
        head = head->next;
        free(temp);
        pthread_mutex_unlock(&mutex);
        sem_post(&semp);
    }
    
    return (void *)0;
}

static void *producer(void *arg){
    int nums = *(int *)arg;
    
    for(int j=0;j<5;j++){
        
        sem_wait(&semp);
        pthread_mutex_lock(&mutex);
        g_count +=20;
        struct Node *node = (struct Node*)malloc(sizeof(struct Node));
        node->number = g_count;
        node->next = head;
        head = node;
        printf("ptid:%d,g_count=%d\n",nums+1,node->number);
        pthread_mutex_unlock(&mutex);
        sem_post(&semc);
        
        sleep(1);
    }
    return (void *)0;
}
int main(){
    pthread_t tidc[5];
    pthread_t tidp[5];
    int ret;
    int num[5]={0,1,2,3,4};


    
    pthread_mutex_init(&mutex, NULL);
    sem_init(&semp, 0, 2);
    sem_init(&semc, 0, 0);
    for (int i=0;i<5;i++){
        pthread_create(&tidc[i], NULL, consumer, &num[i]);
    }
    for (int i=0;i<5;i++){
        pthread_create(&tidp[i], NULL, producer, &num[i]);
    }

    for (int i=0;i<5;i++)
    {
       pthread_join(tidc[i],NULL);
       pthread_join(tidp[i],NULL);
    }
    pthread_mutex_destroy(&mutex);
    sem_destroy(&semc);
    sem_destroy(&semp);
    exit(0);
}

在这里插入图片描述

;