Linux系统编程教学设计-Linux多线程-线程基本编程、线程同步互斥机制...

Linux系统编程教学设计-Linux多线程-线程基本编程、线程同步互斥机制...

2023年8月2日发(作者:)

Linux高级系统编程

教学设计

课程名称: Linux高级系统编程_______________

授课年级: ___________________________

授课学期: ___________________________

教师姓名: ___________________________

20xx年03月01日

课程名称

内容分析

教学目标

教学要求

教学重点

教学难点

教学方式

第4章 多线程

计划学时

4学时

本章主要介绍线程基本编程、线程同步互斥机制、线程池

要求学生了解进程与线程的关系、线程的概念、掌握多线程编程的操作方法、掌握线程的通信、同步互斥机制、掌握基本线程池编程方法

线程基本编程、线程同步互斥机制、线程池

线程同步互斥机制、线程池

课堂讲解及ppt演示

第一课时

(线程基本编程、线程同步互斥机制)

内容回顾

1. 回顾上节内容,引出本课时主题。

为了进一步减少处理器的空转时间,支持多处理器以及减少系统的开销,进程演化过程中出现了另一个概念--线程。它是进程内独立的运行线路,是内核调度的最小单元,也可以称为轻量级进程。多线程编程由于其高效性和可操作性,在应用开发中使用非常广泛,本章将从线程的基本概念开始,介绍围绕多线程的编程诸多知识点,从而帮助读者更加深入地了解系统编程的核心内容。从而引出本节的内容。

2. 明确学习目标

(1)能够掌握线程的基本概念

(2)能够掌握线程的创建

(3)能够掌握线程终止与回收

(4)能够掌握线程编程

(5)能够掌握线程的分离

(6)能够掌握线程的取消

(7)能够掌握线程通信

(8)能够掌握互斥锁的使用

(9)能够掌握互斥锁的死锁

知识讲解

 线程的基本概念

线程是应用程序并发执行多种任务的一种机制。在一个进程中可以创建多个线程执行多个任务。每个进程都可以创建多个线程,创建的多个线程共享进程的地址空间,即线程被创建在进程所使用的地址空间上。上一章介绍了创建子进程的原理,创建子进程是通过复制父进程的地址空间得来的,父子进程只关注自己的地址空间(映射不同的物理地址空间)。因此进程与进程之间是独立的,每个进程都只需要操作属于自己的地址空间即可。而线程则不一样,创建线程无须对地址空间进行复制,同一个进程创建的线程共享进程的地址空间,因此创建线程比创建子进程要快很多。

 线程的创建

函数pthread_create()函数用于在一个进程中创建一个线程。

#include

int pthread_create(pthread_t *thread, const pthread_attr_t

*attr,

void *(*start_routine) (void *), void

*arg);

函数参数thread表示新创建的线程的标识符,或者称为线程的ID。参数attr指向一个pthread_attr_t类型的结构体,用以指定新创建的线程的属性(如线程栈的位置和大小、线程调度策略和优先级以及线程的状态),如果attr被设置为NULL,则线程将采用默认的属性。参数start_routine则是该函 数的重点关注对象,通过函数原型可以看出,该参数为函数指针,因此该参数只需传入函数名即可。需要注意的是,传入的函数名并不等同于一般的程序中在主函数中调用子函数,它是线程的执行函数,通俗地说,线程执行的任务将封装在此函数中。参数arg作为仅有的参数,用于向第三个参数start_routine所指向的函数中传参。

pthread_create()函数的参数thread,其类型为pthread_t本质上是一个经强制转化的无符号长整型的指针。一个线程可以通过pthread_self()来获取自己的ID。

#include

pthread_t pthread_self(void);

 线程终止与回收

线程退出的方式有很多,以下几种情况都会导致线程的退出。

(1)线程的执行函数执行return语句并返回指定值。

(2)线程调用pthread_exit()函数。

(3)调用pthread_cancel()函数取消线程。

(4)任意线程调用exit()函数,或者main()函数中执行了return语句,都会造成进程中的所有线程立即终止。

pthread_exit()函数将终止调用线程,且参数可被其他线程调用pthread_join()函数来获取。参数retval指定了线程的返回值。如果一个线程调用了pthread_exit()函数,但其他线程仍然继续执行。

#include

void pthread_exit(void *retval);

pthread_join()函数用于等待指定thread标识的线程终止。如果线程终止,则pthread_join()函数会立即返回。参数retval如果为非空指针,那么此时参数将会保存标识符为参数thread的线程退出时的返回值,即pthread_exit()函数中指定的参数。

#include

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

若线程并未进行分离,则必须使用pthread_join()函数来进行回收资源。如果未能进行,那么线程终止时将产生与僵尸进程类似的僵尸线程。如果僵尸线程积累过多,不仅浪费资源,而且可能无法继续创建新的线程。

 线程编程

通过阅读前两节的函数介绍,读者可以完成基本的线程编程,并通过线程完成一些任务。本节将通过代码展示创建新线程与封装子函数的区别,以便于更好地理解线程机制。具体如代码参考教材4.1.4节。

 线程的分离

1. pthread_detach设置线程分离

默认的情况下,线程是可连接的(也可称为结合态)。通俗地说,就是当线程退出时,其他线程可以通过调用pthread_join()函数获取其返回状态。但有时,在编程过程中,程序并不关心线程的返回状态,只是希望系统在线程终止时能够自动清理并移除。在这种情况下,可以调用pthread_detach()函数并向thread参数传入指定线程的标识符,将该线程标记为处于分离状态(分离态)。

#include

int pthread_detach(pthread_t thread);

一旦线程处于分离状态,就不能再使用pthread_join()函数来获取其状态,也无法使其重返“可连接”状态。具体案例代码参加教材4.1.5节。

2. pthread_attr_setdetachstate实现线程分离

同理,针对上述设置线程分离状态的方法,也可以在线程刚一创建时即进行分离(而非之后再调用pthread_detach()函数)。首先可以采用默认的方式对线程属性结构进行初始化,接着为创建分离线程而设置属性,最后再以此线程属性结构来创建新线程,线程一旦创建,就无须再保留该属性对象。最后将其摧毁。

初始化线程属性结构及摧毁函数如下。

#include

int pthread_attr_init(pthread_attr_t *attr);

int pthread_attr_destroy(pthread_attr_t *attr);

设置线程分离状态的函数为pthread_attr_setdetachstate()。

#include

int pthread_attr_setdetachstate(pthread_attr_t *attr, int

detachstate);

参数detachstate用来设置线程的状态,设置PTHREAD_CREATE_DETACHED(分离态)与PTHREAD_CREATE_JOINABLE(结合态)。具体案例代码参加教材4.1.5节。

 线程的取消

在通常情况下,程序中的多个线程会并发执行,每个线程处理各自的任务,直到其调用pthread_exit()函数或从线程启动函数中返回。但有时候也会用到线程的取消,即向一个线程发送一个请求,要求其立即退出。例如,一组线程正在执行一个任务,如果某个线程检测到错误发生,需要其他线程退出,此时就需要取消线程的功能。

1. 设置线程取消状态

pthread_cancel()函数向由thread指定的线程发送一个取消请求。发送取消请求后,pthread_cancel()函数立即返回,不会等待目标线程的退出。

#include

int pthread_cancel(pthread_t thread);

那么此时目标线程发生的结果及发生的时间取决于线程的取消状态和类型。

#include

int pthread_setcancelstate(int state, int *oldstate);

pthread_setcancelstate()函数会将调用线程的取消状态设置为参数state所给定的值。参数state可被设置为PTHREAD_CANCEL_DISABLE(线程不可取消),如果此类线程收到取消请求,则会将请求挂起,直至将线程的取消状态置为启用。也可被设置为PTHREAD_CANCEL_ENABLE(线程可以被取消)。一般,新创建的线程默认为可以取消。参数oldstate用以保存前一次状态。具体案例详情参考教材4.1.6节。

2. 设置线程取消类型

如果需要设置线程为可取消状态,则可以选择取消的类型。

#include

int pthread_setcanceltype(int type, int *oldtype);

pthread_setcanceltype()函数用以设置当前线程的可取消的类型,上一次的取消类型保存在参数oldtype中。参数type可以被设置为PTHREAD_CANCEL_DEFERRED,表示线程接收到取消操作后,直到运行到“可取消点”后取消。type也可以被设置为PTHREAD_CANCEL_ASYNCHRONOUS,表示接收到取消操作后,立即取消。具体案例详情参考教材4.1.6节。

 线程通信

4.1.4节已经介绍了线程的基本编程,然而并没有对线程进行实质性的信息传递,即多线程的通信。线程不同于进程的是多线程间共享进程的虚拟地址空间,这也为线程通信提供了便利,线程通信只需要操作共享的进程数据段即可。而进程使用的全局变量存在于进程数据段中,因此多线程编程通信时,一般选择操作全局变量实现通信。线程通信虽然很容易,但也有其弊端,正因为并发的线程访问了相同的资源,所以造成了数据的不确定性。因此,线程的通信需要结合一些同步互斥机制一起使用。

 互斥锁的使用

4.3.1节中所展示的代码中,线程在访问共享的全局变量时,没有按照一定的规则顺序进行访问造成了不可预计的后果。针对代码的运行结果分析,其原因就是因为线程在访问共享资源的过程中,被其他线程打断,其他线程也开始访问共享资源导致了数据的不确定性。对于上述情况,最好的解决办法是当一个线程在进行共享资源的访问时,其他线程不能访问,保证对于共享资源操作的完整性。

本节将介绍一种互斥机制,用以保护对共享资源的操作,即保护线程对共享资源的操作代码可以完整执行,而不会在访问的中途被其他线程介入对共享资源访问,造成错误。在这里,通常把对共享资源操作的代码段,称之为临界区,其共享资源也可以称为临界资源。于是这种机制--互斥锁的工作原理就是对临界区进行加锁,保证处于临界区的线程不被其他线程打断,确保其临界区运行完整,

互斥锁一种互斥机制。互斥锁作为一种资源,在使用之前需要先初始化一个互斥锁。每一个线程在访问共享资源时,都需要进行加锁操作,如果线程加锁成功,则可以访问共享资源,期间不会被打断,在访问结束之后解锁。如果线程在进行上锁时,其锁资源被其他线程持有,那么该线程则会执行阻塞等待,等待锁资源被解除之后,才可以进行加锁。对于多线程而言,在同等条件下,对互斥锁的持有是不确定的,先持有锁的线程先访问,其他线程只能阻塞等待。也就是说,互斥锁并不能保证线程的执行先后,但却可以保证对共享资源操作的完整性。如图所示。

互斥锁的使用包括初始化互斥锁、互斥锁上锁、互斥锁解锁、互斥锁释放。

#include

int pthread_mutex_destroy(pthread_mutex_t *mutex);

int pthread_mutex_init(pthread_mutex_t *restrict mutex,

const pthread_mutexattr_t *restrict attr);

ptherad_mutex_init()函数用来实现互斥锁的初始化,参数mutex用来指定互斥锁额标识符,类似于ID;参数attr为互斥锁的属性,一般设置为NULL,即默认属性。与之相反函数pthread_mutex_destroy(),函数为释放互斥锁,参数mutex用来指定互斥锁的标识符。只有当互斥锁处于未锁定状态,且后续也无任何线程企图锁定它时,将其摧毁才是安全的。

#include

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

初始化之后,互斥锁处于未锁定状态,pthread_mutex_lock()函数为上锁处理,如果该锁资源处于持有状态,那么函数将直接导致线程阻塞。直到其他线程使用pthread_mutex_unlock()函数进行解锁,参数mutex为互斥锁的标识符。需要注意的是,不可对处于未锁定状态的互斥量进程解锁,或者解锁由其他线程锁定的互斥锁。

 互斥锁的死锁

互斥锁在默认属性的情况下使用,一般需要关注死锁的情况。所谓死锁,即互斥锁无法解除同时也无法加持,导致程序可能会无限阻塞的情况。有时,一个线程可能会同时访问多个不同的共享资源,而每个共享资源都需要有不同互斥锁管理。那么在不经意间程序编写极容易造成死锁的情况。造成死锁的原因有很多,本节将通过一些举例展示死锁的情况。

(1)在互斥锁默认属性的情况下,在同一个线程中不允许对同一互斥锁连续进行加锁操作,因为之前锁处于未解除状态,如果再次对同一个互斥锁进行加锁,那么必然会导致程序无限阻塞等待。

(2)多个线程对多个互斥锁交叉使用,每一个线程都试图对其他线程所持有的互斥锁进行加锁。如图所示情况,线程分别持有了对方需要的锁资 源,并相互影响,可能会导致程序无限阻塞,就会造成死锁。

同时,还需要注意的是在一个线程中操作多个互斥锁时,加锁与解锁的顺序一定是相反的,否则也会导致错误。例如上述示例,如果线程先加锁1,后加锁2,之后一定要先解锁2,再解锁1。

(3)一个持有互斥锁的线程被其他线程取消,其他线程将无法获得该锁,则会造成死锁。具体案例详情参考教材4.2.3节。

第二课时

(线程同步互斥机制、线程池)

内容回顾

1. 回顾上节内容,引出本课时主题。

上节已经介绍了线程基本编程、线程同步互斥机制的部分内容,下面将介绍线程同步互斥机制和线程池接下来内容。

2. 明确学习目标

(1)能够掌握互斥锁的属性

(2)能够掌握信号量的使用

(3)能够掌握条件变量的使用

(4)能够掌握wait()函数和waitpid()函数

(5)能够掌握线程池的基本概念

(6)能够掌握线程池的实现

知识讲解

 互斥锁的属性

在4.2.3节中,初始化互斥锁pthread_mutex_init()函数中参数attr为指定互斥锁的属性,而一般默认传参为NULL,表示执行该互斥锁的默认属性。本小节将单独讨论互斥锁的属性。

#include

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

int pthread_mutexattr_init(pthread_mutexattr_t *attr);

pthread_mutexattr_init()函数为初始化互斥锁属性,一般采用默认的方式传参。pthread_mutexattr_destroy()函数摧毁互斥锁属性。

#include

int pthread_mutexattr_getpshared(const pthread_mutexattr_t

*

restrict attr, int *restrict pshared);

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,

int pshared);

pthread_mutexattr_getpshared()函数和pthread_mutexattr_setpshared()函数的功能分别为获得互斥锁的属性、设置互斥锁的属性。参数attr表示互斥锁的属性,参数pshared可以设置为两种情况:(1)PTHREAD_PROCESS_PRIVATE,表示互斥锁只能在一个进程内部的两个线程进行互斥(默认情况);(2)PTHREAD_PROCESS_SHARED,互斥锁可用于两个不同进程中的线程进行互斥,使用时需要在共享内存(后续介绍)中分配互斥锁,再为互斥锁指定该属性即可。

前面介绍了关于互斥锁的属性的基本函数,下面将会着重介绍互斥锁的属性---类型。pthread_mutexattr_gettype()函数和pthread_mutexattr_settype()函数用来获得及设置互斥锁的类型。

#include

int pthread_mutexattr_gettype(const pthread_mutexattr_t

*restrict attr,

int *restrict type);

int pthread_mutexattr_settype(pthread_mutexattr_t *attr,

int type);

参数type用来定义互斥锁的类型。其类型可以被设置为如下几种情况。

(1)PTHREAD_MUTEX_NORMAL:标准互斥锁,该类型的互斥锁不具备死锁检测功能。

(2)PTHREAD_MUTEX_ERRORCHECK:检错互斥锁,对此互斥锁的所有操作都会执行错误检查,这种互斥锁运行起来较一般类型慢,不过却可以作为调试,以发现后续程序在哪里违反了互斥锁的使用规则。

(3)PTHREAD_MUTEX_RECURSIVE:递归互斥锁,该互斥锁维护有一个锁计数器,线程上锁则会将锁计数器的值加1,解锁则会将锁计数器的值减1。只有当锁计数器值降至0时,才会释放该互斥锁。这一类互斥锁与普通互斥锁的区别在于,同一个线程可以多次获得同一个递归锁,不会产生死锁。而如果一个线程多次获得同一个普通锁,则会产生死锁。Linux下的互斥锁默认累性为非递归的。

 信号量的使用

前面章节介绍了解决多线程竞态的机制互斥锁的使用。本节将介绍另外一种多线程编程中广泛使用的一种机制--信号量。

信号量本身代表一种资源,其本质是一个非负的整数计数器,被用来控制对公共资源的访问。换句话说,信号量的核心内容是信号量的值。其工作原理是:所有对共享资源操作的线程,在访问共享资源之前,都需要先操作信号量的值。操作信号量的值又可以称为PV操作,P操作为申请信号量,V操作为释放信号量。当申请信号量成功时,信号量的值减1,而释放信号量成功时,信号量的值加1。但是当信号量的值为0时,申请信号量时将会阻塞,其值不能减为负数。利用这一特性,即可实现对共享资源访问的控制。

信号量作为一种同步互斥机制,若用于实现互斥时,多线程只需设置一个信号量。若用于实现同步时,则需要设置多个信号量,并通过设置不同的信号量的初始值来实现线程的执行顺序。

本节将介绍基于POSIX的无名信号量,其信号量的操作与互斥锁类似。

#include

int sem_init(sem_t *sem, int pshared, unsigned int value);

sem_init()函数被用来进行信号量的初始化。参数sem表示信号量的标识符。pshared参数用来设置信号量的使用环境,其值为0,表示信号量用于同一个进程的多个线程之间使用。其值为非0,表示信号量用于进程间使用。value为重要的参数,表示信号量的初始值。

#include

int sem_destroy(sem_t *sem);

sem_destroy()函数被用来摧毁信号量,参数sem表示信号量的标识符。

#include

int sem_wait(sem_t *sem);

int sem_trywait(sem_t *sem);

int sem_timedwait(sem_t *sem, const struct timespec

*abs_timeout);

sem_wait()函数用来执行申请信号量的操作,当申请信号量成功时,信号量的值减1,当信号量的值为0时,此操作将会阻塞,直到其他线程执行释放信号量。sem_trywait()函数与sem_wait()函数类似,唯一的区别在于sem_trywait()函数不会阻塞,当信号量为0时,函数直接返回错误码EAGAIN。sem_timewait()函数同样,多了参数abs_timeout,用来设置时间限制,如果在该时间内,信号量仍然不能申请,那么该函数不会一直阻塞,而是返回错误码ETIMEOUT。

#include

int sem_post(sem_t *sem);

sem_post()函数用来执行释放信号量的操作,当释放信号量成功时,信号量的值加1。

#include

int sem_getvalue(sem_t *sem, int *sval);

sem_getvalue()函数用于获得当前信号量的值,并保存在参数sval中。

 条件变量的使用

多线程引入同步互斥机制,就是为了在某一时刻只能有一个线程可以实现对共享资源的访问。不论互斥锁还是信号量其本质都是一致的。条件变量的工作原理很简单,即让当前不需要访问共享资源的线程进行阻塞等待(睡眠),如果某一时刻就共享资源的状态改变需要某一个线程处理,那么则可以通知该线程进行处理(唤醒)。

条件变量可以看成是互斥锁的补充,因为条件变量需要结合互斥锁一起使用,之所以这样,是因为互斥锁的状态只有锁定和非锁定两种状态,无法决定线程执行先后,有一定的局限。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。关于条件变量如何配合使用,本节将从示例中详细分析。

条件变量的使用同样需要初始化,其核心操作为阻塞线程以及唤醒线程,最后将其摧毁。

#include

int pthread_cond_destroy(pthread_cond_t *cond);

int pthread_cond_init(pthread_cond_t *restrict cond,

const pthread_condattr_t *restrict attr);

pthread_cond_init()函数的功能是初始化条件变量,参数cond表示条件变量的标识符,参数attr用来设置条件变量的属性,通常为NULL,执行默认属性。如果执行成功则会将条件变量的标识符保存在参数cond中。pthread_cond_destroy()函数表示摧毁一个条件变量。

#include

int pthread_cond_broadcast(pthread_cond_t *cond);

int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_signal()函数的功能是发送信号给至少一个处于阻塞等待的线程,使其脱离阻塞状态,继续执行。如果没有线程处于阻塞等待状态,pthread_cond_signal()函数也会成功返回。pthread_cond_broadcast()函数的功能是唤醒当前条件变量所指定的所有阻塞等待的线程。上述两个函数中,pthread_cond_signal()函数使用的频率会更大,按照互斥锁对共享资源保护规则,条件变量cond也作为一种共享资源,则pthread_cond_signal()函数即可以放在pthread_mutex_lock()函数和pthread_mutex_unlock()函数之间,也可以采用另一种写法,将pthread_cond_signal()函数放在pthread_mutex_lock()函数和pthread_mutex_unlock()函数之后,当然有时也可以不添加加锁解锁操作。这些都要视环境而定,后续将通过代码示例分析。

#include

int pthread_cond_timedwait(pthread_cond_t *restrict cond,

pthread_mutex_t *restrict mutex,

const struct timespec *restrict abstime);

int pthread_cond_wait(pthread_cond_t *restrict cond,

pthread_mutex_t *restrict mutex);

pthread_cond_wait()函数用于使线程进入睡眠状态,当使用pthread_cond_wait()函数使线程进入阻塞状态时,必须先对其进行加锁操作,之后再进行解锁操作。通俗地说,即pthread_cond_wait()函数必须放在pthread_mutex_lock()函数和pthread_mutex_unlock()函数之间。参数cond为条件变量的标识符,参数mutex则为互斥锁的标识符。值得注意的是,也是函数的重点是pthread_cond_wait()函数一旦实现阻塞,使线程进入睡眠之后,函数自身会将之前线程已经持有的互斥锁自动释放。不同于唤醒操作,睡眠操作必须要进行加锁处理。

 线程池的基本概念

在4.2节中,介绍了多线程编程的通信问题,多线程编程实现通信需要考虑数据的正确性。此外,多线程编程还需要考虑开销、性能的问题。故此,引出了线程的一种使用模式,叫做线程池。顾名思义,把一堆开辟好的 线程放在一个池子里统一管理,就是一个线程池。

之所以出现了线程池的概念,部分原因是考虑线程的频繁创建和销毁会消耗大量时间和资源。设想在一个传统的服务器中,有这样一种监听线程用来监听是否有新的用户连接服务器,每当有一个新的用户加入,服务器就开启一个新的线程处理这个用户的数据包。这个线程只服务于这个用户,当用户与服务器断开连接以后,服务器就销毁这个线程。然而频繁地开辟与销毁线程极大地占用了系统的资源。在大量用户的情况下,这种情况方式将浪费 大量的时间。而线程池与传统的一个用户对应一个线程的处理方法不同。它的基本实现思想就是在程序开始时就在内存中开辟一些线程,线程的数量是固定的,它们独自形成一个类,屏蔽了对外的操作,服务器只需要将数据包交给线程池就可以。当有一个新的用户请求到达时,不是新创建一个线程为其服务,而是从“池”中选择一个空闲的线程为新的用户请求服务,服务完成后,线程进入空闲线程池中。如果没有线程空闲的话,就将数据包暂时累积,等待线程池中有空闲的线程以后再进行处理。

一个线程池主要包括以下几个组成部分:

(1)线程管理器:创建并管理线程池。

(2)工作线程:线程池中实际执行任务的线程。在初始化线程池时,将预先创建好固定数目的线程在池中,这些初始化的线程一般处于空闲状态。

(3)任务接口:每个任务必须实现的接口,当线程池的任务队列中有可执行任务时,被空闲的工作线程调去执行,把任务抽象成接口,可以做到线程池与具体的任务无关。

(4)任务队列:用来存放没有处理的任务,提供一种缓冲机制,实现这种结构的方法有很多,常使用队列,主要运用先进先出的原理。

那么,在何种场合下会创建线程池。当一个应用需要频繁的创建和销毁线程,而任务执行的时间又非常短,这样线程的创建和销毁带来的开销就不容忽视。如果线程创建和销毁的时间相比任务执行时间可以忽略不计,则没有必要使用线程池。

 线程池的实现

下面将通过代码在Linux系统中实现线程池。如图所示为线程池的创建思路。

(1)用户程序向任务队列中添加任务。

(2)创建线程池,线程睡眠,处于空闲状态。

(3)唤醒线程,线程池中的线程执行函数取出任务队列中的任务。

(4)执行任务中的调用函数,完成工作。

(5)线程池任务执行完判断,如果没有程序调用,线程继续睡眠。

(6)调用销毁函数对线程池进行销毁。

第三课时

上机练习(总结、练习题)

1. 总结本章内容。

2. 通过题库发送相关测试题,检查学生掌握情况。

上机练习主要针对本章中需要重点掌握的知识点,以及在程序中容易出错的内容进行练习,通过上机练习可以考察同学对知识点的掌握情况,对代码的熟练程度。

第四课时

上机练习(总结、练习题)

1. 总结本章内容

2. 通过题库发送相关测试题,检查学生掌握情况。

上机练习主要针对本章中需要重点掌握的知识点,以及在程序中容易出错的内容进行练习,通过上机练习可以考察同学对知识点的掌握情况,对代码的熟练程度。

习题 教材第4章习题

发布者:admin,转转请注明出处:http://www.yc00.com/web/1690957762a472841.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信