Linucx -- 线程(二)

本文详细介绍了线程同步的概念,通过代码示例展示了线程同步问题及解决方案。分析了由于资源共享、调度随机导致的数据混乱,并讲解了互斥量(mutex)机制在解决线程同步中的作用,包括动态和静态初始化互斥量。同时,讨论了条件变量在改善忙等法中的应用,以更高效地实现线程间的协作。最后,给出了使用条件变量改进顺序输出和筛质数算法的例子,展示了条件变量如何提高程序效率。
摘要由CSDN通过智能技术生成

线程同步的概念

在这里插入图片描述
在这里插入图片描述
代码举例1

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

void* thr1(void* arg)
{
    while(1)
    {
        printf("hello");//hello在缓冲区中,遇到\n才会输出出来
        sleep(rand()%3);//范围是0-2
        printf("world\n");
        sleep(rand()%3);
    }
}

void* thr2(void* arg)
{
    while(1)
    {
        printf("HELLO");
        sleep(rand()%3);//失去CPU
        printf("WORLD\n");
        sleep(rand()%3);
    }
}

int main()
{
    pthread_t tid[2];
    pthread_create(&tid[0],NULL,thr1,NULL);
    pthread_create(&tid[1],NULL,thr2,NULL);

    pthread_join(tid[0],NULL);
    pthread_join(tid[1],NULL);

    return 0;
}

程序运行结果如下:
在这里插入图片描述
造成这种数据混乱的原因是什么呢?
调度随机、线程间缺乏必要的同步机制

代码举例2

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

char buf[20];

void* thr1(void* arg)
{
    int i=0;
    for(;i<20;i++)
    {
        usleep(rand()%3);
        buf[i]='0';
    }
    return NULL;
}

void* thr2(void* arg)
{

    int i=0;
    for(;i<20;i++)
    {
        usleep(rand()%3);
        buf[i]='1';
    }
    return NULL;
}

int main()
{
    memset(buf,0x00,sizeof(buf));
    pthread_t tid[2];
    pthread_create(&tid[0],NULL,thr1,NULL);
    pthread_create(&tid[1],NULL,thr2,NULL);

    pthread_join(tid[0],NULL);
    pthread_join(tid[1],NULL);

    printf("buf is %s.\n",buf);

    return 0;
}

在这里插入图片描述
造成这种数据混乱的原因是什么呢?
资源共享、调度随机、线程间缺乏必要的同步机制
在这里插入图片描述
解决同步的问题:加锁(互斥量机制)。
在这里插入图片描述
因此,即使有了mutex,如果有线程不按规矩来访问数据,依然会造成数据混乱。

mutex 互斥量机制的相关函数

使用互斥量机制之前,需要进行初始化,有两种初始化方式:

1、动态初始化方式
在这里插入图片描述
2、静态初始化方式
在这里插入图片描述
动态初始化和静态初始化会放在不同的情况下使用。
比如你的互斥量就是凭空定义出来的变量时,使用静态初始化方式就够用了。
比如你的互斥量是位于某个结构体中的,你就必须使用动态初始化方式。
在这里插入图片描述
在这里插入图片描述
pthread_mutex_lock是死心眼,必须要加锁,当锁被别人占用时,就死等(阻塞等待)。
而pthread_mutex_trylock则是尝试加锁,如果加不上,就走。
在这里插入图片描述
在这里插入图片描述
程序结束且不再对互斥量操作了,要摧毁锁。

互斥量机制相关函数的使用

同一时间段只能有一个线程执行的那段代码称之为临界区

//全局变量--共享数据
char buf[20];

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

/*上锁的目的主要是为了操作共享数据*/
void* thr1(void* arg)
{
    //上锁
    pthread_mutex_lock(&mutex);
    /*进入临界区--不要一直占用临界区(比如延时)*/
    int i=0;
    for(;i<20;i++)
    {
        buf[i]='0';
    }
    //解锁
    pthread_mutex_unlock(&mutex);
    /*退出临界区*/
    
    return NULL;
}

通过下面这个例子来理解互斥锁锁住的是一段代码。

练习:有四个线程,这四个线程拼命的往终端上输出abcd.

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

#define THRNUM 4

void* thr_func(void *p)
{
    int c = 'a'+(int)p;
    while(1)
    {
        write(1,&c,1);
    }

    pthread_exit(NULL);
}

int main()
{
	pthread_t tid[THRNUM];
	
    int i,err;
    for(i=0;i<THRNUM;i++)
    {                           //传给线程执行函数的参数
        err = pthread_create(tid+i,NULL,thr_func,(void *)i);

        if(err)
        {
            fprintf(stderr,"pthread_create():%s\n",strerror(err));
            exit(1);
        }
    }

    alarm(3);

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

    exit(0);
}

程序执行结果如下:
在这里插入图片描述
我们从打印结果可以看出输出的序列中要么是单个字母序列,要么就是两个字母序列,就是没有三个字母序列或者四个字母序列的,那到底是什么原因呢?是因为,我们的虚拟机是双核的,只能模拟两个线程并发执行的情形,不能实现三个线程并发。

程序并没有像我们期待的那样输出abcd序列,那怎么解决这个问题呢?

定义互斥量,让打印 ‘a’ 字符的线程去解锁打印 ‘b’ 字符的线程,以此类推。
那这样就需要四个互斥量。

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

#define THRNUM 4

//定义四个互斥量
static pthread_mutex_t mut[THRNUM];

static int next(int n)
{
    if(n+1 == THRNUM)
    {
        return 0;
    }
    return n+1;
}

void* thr_func(void *p)
{
    int n =(int)p;//第n把锁
    int c = 'a'+ n;
    while(1)
    {
        //把自己给锁上
        pthread_mutex_lock(mut+n);
        write(1,&c,1);
        pthread_mutex_unlock(mut+next(n));
    }

    pthread_exit(NULL);
}

int main()
{
    pthread_t tid[THRNUM];

    int i,err;
    for(i=0;i<THRNUM;i++)
    {
        //创建四把锁
        pthread_mutex_init(mut+i,NULL);
        pthread_mutex_lock(mut+i);

        //创建了四个线程,i是传给线程执行函数的参数
        err = pthread_create(tid+i,NULL,thr_func,(void *)i);

        if(err)
        {
            fprintf(stderr,"pthread_create():%s\n",strerror(err));
            exit(1);
        }
    }
    //主线程执行到这里后,四个线程都被创建出来了,但都因为lock而被阻塞。

    //解开第一个锁
    pthread_mutex_unlock(mut+0);

    alarm(3);

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

    exit(0);
}

需要注意代码执行逻辑和顺序。尤其需要注意的是那个while循环体中执行流程。

程序中,当四个线程都被创建出来了,但都因为lock而被阻塞。当主线程执行了unlock后,第一个线程不再被阻塞,将自己再次给锁起来后,输出字符 a ,然后将下一个进程给解锁,需要注意,因为有while的存在,以此类推。

看下输出结果。在这里插入图片描述

线程池(任务池)

"池"可以想象成就是一个容器保存着各种我们需要的对象。

标准的进程池或线程池是将进程或线程所在的位置模拟成 “池” 的实现。
在这里插入图片描述
我们要实现的目标:

利用线程完成对30000000–30000200之间质数的查找。

我们这里要做的并不是一个标准的线程池,相当于是做一个任务池。
在这里插入图片描述
定义一个全局变量num,上游的main线程利用num将任务(这里是一个个数)进行发放。下游定义3个线程来抢任务,比如上游将30000000扔了下去,那么下游就开始抢。那就看谁抢的快了。抢的快的那个人将30000000拿走。上游再将30000001放进去,如果执行30000000那个完成的够快,则再一起抢,否则的话,剩下的两个线程再一起抢,从而最大限度的利用当前所有的资源。

我们再定义几个状态,
在这里插入图片描述
当num大于0时,表明是任务,而当num等于0时,则表明当前没有任务,任务为空。这个0由取走任务的那个线程来写(该线程取走这个(任务)值后,立刻将num写成0),当下游的两个线程一看num=0,就知道当前没有任务。也就不再抢了。当上游线程发现num=0时,就会把下一个任务给放进去。依此类推。

总有一个时刻,任务会发放完毕。当任务结束,规定num的值为-1,
由main线程将num的值写为-1。下游的线程一看num=-1,就知道任务发放完毕了,就准备退出,然后上游线程(主线程)等着给他们收尸。

注意下述程序中有可能出现的死锁

查询法典型特点:加锁查看,解锁等待

代码编写如下:

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

#define LEFT   30000000
#define RIGHT  30000200
#define THRNUM 4

int num=0;

//从名字上体现我做互斥量是为了保障对num的使用,是一种独占资源的形式在使用。
pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER;

void* thr_prime(void *p)
{
    int i,j,mark;

    while(1)
    {
        pthread_mutex_lock(&mut_num);
        while(num == 0)//说明没有新的任务
        {
             //解锁等待
             pthread_mutex_unlock(&mut_num);
             sched_yield();//作用是出让调度器给别的线程,可以理解为非常非常短的sleep,且不会引起调度颠簸
             //在出让调度器的这段时间,如果主线程将num的值写为不为0的数,说明下发了任务或任务结束,且由于num不为0,也就将不再执行while循环.
             pthread_mutex_lock(&mut_num);
        }

        //退出循环体,即将结束线程
        if(num == -1)
        {
             //需要注意
             //如果不写这句话就会造成死锁
             //一个锁对应两种情况的解锁
             pthread_mutex_unlock(&mut_num);
            //临界区内的任何一个跳转语句一定要记得解锁之后再跳转
             break;
        }
        
		i = num;//领取任务
        num=0;
        pthread_mutex_unlock(&mut_num);
        //拿到num值之后,不是0和-1时才去执行下面代码。
        mark = 1;
        for(j=2;j<i/2;j++)
        {
            if(i%j == 0)
            {
                mark = 0;
                break;
            }
        }
        if(mark)
        {
            printf("[%d]%d is a primer.\n",(int)p,i);
        }
    }

    pthread_exit(NULL);
}

int main()
{
    int i,err;

    pthread_t tid[THRNUM];

    for(i = 0;i < THRNUM;i++)
    {
        err = pthread_create(&tid[i],NULL,thr_prime,(void* )i);
        if(err)
        {
            fprintf(stderr,"pthread_create():%s\n",strerror(err));
            exit(1);
        }
    }
    
	for(i = LEFT;i <= RIGHT;i++)
    {
        //下发任务
        pthread_mutex_lock(&mut_num);
        //不能冒昧的直接将num的值给i,需要先进行判断
        //不能是if来判断,而是应该循环判断
        while(num!=0)
        {
            //需要让别人有机会将num的值变成0,所以需要先解锁
            pthread_mutex_unlock(&mut_num);
            //直接解锁就立刻去加锁,很有可能这把锁还是被你抢走,
            //所以需要有个停顿,让进程有机会将num的值给写0.
            //那么可以使用sleep(1);吗?需要注意如果使用sleep的话,
            //可能会引起当前进程调度的颠簸,因为使用sleep后,当前进程
            //会由running态切换到可中断的睡眠状态,当sleep时间结束后,
            //这个进程又会被切换到running态,从而引起进程调度颠簸。
            //使用sched_yield();可以使进程平稳过渡。
            sched_yield();//作用是出让调度器给别的线程,可以理解为非常非常短的sleep,且不会引起调度颠簸
            //在出让调度器的这段时间,如果有线程将num的值写为0,说明有线程领取任务,且由于num=0,将不再执行while循环.

            //再马上加锁
            pthread_mutex_lock(&mut_num);
        }
        num = i;
        //当前下发任务完成
        pthread_mutex_unlock(&mut_num);
    }

    //所有任务下发完毕,main线程需要将num的值写为-1
    //但这里依然不能掉以轻心,首先,因为是要修改num的值,所以依然要加锁解锁
    //其次,当我们发放完最后一个任务时,也就是执行了num=i之后,接着执
    //行pthread_mutex_unlock(&mut_num);然后执行i++,此时,i的值不满足循环条件,从而跳出循环体
    //继而执行下面抢锁命令,因为它们属于同一线程,在抢锁有优势,所以有很大希望抢到锁,但是,
    //i=30000200(最后一个任务)还没有执行,就被你修改为-1了,这是不对的,所以这里也要不停的去判断。
    pthread_mutex_lock(&mut_num);
    //当num==0,说明最后一个任务也执行完毕了。此时,main线程才可以将num=-1
    while(num != 0)
    {
    	pthread_mutex_unlock(&mut_num);
         sched_yield();
         pthread_mutex_lock(&mut_num);
    }

    num = -1;
    pthread_mutex_unlock(&mut_num);


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

    pthread_mutex_destroy(&mut_num);

    exit(0);
}

程序运行结果如下:
在这里插入图片描述
尽管使用 “池” 的方法来筛质数是非常可靠的一种方式,但是,从打印输出的结果来看,程序有些卡顿,且这段时间的CPU占用率非常的高,这是因为上下游都在等待,上游在不停的加锁查看,下游也在不停的加锁查看,都在等待num的值变化。也就是说大量资源被耗尽在不停的查询上,所以属于忙等法。

程序还有更好的方法是采用通知的方法,也就是当上游完成了,告诉下游,下游完成了,通知上游。采用通知的方法需要使用到一个机制 – 条件变量。

条件变量

条件变量中涉及到一个类型 pthread_cond_t

#include <pthread.h>

//与互斥量一样,条件变量也可以静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

//动态初始化
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
参数:cond是条件变量,cond_attr是属性,不需要设置时填NULL//条件变量的销毁
int pthread_cond_destroy(pthread_cond_t *cond);
参数:cond是条件变量

具体用法还涉及到
怎么样去发一个消息呢?

//把所有的等待都叫醒
int pthread_cond_broadcast(pthread_cond_t *cond);
参数:cond是条件变量

//叫醒任意一个等待
int pthread_cond_signal(pthread_cond_t *cond);
参数:cond是条件变量

需要注意:这里的signal和信号那个signal无关。

还有

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

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
参数:cond是条件变量,mutex是互斥量

pthread_cond_wait 是死心眼,一直等待,直到有人来打断我为止。

pthread_cond_timedwait 是规定的时间内等不到就直接结束。其中参数abstime是超时设置,表示在我等待的abstime时间内,还没有发生我等待的行为的话,就结束。

1、使用条件变量来修改令牌桶的代码(见令牌桶章节)

2、使用条件变量来修改筛质数的代码

前面我们使用互斥量实现的本质是查询法的筛质数,上游和下游都不停的抢num,看状态。

上游抢num,期待它是0,下游抢num,期待它不是0。

现在,我们把它变成通知法,应该是这样一个逻辑。
比如当前下游一直处于一个等待状态。上游把任务放到num之后,给下游正在wait的几个线程发一个通知,使用signal就可以,因为下游几个线程的目的是一样的。叫醒任何一个,叫他取任务都可以。

如果现在下游都在忙,我的signal没有打断任何一个wait,那应该怎么办?
答:最先完成任务的程序执行到wait这个位置,再做处理。

当下游的某个线程将num取走之后,会使用pthread_cond_broadcast函数给main线程和其他正在等待的线程都发一个通知。
main线程接到通知后,知道任务被取走了,要再发任务或结束任务。
其他正在等待的线程收到通知,知道任务被别人取走了,继续等待.

具体看代码如何实现。

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

#define LEFT   30000000
#define RIGHT  30000200
#define THRNUM 4

int num=0;

//从名字上体现我做互斥量是为了保障对num的使用,是一种独占资源的形式在使用。
pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_num = PTHREAD_COND_INITIALIZER;

void* thr_prime(void *p)
{
    int i,j,mark;

    while(1)
    {
        pthread_mutex_lock(&mut_num);
        while(num == 0)//说明没有新的任务
        {
             pthread_cond_wait(&cond_num, &mut_num);
        }

        //退出循环体,即将结束线程
        if(num == -1)
        {
             //需要注意
             //如果不写这句话就会造成死锁
             //一个锁对应两种情况的解锁
             pthread_mutex_unlock(&mut_num);
            //临界区内的任何一个跳转语句一定要记得解锁之后再跳转
             break;
        }

        i = num;//领取任务
        num=0;
        //给所有线程发通知,主线程收到通知,知道要再发任务或结束任务了,
        //其他正在等待的线程收到通知,知道任务被别人取走了,继续等待.
        //解锁之前发通知可以先让主线程准备好抢锁.让其他线程继续保持等待状态.
		//所以应该使用pthread_cond_broadcast
        pthread_cond_broadcast(&cond_num);
        pthread_mutex_unlock(&mut_num);
        //拿到num值之后,不是0和-1时才去执行下面代码。
        mark = 1;
        for(j=2;j<i/2;j++)
        {
            if(i%j == 0)
            {
                mark = 0;
                break;
            }
        }
        if(mark)
        {
            printf("[%d]%d is a primer.\n",(int)p,i);
        }
    }

    pthread_exit(NULL);
}

int main()
{
    int i,err;

    pthread_t tid[THRNUM];

    for(i = 0;i < THRNUM;i++)
    {
        err = pthread_create(&tid[i],NULL,thr_prime,(void* )i);
        if(err)
        {
            fprintf(stderr,"pthread_create():%s\n",strerror(err));
            exit(1);
        }
    }

    for(i = LEFT;i <= RIGHT;i++)
    {
        //下发任务
        pthread_mutex_lock(&mut_num);
        //不能冒昧的直接将num的值给i,需要先进行判断
        //不能是if来判断,而是应该循环判断
        while(num!=0)
        {
            pthread_cond_wait(&cond_num, &mut_num);
        }
        num = i;
        //给任意一个线程发就行
        pthread_cond_signal(&cond_num);

        //当前下发任务完成
        pthread_mutex_unlock(&mut_num);
    }

    pthread_mutex_lock(&mut_num);
    //当num==0,说明最后一个任务也执行完毕了。此时,main线程才可以将num=-1
    while(num != 0)
    {
        pthread_cond_wait(&cond_num, &mut_num);
    }

    num = -1;
    //给所有线程发,任务结束,让他们都准备退出吧.
    pthread_cond_broadcast(&cond_num);
    pthread_mutex_unlock(&mut_num);


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

    pthread_mutex_destroy(&mut_num);
    pthread_cond_destroy(&cond_num);

    exit(0);
}

3、使用条件变量来修改顺序输出abcd的代码

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

#define THRNUM 4

static int num = 0;
//定义互斥量
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
//定义条件变量
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
 
static int next(int n)
{
    if(n+1 == THRNUM)
    {
        return 0;
    }
    return n+1;
}

void* thr_func(void *p)
{
    int n =(int)p;
    int c = 'a'+ n;
    while(1)
    {
        pthread_mutex_lock(&mut);
        //当num的值不等于n时就等待
        while(num != n)
        {
            pthread_cond_wait(&cond, &mut);
        }

        write(1,&c,1);
        //打印完之后,我要让num的值变成num的下一个人的编号
        num = next(num);
        //然后通知下一个人,怎么通知呢?
        //实际上a在打印的时候,bcd都在等待,直接用broadcast叫醒所有
        pthread_cond_broadcast(&cond);
        pthread_mutex_unlock(&mut);
	 }
    pthread_exit(NULL);
}

int main()
{
    pthread_t tid[THRNUM];

    int i,err;
    for(i=0;i<THRNUM;i++)
    {
        //创建了四个线程,i是传给线程执行函数的参数
        err = pthread_create(tid+i,NULL,thr_func,(void *)i);

        if(err)
        {
            fprintf(stderr,"pthread_create():%s\n",strerror(err));
            exit(1);
        }
    }
    //主线程执行到这里后,四个线程都被创建出来了.

    alarm(3);

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

    pthread_mutex_destroy(&mut);
    pthread_cond_destroy(&cond);

    exit(0);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xuechanba

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值