2021-2022-1-diocs-并发编程第八周学习笔记

20191205 2021-2022-1-diocs-并发编程(第八周学习笔记)

一、任务详情

自学教材第4章,提交学习笔记(10分)

知识点归纳以及自己最有收获的内容 (3分)

问题与解决思路(2分)

实践内容与截图,代码链接(3分)

...(知识的结构化,知识的完整性等,提交markdown文档,使用openeuler系统等)(2分)

 

二、教材内容归纳整理

本章论述了并发编程,介绍了并行计算的概念,指出了并行计算的重要性;比较了顺序算法与并行算法,以及并行性与并发性;解释了线程的原理及其相对于进程的优势;通过示例介绍了 Pthread 中的线程操作,句括线程管理函数。互斥量、连接、条件变量和屏魔等线

程同步工具;通过具体示例演示了如何使用线程进行并发编程,包括矩阵计算、快速排序和用并发线程求解线性方程组等方法;解释了死锁问题,并说明了如何防止并发程序中的死锁问题;讨论了信号量,并论证了它们相对于条件变量的优点;还解释了支持Linux 中线程的独特方式。编程项目是为了实现用户级线程。它提供了一个基础系统来帮助读者开始工作。这个基础系统支持并发任务的动态创建、执行和终止,相当干在某个进程的同一地址空间中执行线程。读者可通过该项目实现线程同步的线程连接、互斥量和信号量,并演示它们在并发程序中的用法。该编程项目会让读者更加深入地了解多任务处理、线程同步和并发编程的原理及方法。

思维导图

 1.并行计算导论

(1)顺序算法与并行算法

2)并行性与并发性

通常,并行算法只识别可并行执行的任务,但是它没有规定如何将任务映射到处理组件。在理想情况下,并行算法中的所有任务都应该同时实时执行。然而,真正的并行执行只能在有多个处理组件的系统中实现,比如多处理器或多核系统。在单 CPU 系统中,一次只能执行一个任务。在这种情况下,不同的任务只能并发执行、即在逻辑上并行执行。在单CPU系统中,并发性是通过多任务处理来实现的,该内容已在第3章中讨论过。在本章的最后,我们将在一个编程项目中再次讲解和示范多任务处理的原理和方法。

2.线程

(1)线程的原理

线程是某进程同一地址空间上的独立执行单元。创建某个进程就是在一个唯一地址空间创建一个主线程。当某进程开始时,就会执行该进程的主线程。如果只有一个主线程,那么进程和线程实际上并没有区别。但是,主线程可能会创建其他线程。每个线程又可以创建更多的线程等。

(2)线程的优点

  1. 线程创建和切换速度更快;
  2. 线程的相应速度更快;
  3. 线程更适合并行计算;

(3)线程的缺点

1)由于地址空间共享,线程需要来自用户的明确同步。

2)许多库函数可能对线程不安全,例如传统 strtok()函数将一个字符串分成一连串令牌。通常,任何使用全局变量或依赖于静态内存内容的函数,线程都不安全。为了使库函数适应线程环境,还需要做大量的工作。

3)在单 CPU系统上,使用线程解决问题实际上要比使用顺序程序慢,这是由在运行时创建线程和切换上下文的系统开销造成的。

3.线程管理函数

1)创建线程

使用pthread_create()函数创建线程。

int prhread_create (pthread_t *pthread_id,pthread_attr_t *attr,

                Void *(*func)(void *), void *arg);

如果成功则返回0,如果失败则返回错误代码。

其中,attr参数最复杂。下面给出了 attr参数的使用步骤。

1.定义一个pthread属性变量 pthread_attr_t attr

2.pthread_attr_init&attr)初始化属性变量。

3.设置属性变量并在 pthread_create()调用中使用。

4.必要时,通过 pthread_attr_destroy&attr)释放 attr资源。

2)线程ID

线程 ID是一种不透明的数据类型,取决于实现情况。因此,不应该直接比较线程 ID。如果需要,可以使用 pthread_equal()函数对它们进行比较。

int pthread_equal (pthread_t t1, pthread_t t2);

如果是不同的线程,则返回0,否则返回非0

3)线程终止

线程函数结束后,线程即终止。或者,线程可以调用函数

int pthread_exit (void *status);

进行显示终止,其中状态是线程的退出状态。通常,0退出值表示正常终止,非0只表示异常终止。

4)线程连接

一个线程可以等待另一个线程的终止,通过:

int pthread_join (pthread_t thread,void **status_ptr);

终止线程退出状态以status_ptr返回。

4.线程同步

(1)互斥量

1.一种是静态方法:

pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER

定义互斥量m,并使用默认属性对其进行初始化。

2.一种是动态方法:使用pthread_mutex_init()函数,可通过attr参数设置互斥属性。

pthread_mutex_init(pthread_mutex_t *m,pthread_mutexattr_t,*attr);

(2)死锁预防

有多种方法可以解决可能的死锁问题,其中包括死锁预防、死锁规避、死锁检测和恢复等。在实际系统中,唯一可行的方法是死锁预防,试图在设计并行算法时防止死锁的发生。一种简单的死锁预防方法是对互斥量进行排序,并确保每个线程只在一个方向请求互斥量,这样请求序列中就不会有循环。

但是,仅使用单向加锁请求来设计每个并行算法是不可能的。在这种情况下,可以使用条件加锁函数 pthread mutex trylock(来预防死锁。如果互斥量已被加锁,则 trylock(函数会立即返回一个错误。在这种情况下,调用线程可能会释放它已经获取的一些互斥量以便进行退避,从而让其他线程继续执行。在上面的交叉加锁示例中,我们可以重新设计其中一个线程,例如 T1,利用条件加锁和退避来预防死锁。

(3)条件变量

1.静态方法:

pthread_cond_t con = PTHREAD_COND_INITIALIZER;

定义一个条件变量con,并使用默认属性对其进行初始化。

2.动态方法:

使用pthread cond init()函数,可通过 attr参数设置条件变量。为简便起见,我们总是使用NULL attr参数作为默认属性。

在互斥量的临界区中,线程可通过以下函数使用条件变量来相互协作。

pthread_cond_waitconditlonmutex该函数会阻塞调用线程,直到发出指定条件的信号。当互斥量被加锁时、应调用该例程。它会在线程等待时自动释放互斥量。互斥量将在接收到信号并唤醒阻塞的线程后自动锁定。

pthread cond signalcondition;该函数用来发出信号,即唤醒正在等待条件变量的线程或解除阻塞。它应在互斥量被加锁后调用,而且必须解锁互斥量才能完成pthread_cond_wait ()

pthread cond broadcastcondition该函数会解除被阻塞在条件变量上的所有线程阻塞。所有未阻塞的线程将争用同一个互斥量来访问条件变量。它们的执行顺序取决于线程调度。

(4)信号量

信号量和条件变量之间的主要区别是,前者包含一个计数器,可操作计数器,测试计数器值以做出决策等,所有这些都是临界区的原子操作或基本操作,而后者需要一个特定的互斥量来执行临界区。在 Pthreads 中,互斥量严格用于封锁。而条件变量可用于线程协作。相反,可以把使用初始值1计算信号量当作锁。带有其他初始值的信号量可用于协作。因此,信号量比条件变量更通用、更灵活。下面的示例说明了信号量相对于条件变量的优势。

下面显示了使用信号量的生产者-消费者问题的伪代码。

 (5)屏障

线程连接操作允许某线程(通常是主线程)等待其他线程终止。在等待的所有线程都终止后,主线程可创建新线程来继续执行并行程序的其余部分。创建新线程需要系统开销。在某些情况下,保持线程活动会更好,但应要求它们在所有线程都达到指定同步点之前不能继续活动。

(6)Linux中的线程

与许多其他操作系统不同,Linux不区分进程和线程。对于Linux 内核,线程只是一个与其他进程共享某些资源的进程。在Linux 中,进程和线程都是由clone()系统调用创建的,具有以下原型

int clone(int(*fn)(void *),void *child_stack,int flags,void *arg)

 

三、最有收获的内容

生产者—消费者问题

我们将使用线程和条件变量来实现一个简化版的生产者-消费者问题,也称有限缓冲问题。生产者-消费者问题通常将进程定义为执行实体,可看作当前上下文中的线程。下面是该问题的定义。

一系列生产者和消费者进程共享数量有限的缓冲区。每个缓冲区每次有一个特定的项目。最开始,所有缓冲区都是空的。当一个生产者将一个项目放人一个空缓冲区时,该缓冲区就会变满。当一个消费者从一个满的缓冲区中获取一个项目时,该缓冲区就会变空。如果没有空缓冲区,生产者必须等待。同样,如果没有满缓冲区,则消费者必须等待。此外,当等待事件发生时、必须允许等待进程继续。

在示例程序中,假设每个缓冲区都有一个整数值。共享全局变量定义为;

// shared global variables int buf[NBUF];

int head, tail;// number of full buffers

int data;

缓冲区用作一系列循环缓冲区。索引变量 head用于将一个项目放人空缓冲区,tail 则用于从满缓冲区中取出一个项目。变量数据就是满缓冲区的数量。为支持生产者和消费者之间的协作,我们定义了一个互斥量和两个条件变量。

pthread_mutex_t mutex;

pthread_cond_t empty,full;// condition variables

其中,empty 表示所有空缓冲区的条件,full 表示所有满缓冲区的条件。当生产者发现没有空缓冲区时,它会等待empty 条件变量,当消费者使用了一个满缓冲区时,就会发出信号。同样,当消费者发现没有满缓冲区时,它会等待 ful1条件变量。 当生产者将一个项放人空缓冲区时,就会发出信号。

该程序从主线程开始,将缓冲区控制变量和条件变量初始化。初始化完成后,它会创建一个生产者线程和一个消费者线程,并等待线程结合。缓冲区大小设置为NBUF=5.如果生产者试图将 N=10个项放入缓冲区,当所有缓冲区已满时,它就要等待。同样.如果消费者试图从缓冲区获取N=10个项,将会导致它在所有缓冲区都空时等待。在这两种情况下,当等待条件得到满足时,另一个线程会通知正在等待的线程。因此,这两个线程通过条件变量相互协作。下面是示例程序 C4.4 的程序代码,实现了一个简版的生产者-消费者问题,即只有一个生产者和一个消费者。

 

问题与解决思路

问题:Linux里进程与线程的区别?

解决思路:

线程是指进程内的一个执行单元,也是进程内的可调度实体.

与进程的区别:

(1)地址空间:进程内的一个执行单元;进程至少有一个线程;它们共享进程的地址空间;而进程有自己独立的地址空间;

(2)资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源;

(3)线程是处理器调度的基本单位,但进程不是;

(4)二者均可并发执行:

进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。进程是系统进行资源分配和调度的一个独立单位。线程是进程的一个实体,是CPU调度和分派的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

 

五、实践内容(截图、代码链接)本次实践是基于OpenEuler系统下实现的

编写一个C语言程序使用信号量实现生产者—消费者问题:

1.目的

1实现生产者消费者问题的模拟,以便更好的理解此经典进程同步问题。生产者-消费者问题是典型的PV操作问题,假设系统中有一个比较大的缓冲池,生产者的任务是只要缓冲池未满就可以将生产出的产品放入其中,而消费者的任务是只要缓冲池未空就可以从缓冲池中拿走产品。缓冲池被占用时,任何进程都不能访问。

2每一个生产者都要把自己生产的产品放入缓冲池,每个消费者从缓冲池中取走产品消费。在这种情况下,生产者消费者进程同步,因为只有通过互通消息才知道是否能存入产品或者取走产品。他们之间也存在互斥,即生产者消费者必须互斥访问缓冲池,即不能有两个以上的进程同时进行。

2.原理

在同一个进程地址空间内执行两个线程。生产者线程生产物品,然后将物品放置在一个空缓冲区中供消费者线程消费。消费者线程从缓冲区中获得物品,然后释放缓冲区。当生产者线程生产物品时,如果没有空缓冲区可用,那么生产者线程必须等待消费者线程释放一个空缓冲区。当消费者线程消费物品时,如果没有满的缓冲区,那么消费者线程将被阻挡,直到新的物品被生产出来。

3.流程图(左面是生产者流程图,右面是消费者流程图)

C语言代码链接:

https://gitee.com/two_thousand_and_thirteen/codes/y172whr9os5ma4fzpbix066

编译运行结果截图:

 

 

posted @ 2021-10-28 21:43  张灯结彩,潇潇暮雨  阅读(64)  评论(0编辑  收藏  举报