Linux多线程编程之线程同步

Linux多线程(用户态线程)编程之线程同步相关知识点

相关名词解析

同步:
同步就是指在一定时间内只允许一个线程访问某一资源。在该时间内其他线程不可访问该资源(实现方式:mutex互斥锁、condition variable条件变量、spin lock自旋锁(read/write lock读写锁是特殊自旋锁)、sem信号量等)

原子操作:
某段代码在执行时不会被其他线程影响,则构成了一个原子操作

临界区(非内核对象时,只在Win下多线程中存在,此处只提及不做过多说明):
临界区是一种轻量级机制,在某一时间内只允许一个线程执行某个给定代码段(在RTOS下表示禁止总中断和开启总中断之间的代码段)。临界区只能用于对同一进程内的线程进行同步

多线程同步实现方式(mutex、cond、sem、rw重要)

  • 互斥锁的实现
    主要实现函数:

1、pthread_mutex_t mutex_test = PTHREAD_MUTEX_INITIALIZER;//初始化静态定义的锁,一般用此即可
pthread_mutex_init(&mutex,NULL);//互斥锁被存放在参数mutex指向的内存中,第二个参数一般默认为NULL,使用缺省属性

2、pthread_mutex_lock(&mutex_test);//上锁

3、pthread_mutex_unlock(&mutex_test);//解锁

4、pthread_mutex_destroy (&mutex_test); //销毁互斥锁

/*************************************************************************
    > File Name: mutexlock.c
    > Author:lizhong 
    > Mail: 
    > Created Time: 2017年11月12日 星期日 06时11分54秒
 ************************************************************************/

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

pthread_mutex_t mutex_test = PTHREAD_MUTEX_INITIALIZER;

pid_t gettid(void)  
{  
  return syscall(__NR_gettid);  
} 

void *Thread_Test(void *main_i)
{
    int *tem_i = main_i;

    while(1)
    {
        //创建互斥锁
        pthread_mutex_lock(&mutex_test);

        if(*tem_i < 10)
            (*tem_i)++;    
        else
            break;
        printf("New ThreadID is:%lu, Main_i value is:%d\r\n", (unsigned long int)gettid(),*tem_i);
        //解锁,从此可以看出,锁实现了一段代码的原子操作,而非只针对某变量的原子操作 
        pthread_mutex_unlock(&mutex_test);
        sleep(1);
    }
    printf("New ThreadID is:%lu, Main_i value is:%d\r\n", (unsigned long int)gettid(),*tem_i);
    pthread_mutex_unlock(&mutex_test);
}

int main(void)
{
    int i = 0;
    int j = 0;

    pthread_t threadinfo[3];

    for(j = 0; j < 3; ++j)
    {
        pthread_create(&threadinfo[j], NULL, Thread_Test, (void *)&i);
    }

    for(j = 0; j < 3; ++j)
    {
        pthread_join(threadinfo[j], NULL);
    }

    printf("All End\r\n");
}  
  • 条件变量的实现(注意:条件变量总是和一个互斥锁搭配使用,当一个线程的行为依赖于另外一个线程对共享数据状态的改变时,这时候就可以使用条件变量)
    与互斥锁不同,条件变量是主动阻塞自身线程,直到某条件达成。但条件的检测需要在互斥锁的保护下进行。
    主要实现函数:

1、pthread_cond_t temcond = PTHREAD_COND_INITIALIZER;//初始化静态定义的条件变量,一般用此即可
int pthread_cond_init(pthread_cond_t cv,const pthread_condattr_t cattr);//条件变量被存放在参数cv指向的内存中,cattr一般默认为NULL,使用缺省属性

2、int pthread_cond_signal(pthread_cond_t *cv);//通过条件变量cv发送消息,若多个线程等待,按入队顺序激活其中一个。调用后要立即释放本线程的互斥锁,因为在满足条件后pthread_cond_wait会在另一个线程立马上锁继续执行。pthread_cond_broadcast可以唤醒所有等待线程

3、int pthread_cond_wait(pthread_cond_t cv,
pthread_mutex_t
mutex);//函数执行时先自动释放指定的锁,然后等待条件变量的变化。在函数调用返回之前,自动将指定的互斥量重新锁住。解锁->阻塞等待条件成立->上锁,继续执行。所以需要注意在另一线程条件变量成立signal后需要立即解锁

4、int pthread_cond_destroy(pthread_cond_t *cv);//销毁条件变量

/*************************************************************************
    > File Name: ConditionVariable.c
    > Author: lizhong
    > Mail: 
    > Created Time: 2017年11月16日 星期四 05时26分09秒
 ************************************************************************/

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

pthread_mutex_t mutex_test = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_test = PTHREAD_COND_INITIALIZER;

void *One_Thread(void * tparam)
{
    printf("One_Thread Start!\r\n");
    pthread_mutex_lock(&mutex_test);

    //解锁并等待条件变量信号,信号来后立即上锁继续执行该线程
    pthread_cond_wait(&cond_test, &mutex_test);
    printf("One_Thread Receive Signal\r\n");
    pthread_mutex_unlock(&mutex_test);
}

void *Two_Thread(void * tparam)
{
    unsigned char j = 0;

    printf("Two_Thread Start!\r\n");

    for(j = 0; j < 10; ++j)
    {
        if(j == 5)
        {
            printf("Two _Thread Ready Send Signal\r\n");

            //发送信号的线程可以不使用互斥锁,根据需求来设计
            pthread_cond_signal(&cond_test);
            printf("Two_Thread Send Signal OK\r\n");
        }
        printf("Two_Thread j Value is:%d\r\n", j);       
    }

}

int main(void)
{
    pthread_t one_thread;
    pthread_t two_thread;

    //可能会出现2号线程一直跑,
    //1号线程却还没开始跑的情况,
    //导致1号线程没能收到signal
    pthread_create(&one_thread, NULL, One_Thread, (void *)0);
    sleep(1);//确保1号线程先跑,也可使用SCHED_FIFO或SCHED_RR实时调度算法初始化线程
    pthread_create(&two_thread, NULL, Two_Thread, (void *)0);

    pthread_join(one_thread, NULL);
    pthread_join(two_thread, NULL);
    printf("ALL END\r\n");
}
  • 信号量的实现(使用信号量,需要包含semaphore.h头文件):
    POSIX信号量(linux中还有一种过时的System V信号量)分为有名信号量(值存在文件中)和无名信号量(值保持在内存中),都可以用于线程的同步(有名信号量还可用于进程间同步,无名信号量还可用于有亲缘关系的进程间同步)
    无名信号量必须是共享变量,并且无名信号量要保护的变量也必须是共享变量。无名信号量主要实现函数(函数本身均属于原子操作,可以多线程同时执行):

1、int sem_init(sem_t *sem, int pshared, unsigned int value);//初始化一个信号量,value参数指定信号量的初始值,pshared参数指明信号量是由进程内线程共享(0值),还是进程间共享(非0值),value指定了信号量的初始值

2、int sem_wait(sem_t *sem);//若sem值非零则立刻执行减1操作,线程继续执行。若sem值为0则线程睡眠,等待直到其他线程操作sem使其非零时才会唤醒该线程,减1后继续执行(同条件变量一样,收到信号后随机继续跑一个线程,有可能是发信号的线程也有可能是收信号的线程)。若有两个线程都在等待同一个信号量变成非零值,则第三个线程增加1时,等待线程中只有一个随机线程能对信号量做减法并继续执行,另一个仍处于等待状态

3、int sem_post(sem_t *sem);//将信号量值加1

4、int sem_destroy(sem_t *sem);//该函数用于清理信号量,在清理信号量时若还有线程在等待该信号量则会报错

/*************************************************************************
    > File Name: Sem.c
    > Author: 
    > Mail: 
    > Created Time: 2017年11月19日 星期日 00时44分58秒
 ************************************************************************/

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

sem_t sem_test;

void *One_Thread(void *tparam)
{
    printf("One_Thread is Start!\r\n");
    sem_wait(&sem_test);
    printf("One_Thread received sem\r\n");
    printf("One_Thread End!\r\n");
}

void *Two_Thread(void *tparam)
{
    unsigned char i = 0;
    printf("Two_Thread is Start!\r\n");

    for(i = 0; i <= 5; ++i)
    {
        sleep(1);
        printf("Two_Thread i value is:%d\r\n", i);
    }
    sem_post(&sem_test);
    printf("Two_Thread End!\r\n");
}

int main(void)
{
    pthread_t one_thread;
    pthread_t two_thread;

    sem_init(&sem_test, 0, 0);

    pthread_create(&one_thread, NULL, One_Thread, (void *)0);
    pthread_create(&two_thread, NULL, Two_Thread, (void *)0);

    pthread_join(one_thread, NULL);
    pthread_join(two_thread, NULL);

    printf("All End\r\n");
}
  • 读写锁的实现(适合一个线程写,多个线程读):
    若一个线程用了读锁锁定临界区,则其他线程依然可以使用读锁来进入临界区(不加锁也可进入临界区)。若在此时一个线程想对临界区加写锁,则需要等待所有先前的读锁都解锁后才会锁上写锁(之后来的读上锁操作不会被写锁阻塞,依然可以上锁)(导致写锁饥饿的原因),也就是说,默认属性总是读锁优先的,若想改成写优先,则需要将attr属性设置为PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP(即写锁优先,看看源码便知道为什么了),这样一来,后来的写锁将不会被阻塞,之后的读锁,需等待写锁解锁后,才能拿到写锁(看看源码就明白了)。
    若一个线程进行写锁加锁,则此后访问这个临界区的读锁或写锁都将进入阻塞。
    主要实现函数(1-5重要):

1、pthread_rwlock_t rwlock_test = PTHREAD_RWLOCK_INITIALIZER;//静态初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t rwptr, const pthread_rwlockattr_t attr);//初始化读写锁,attr为空指针,表示使用缺省属性

2、int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);//读锁上锁

3、int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);//写锁上锁

4、int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);//解锁读写锁

5、int pthread_rwlock_destroy(pthread_rwlock_t *rwptr);//销毁读写锁

6、int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);//尝试读锁上锁(非阻塞),成功返回0,失败返回错误码

7、int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);//尝试写锁上锁(非阻塞),成功返回0,失败返回错误码

/*************************************************************************
    > File Name: RWlock.c
    > Author:lizhong 
    > Mail: 
    > Created Time: 2017年11月19日 星期日 21时35分38秒
 ************************************************************************/

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

#define MAXDATASIZE 1024

typedef struct
{
    unsigned char flag_hasdata;//防止读者线程先运行时读数据
    char data[MAXDATASIZE]; 
}Data_t;

pthread_rwlock_t rwlock_test = PTHREAD_RWLOCK_INITIALIZER;

//读者线程
void *One_Thread(void *tparam)
{
     Data_t *pdata = tparam;
    printf("One_Thread Start!\r\n");

    while(1)
    {
        pthread_rwlock_rdlock(&rwlock_test);

        if(pdata->flag_hasdata == 1)
        {
            printf("One_Thread has rev data:%s", pdata->data);//stdin中的数据自带换行
            break;   
        }
        pthread_rwlock_unlock(&rwlock_test);
    }
    printf("One_Thread End!\r\n");
}

//写者线程
void *Two_Thread(void *tparam)
{
    Data_t *pdata = tparam;
    printf("Two_Thread Start!,Please input data!\r\n");

    while(1)
    {
        pthread_rwlock_wrlock(&rwlock_test);
        pdata->flag_hasdata = 0;
        fgets(pdata->data, MAXDATASIZE, stdin);
        printf("Two_Thread has rev Data!\r\n");
        pdata->flag_hasdata = 1;
        pthread_rwlock_unlock(&rwlock_test);
        break;
    }
    printf("Two_Thread End!\r\n");
}

int main(void)
{ 
    pthread_t one_thread;
    pthread_t two_thread;
    Data_t tdata = {0};

    pthread_create(&one_thread, NULL, One_Thread, (void *)&tdata);
    sleep(1);
    pthread_create(&two_thread, NULL, Two_Thread, (void *)&tdata);

    pthread_join(one_thread, NULL);
    pthread_join(two_thread, NULL);
    printf("ALL END\r\n");
}
  • 自旋锁的实现(在用户态暂时当做mutex看待,在用户态好像没有意义):
    自旋锁与互斥锁相似,唯一的区别是当一个线程试图获取一个被锁定的互斥锁时,该操作会失败然后该线程会进入睡眠,让其他线程运行。而当一个线程试图获取一个自旋锁却没成功时,该线程会不断重试,直到最终成功为止,所以在内核态的自旋锁中单核CPU就算关中断也会导致死锁(用户态的单核多线程自旋锁,及其容易产生死锁,因为自旋会占用该CPU大量的时间片,导致主业务无法正常处理,从而难以解锁)。
    主要实现函数(1-5重要):

1、pthread_spin_init(spinlock_t *lock, 0);//初始化自旋锁,将自旋锁设置为1(未上锁)

2、pthread_spin_lock(spinlock_t *lock);//循环等待,直到自旋锁解锁(被设置为1),然后再将自旋锁锁上(设置为0)

3、pthread_spin_unlock(spinlock_t *lock);//将自旋锁解锁(设置为1)

4、pthread_spin_lock_irq(spinlock_t *lock);//循环等待直到自旋锁解锁(被设置为1),然后将自旋锁锁上(设置为0),关中断

5、pthread_spin_unlock_irq(spinlock_t *lock);//将自旋锁解锁(设置为1),开中断

6、pthread_spin_is_locked(spinlock_t *lock);//如果自旋锁未上锁(值为1)则返回0,否则返回1

7、pthread_spin_unlock_wait();//等待,直到自旋锁解锁(被设置为1)

8、pthread_spin_trylock(spinlock_t *lock);//尝试锁上自旋锁(设置为0) ,如果原来为未上锁状态,则返回1,否则返回0

9、pthread_spin_lock_irqsave(spinlock_t *lock);//循环等待直到自旋锁解锁(被设置为1),随后将自旋锁上锁(设置为0)。关中断,将状态寄存器值存入flags

10、pthread_spin_unlock_irqsave(spinlock_t *lock);//解锁自旋锁(设置为1)。开中断,将状态寄存器值从flags存入状态寄存器

11、pthread_spin_lock_bh(spinlock_t *lock);//循环等待直到自旋锁解锁(被置为1),随后将自旋锁上锁(置为0)。阻止软中断底半部的执行

12、pthread_spin_unlock_bh(spinlock_t *lock);//将自旋锁解锁(设置为1)。开启底半部的执行

*************************************************************************
    > File Name: Spinlock.c
    > Author:lizhong
    > Mail: 
    > Created Time: 2017年11月18日 星期六 02时44分07秒
 ************************************************************************/

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

pthread_spinlock_t spinlock_test; 

void *One_Thread(void *tparam)
{
    unsigned char i = 0;
    printf("One_Thread Start!\r\n");
    pthread_spin_lock(&spinlock_test);

    for(i = 0; i <= 5; ++i)
    {
        sleep(1);
        printf("One_Thread i value is:%d\r\n", i);
    }
    pthread_spin_unlock(&spinlock_test);
}

void *Two_Thread(void *tparam)
{
    unsigned char i = 0;
    printf("Two_Thread Start!\r\n");
    //pthread_spin_lock(&spinlock_test);

    for(i = 0; i <= 5; ++i)
    {
        sleep(1);
        //两线程依然同时输出,说明依然存在内核抢占,线程调度
        printf("Two_Thread i value is:%d\r\n", i);
    }

    //pthread_spin_unlock(&spinlock_test); 
}

int main(void)
{ 
    pthread_t one_thread;
    pthread_t two_thread;

    pthread_spin_init(&spinlock_test, 0);

    pthread_create(&one_thread, NULL, One_Thread, (void *)0);
    pthread_create(&two_thread, NULL, Two_Thread, (void *)0);

    pthread_join(one_thread, NULL);
    pthread_join(two_thread, NULL);
    printf("ALL END\r\n");
}  

补充说明:
在linux中大部分用户态API都是使用glibc库实现的,在我这台Centos7(内核版本为3.10.0)上对应的glibc版本是2.17(可通过rpm -qa | grep glibc命令进行查看),通过以下路径可以拿到源码
http://git.savannah.gnu.org/cgit/hurd/glibc.git/refs/tags
现在,对API的使用或实现,哪里有疑问的直接戳源码查看就好了,这样效率最高了