实验相关知识
1.进程与线程
进程(Process):
- 定义: 进程是操作系统中的一个独立执行单元。每个进程都有独立的内存空间、程序代码、数据和系统资源。
- 资源独立性: 进程之间相互独立,一个进程的崩溃不会直接影响其他进程。
切换代价: 进程切换的代价相对较高,因为切换时需要保存和恢复完整的上下文信息,包括内存、寄存器等。 - 通信: 进程间通信相对复杂,通常需要通过进程间通信机制(IPC,Inter-Process Communication)来实现,如消息队列、信号量、管道等。
- 创建: 进程的创建通常较为耗时,并且新的进程拥有自己的地址空间。
线程(Thread):
- 定义: 线程是进程中的一个执行单元,是进程的一部分。一个进程可以包含多个线程,它们共享进程的地址空间和资源。
- 资源共享: 线程之间共享相同的地址空间和文件描述符,它们之间的通信相对容易。
- 切换代价: 线程切换的代价相对较低,因为线程共享相同的地址空间,切换时只需要保存和恢复寄存器等少量上下文信息。
- 通信: 线程之间的通信相对容易,因为它们共享相同的内存空间。但也需要注意同步和互斥,以防止数据竞争等问题。
- 创建: 线程的创建通常较为轻量,速度较快。
区别:
- 资源独立性: 进程有独立的内存空间,而线程共享相同的内存空间。
- 切换代价: 进程切换代价高,线程切换代价相对较低。
- 通信: 进程间通信相对复杂,线程通信相对容易。
- 创建: 进程创建代价较高,线程创建较为轻量。
- 健壮性: 由于进程有独立的内存空间,一个进程的崩溃不会直接影响其他进程。在多线程中,一个线程的问题可能导致整个进程的崩溃。
2.线程同步
线程同步是指多个线程在访问共享资源时采取的一种协调机制,以确保对共享资源的访问是有序和安全的。在多线程环境中,如果没有适当的同步机制,可能会导致竞争条件(Race Condition)、死锁(Deadlock)、数据不一致等问题。以下是一些常见的线程同步机制:
互斥锁(Mutex): 互斥锁是最基本的同步机制之一。一次只允许一个线程持有互斥锁,其他线程必须等待锁的释放。这确保了对共享资源的独占式访问。
信号量(Semaphore): 信号量是一种更为通用的同步机制,它可以允许多个线程同时访问临界区。信号量维护一个计数器,表示可同时访问的线程数量。
条件变量(Condition Variable): 条件变量允许线程在某个条件发生或满足时等待,从而避免了忙等待。它通常与互斥锁一起使用,等待某个条件的线程会释放锁,然后进入阻塞状态。
读写锁(Read-Write Lock): 读写锁允许多个线程同时读取共享资源,但在写操作时需要互斥。这样可以提高读取性能,因为多个线程可以同时读取,但写操作仍然是互斥的。
原子操作: 原子操作是一种不可分割的操作,它可以保证在执行期间不会被其他线程中断。一些现代编程语言和库提供了原子操作,用于确保对共享数据的操作是原子的。
屏障(Barrier): 屏障用于确保所有线程都达到某个点之后才能继续执行。它常用于同步多个线程的执行顺序。
这些同步机制可以根据具体的应用场景和需求进行选择和组合。正确使用线程同步机制可以有效避免并发环境中的问题,确保多线程程序的正确性和稳定性。然而,不正确的同步可能导致难以调试和修复的问题,因此在设计和实现多线程程序时,需要仔细考虑同步机制的选择和使用。
3.多线程
多线程是一种多任务并发的工作方式,在linux中线程包括内核线程和用户线程,内核线程有内核管理,不需要我们做更多的工作,我们这里讲的是用户线程,线程统一由用户线程来切换。
多线程的优势包括:
并发执行: 多线程使得程序的不同部分可以同时执行,提高了程序的并发性。
资源共享: 线程之间共享相同的地址空间和资源,简化了数据共享和通信。
响应性: 多线程可以提高系统的响应性,因为其中一个线程的阻塞不会影响其他线程的执行。
任务分解: 可以将复杂任务分解成多个线程,提高程序的结构性和可维护性。
并行处理: 在多核处理器上,多线程可以实现真正的并行处理,充分利用硬件资源。
然而,多线程编程也带来了一些挑战,例如:
同步和互斥: 多线程共享资源可能导致竞争条件,需要使用同步和互斥机制来确保数据的一致性。
死锁: 不正确的同步可能导致死锁,使得线程无法继续执行。
调试困难: 多线程程序的调试相对复杂,因为存在多个执行流。
性能开销: 线程的创建和切换都有一定的性能开销。
4.线程相关函数
int pthread_create(pthread_t id,pthread_attr_t *attr, void *(*start_runtine)(void *), void *arg);//线程创建函数 获取线程ID(即上面创建的pthread_t id):pthread_t pthread_self(); 退出线程:void pthread_exit(void *retval); 挂起线程:int pthread_join(pthread_t id,void **return); 线程同步:在POSIX中提供线程同步的方式有两种,条件变量和互斥锁
互斥锁:
pthread_mutex_t *mutex;//互斥锁变量 int pthread_mutex_init(pthread_mutex_t *mutex, pthread_attr_t *attr);//初始化一个互斥锁 int pthread_mutex_lock(pthread_mutex_t *mutex);//锁定互斥锁,这样子当一个线程锁定的话,另一个线程就会处于等待状态 int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁互斥锁,如果解锁后,处于等待状态的线程就有机会访问临界区
条件变量:其实是对互斥锁的一种补充,因为线程可以在等待条件变量的时候同时解锁,这在生产者和消费者模式可以体现。
pthread_cond_t cond; int pthread_cond_init(pthread_cond_t *cond, const pthread_cond_addr *attr);//初始化一个条件变量,后面参数attr是条件变量的属性 int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);//释放互斥量mutex,等待条件变量cond int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex,const struct timespec *abstime);//释放互斥量mutex,等待条件变量cond,与pthread_cond_wait函数不一样的是,该函数可以是线程在abstime时间内不阻塞。 int pthread_cond_signal(pthread_cond_t *cond);//释放条件变量 int pthread_cond_broadcast(pthread_cond_t *cond);//释放所有由cond阻塞的线程,这里要小心使用
5.线程属性
这些属性在使用前,必须调用相关的初始化函数pthread_xxx_init(xxx *);
线程属性:pthread_attr_t
上面的相关属性,POSIX大部分都提供了相应的接口来操作。如设置调度测略: int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy); int pthread_attr_init(pthread_attr_t *attr);//初始化线程属性对象 int pthread_attr_destroy(pthread_attr_t *attr);//销毁线程属性对象
实验设备与软件环境
安装环境:分为软件环境和硬件环境
硬件环境:内存ddr3 4G及以上的x86架构主机一部
系统环境:windows 、linux或者mac os x
软件环境:运行vmware或者virtualbox
软件环境:Ubuntu操作系统
实验内容
题目要求
在linux环境下,利用多线程及同步的方法,编写一个程序模拟火车售票系统,共3个窗口,卖10张票,程序输出结果类似(程序输出不唯一,可以是其他类似的结果)
即有三点要求:
1.创建三个线程
2.使用互斥锁保证线程安全
3.车票为0的时候停止卖票
我的思路
通过使用pthread_create(pthread_t *tidp,const pthread_attr_t *attr,void *(start_rtn)(void),void *arg);函数创建线程,其中参数
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。
其中线程运行函数为自己编写的buyTicket()函数,作用是进行售票工作,车票为0的时候停止卖票。在函数内部使用pthread_mutex_lock(&mutex)对于互斥锁进行锁定。线程调用该函数让互斥锁上锁,如果该互斥锁已被另一个线程锁定和拥有,则调用该线程将阻塞,直到该互斥锁变为可用为止(这样可以防止多人买票的时候计票数错误)。
同时为了防止在售票函数中,当完成了售票,进行解锁,此时该线程所分配的cpu时间片还没有完,于是又继续循环上去加锁售票,以此往复导致只有一个线程售票,其他线程被卡在获取锁加锁的环节。(即防止只有一个窗口将票卖完)我在解锁后增加一个睡眠(usleep(1);)。
最后通过使用pthread_join()函数等待线程的结束。
#include<stdio.h> #include<unistd.h> #include<pthread.h> //互斥锁的头文件 int tickets = 10; //总票数 pthread_mutex_t mutex; //C语言多线程中互斥锁的初始化 void *buyTicket(void *arg) { const char* name = (char*)arg; while(tickets>0) { pthread_mutex_lock(&mutex); tickets--; printf("[%s]窗口卖出一张票,还剩%d张票\n",name,tickets); pthread_mutex_unlock(&mutex); //防止只有一个窗口将票卖完 usleep(1); } printf("%s quit!\n",name); pthread_exit((void*)0); // return NULL; } int main() { pthread_mutex_init(&mutex, NULL); pthread_t t1,t2,t3; //创建线程 pthread_create(&t1, NULL, buyTicket, "thread 1"); pthread_create(&t2, NULL, buyTicket, "thread 2"); pthread_create(&t3, NULL, buyTicket, "thread 3"); //等待线程执行结束 pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); //注销互斥锁 pthread_mutex_destroy(&mutex); return 0; }
我们可以看到这个程序成功的利用了多线程及同步的方法,实现了模拟火车售票系统,一共3个窗口,分别是thread1,thread2和thread3,一共卖10张票,车票为0的时候停止卖票。同时使用互斥锁保证了线程安全。
异常问题与解决方案
异常:
编写好关于线程的程序后不能正常编译。
解决方法:
在头文件的地方加入#include<pthread.h>(互斥锁的头文件)
异常:
编译的时候出现“undefined reference to ‘pthread_create’”
解决方法:
原因:pthread不是Linux下的默认的库,也就是在链接的时候,无法找到phread库中哥函数的入口地址,于是链接会失败。
解决:在gcc编译的时候,附加要加 -lpthread参数即可解决。