Linux--线程同步与互斥

1 线程同步与互斥的引入

线程的互斥:对于某种资源,如果有一个线程正在访问该资源,则其它的进程必须等待,当那个进程访问完成后其它进程才能访问。 也就是一个资源一次仅允许一个线程访问。
线程的同步:某些进程的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。

1.由于同一个进程之间的线程很多东西是共享的,就会使得多个线程看到同一份资源,不同线程都可以访问该共同资源。如果多个线程并发的访问某个共享资源就会出现数据不一致问题。
2.这个共享资源就称为临界资源,访问临界资源的代码称为临界区。
3.因此,当有多个线程同时访问临界资源的时候,必须要互斥的访问;有时候有些线程之间执行某些任务需要按照一定的顺序执行,因此线程之间还需要同步。
4.Linux下通过互斥量实现线程间的互斥,通过条件变量或信号量实现线程间的同步。
5.示例:多个线程访问同一个资源,出现数据错误的情形:

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>

//定义一个全局变量,使得所有线程都可以得到
int count = 100;

void* entry(void* arg)
{
    int threadindex = (int)arg;
    while(1)
    {   
        if(count > 0)
        {
            usleep(1000);
            printf("thread%d count %d\n",threadindex,count);    //threadindex表示线程编号,由创建线程的参数传进来的
            --count;
        }
        else                                                                                                                            
        {
            break;
        }
    }   
}


int main()
{
    //1.创建4个线程
    pthread_t tid[4];
    int i = 0;
    for( ; i<sizeof(tid)/sizeof(tid[0]); ++i)
    {
           pthread_create(&tid[i],NULL,entry,(void*)i);
    }

    i = 0;
    for(;i<sizeof(tid)/sizeof(tid[0]);++i)
    {
        pthread_join(tid[i],NULL);
    }
    return 0;
}     

运行结果:(可以发现虽然线程处理函数里面加了判断当count>0才去进行自减,但是最后还会输出-1,-2,这就是由于–count不是原子操作,再加上包含usleep这个模拟漫长业务的过程就会导致线程随时都有可能被切换出去,就会使得当一个正要去减count,但是被切换了,使得结果不对)。

thread2 count 100
thread3 count 99
...  (中间的输出结果正确,由于太长所以省略)
thread2 count 1
thread1 count 0
thread3 count -1
thread0 count -2

可以发现:在线程处理函数中如果不加上usleep(1000),结果也会运行正确,因为有可能只有一个线程被调度,没有被切换出去,别的线程执行时发现为0,没有操作,就可能不会出错,但是这种情况还是很危险。

2 互斥量

1.由于多个线程并发执行某个代码,会出现问题,所以就必须让一个线程进入临界区时,其它线程不能再进入该临界区。并且一次只允许有一个线程进入临界区。不是临界区多个线程可以并发执行,所以就需要一把锁,Linux上提供的这把锁就叫互斥量。
2.互斥量本质上是一把锁,在访问共享资源前对互斥量进行加锁,当共享资源访问完毕之后对互斥量进行解锁。对互斥量进行加锁之后,任何试图访问该共享资源的线程都会被阻塞直到当前线程释放这个锁。如果释放这把锁之前有多个线程被阻塞,那么所有阻塞在该互斥量的线程都会变为可运行状态,但是只有第一个变为运行状态的线程可以申请到这把锁,那么其它线程就会看到这把锁依然是锁着的,只能回去再次等待。
3.互斥量是用pthread_mutex_t类型的数据表示的,当使用互斥量时,需要先对互斥量进行初始化(通过pthread_mutex_init函数),在程序结束前需要对互斥量进行销毁(通过pthread_mutex_destroy函数)。
4.互斥量的相关接口:
(1)互斥量的初始化:
①函数原型:

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

②参数解析:

mutex:是定义的一个pthread_mutex_t类型的变量,定义这个变量其实就是定义了一把锁
attr:表示相关属性,默认为NULL
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;中mutex就是锁,该式也是对互斥量进行初始化,定义全局锁一般用它(它是一个宏)。

注意:一般在创建线程之前创建锁,这样线程就可以看到这把锁。
(2)信号量的销毁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

//mutex表示需要销毁的信号量

注意:已经加锁的信号量不能被销毁。而已经销毁的信号量不能再对其加锁。
(3)互斥量的加锁
对互斥量加锁调用pthread_mutex_lock函数,如果互斥量已经被加锁,那么其它访问该互斥量的线程就会被阻塞,如果不希望被阻塞可以调用pthread_mutex_trylock函数。

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);

(4)互斥量的解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

注意:当一个线程已经将互斥量锁定了,那么其它的线程没有竞争到互斥量,就会阻塞等待。
5.互斥量相关接口的使用:(将上面的程序里对临界资源的访问加锁)

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>

//定义一个全局变量,使得所有线程都可以得到
int count = 100;                                                                                                                        
pthread_mutex_t lock;  //定义一把全局锁
void* entry(void* arg)
{
    int threadindex = (int)arg;
    while(1)
    {   
        pthread_mutex_lock(&lock);
        if(count > 0)
        {
            usleep(1000);
            printf("thread%d count %d\n",threadindex,count);
            --count;

            pthread_mutex_unlock(&lock);      //注意:必须在if和else语句里面都要解锁,因为如果在外面解锁,就会使得已经执行break语句,再有线程过来就会一直阻塞
        }
        else
        {
            pthread_mutex_unlock(&lock);                                                                                                
            break;
        }
    }
}
int main()
{
    //1.先初始化锁
    pthread_mutex_init(&lock,NULL);
    //2.创建4个线程
    pthread_t tid[4];
    int i = 0;
    for( ; i<sizeof(tid)/sizeof(tid[0]); ++i)
    {
        pthread_create(&tid[i],NULL,entry,(void*)i);
    }

    i = 0;
    for(;i<sizeof(tid)/sizeof(tid[0]);++i)
    {
        pthread_join(tid[i],NULL);
    }
    //3.销毁锁
    pthread_mutex_destroy(&lock);
    return 0;
}                   

运行结果:(可以看到运行结果正确)

...
thread2 count 3
thread2 count 2
thread2 count 1

3 条件变量

1.举一个例子:小A到银行取钱,但是取款机里没有钱了,小A就会去取款机外面等,中间间隔很短又进入取款机里面,小A就这样一直进入取款机再出来,但是在短时间内银行工作人员发现取款机门锁着,因此无法进去放钱。就会使得小A很久都取不到钱。
这就类似于一个线程频繁的申请锁释放锁,使得效率变得很低。只要其它的线程能够在条件满足的时候通知该线程,就会提高效率,因此引入了条件变量。
2.条件变量描述了临界资源状态的相关信息,表示当满足某个条件时才执行,条件不满足就需要一直在该条件变量下等待。
3.互斥量主要实现了线程间的互斥,条件变量主要为了线程间的同步。(如果没有条件变量就需要一直检测条件变量是否满足,如果再加锁条件下,如果条件不满足一个线程就需要一直申请锁释放锁,就会使得其它线程无法执行,更加满足不了条件,因此需要条件变量)。
4.条件变量是进行线程同步的一种机制,条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
5.条件变量本身是由互斥量保护的,线程在改变条件状态时必须要先锁住互斥量,互斥量在锁定条件以后才能计算条件。
6.条件变量相关接口:
(1)条件变量的初始化:

#include <pthread.h

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

//cond:表示自己定义的条件变量,该条件变量是pthread_cond_t类型
//attr表示该条件变量的相关属性

(2)销毁条件变量

 int pthread_cond_destroy(pthread_cond_t *cond);

(3)等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

①等待条件满足的时候,参数不仅需要条件变量还需要加上互斥量,因为一方面条件不会没有任何缘由就会满足,那么就需要通过其它线程来改变条件,改变条件就可能会对共享资源进行改变,因此需要互斥量来保护临界资源,同时也需要通过互斥量来保护条件变量。
另一方面当我们等待条件满足的时候,需要先释放申请到的锁,然后等待条件满足,条件满足之后再申请锁,由于线程随时可能被切换出去,如果在线程解锁之后等待条件之前,当前线程已经被切换出去了,如果其它线程获得互斥量摒弃条件满足,立即发送信号,那么等待的那个线程就会错过这个信号,就会一直被阻塞。
②调用者把锁住的互斥量传递给函数,函数就会将该线程放在等待列表里面并会对互斥量进行解锁,当该函数返回之后,当前互斥量再次被锁住。
该函数会进行:

(1)默认释放锁(如果不释放锁,就会导致当前线程等待的时候,什么事情都没干,导致其它线程无法拿到锁,也会等待,甚至会造成死锁)
(2)等待条件满足
(3)申请锁

(4)唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

①broadcast表示唤醒多个线程
②signal表示唤醒单个线程
signal比broadcast好,因为同时唤醒多个,就会使得多个线程同时去竞争锁。

注意:
当醒来 的时候还需要检测条件是否满足,因为有个能是被异常唤醒。
当某个线程被唤醒后,会再次申请锁,并从等待处继续执行;一般是谁快谁需要等待。

7.使用条件变量时,检测条件是否满足必须要采用while语句而不用if语句,当条件不满足时,在while语句里面一直等待(因为pthread_cond_wait会被信号打断,如果用if则信号处理之后就不会再次检测条件是否满足)
(1)等待条件代码

pthread_mutex_lock(&mutex);

while (条件为假)
{
     pthread_cond_wait(cond, mutex); 
}

修改条件 
pthread_mutex_unlock(&mutex);

(2)给条件发送信号代码:

pthread_mutex_lock(&mutex);
设置条件为真 
pthread_cond_signal(cond); 
pthread_mutex_unlock(&mutex);

3 信号量

1.在进程间通信中介绍了一种信号量为System V版本的信号量,这里介绍一下POSIX版本的信号量,信号量表示临界资源的数目。
2.POSIX版本的信号量与System V版本的信号量的区别
(1)POSIX版本的信号量与System V版本的信号量的作用相同,主要用于同步操作。
(2)POSIX版本的信号量既可用于进程间同步也可用于线程间同步;
(3)System V版本的信号量的操作都是针对信号量集进行的,POSIX版本的信号量的操作针对于某一个的信号量,这就使得POSIX版本的信号量的相关操作比System V版本的简单很多。
3.信号量的相关操作:
(1)初始化信号量:

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数解析:
sem表示自己定义的sem_t类型信号量的地址
pshared:为0表示用于线程之间,非0表示用于进程之间
value:表示信号量的初始值

(2)销毁信号量

#include <semaphore.h>
int sem_destroy(sem_t *sem);

(3)等待信号量

int sem_wait(sem_t *sem);

//等待信号量,表示需要申请相应的资源将信号量的值减一,类似于P操作

(4)发布信号量

int sem_post(sem_t *sem);

//发布信号量,表示申请到的资源已经使用完毕,可以归还相应的资源了,将信号量的值加一,相当于V操作

4 信号量与互斥量的区别

1.信号量用于线程或进程的同步操作,互斥量用于线程的互斥操作;
2.互斥量的值只能取0或1,而信号量的取值可以为任意整数,当信号量的只能取0或1时,此时的信号量相当于一个互斥量(互斥锁)。
3.互斥量的加锁或解锁必须由同一线程对应使用,但是信号量可以是一个线程(或进程)释放另一个线程得到。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值