操作系统(四)——多线程编程

一、线程概述

1、什么是线程

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它与同属于一个进程的其他线程共享进程所拥有的全部资源

2、进程和线程

Linux进程创建一个新线程时,线程将拥有自己的栈(因为线程有自己的局部变量),但它与的创建者共享全局变量文件描述符信号句柄和当前目录状态。也因此一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮。

Linux通过fork创建子进程与创建线程之间是有区别的:fork创建出该进程的一份拷贝,这个新进程拥有自己的变量和自己的PID,它的时间调度是独立的,它的执行几乎完全独立于父进程。

进程可以看成是一个资源的基本单位,而线程是程序调度的基本单位,一个进程内部的线程之间贡献进程获得的时间。

3、为什么要进程

和进程相比,它是一种非常“节俭”的多任务操作方式。在linux系统下, 启动一个新的进程必须分配给它独立的地址空间,建立众多的数据来维护它的代码段、堆栈端和数据段。

运行一个进程中的多个线程,它们之间使用相同的地址空间,而且线程之间彼此切换所需时间小于进程切换所需的时间。一个进程的开销大约是一个线程的30倍左右。

线程间方便的通信机制,对不同进程来说,它们有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,但同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其他线程所用。

使CPU系统更加有效。操作系统会保证当线程数目不大于CPU数目时,不同的线程运行与不同的CPU上。

改善程序结构,一个既长又复杂的进程可以考虑分为多个进程,成为几个独立的或半独立的运行部分,这样的程序有利于理解、修改。

4、如何使用

Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h。连接时需要使用libpthread.a。如: gcc main.c -lpthread -o main

 

二、多线程程序设计

1、线程创建

#include <pthread.h>

int pthread_create(pthread_t * thread,    pthread_attr_t * attr,      void *(*start_routine)(void *),      void *arg);

thread:指向pthread_t类型的指针,用于引用新创建的线程

attr:  用于设置线程的属性,一般为NULL

void *(*start_routine)(void *) :  线程函数

arg :  线程函数参数

2、线程ID获取

include <pthread.h>

pthread_t pthread_self(void);   //返回当前线程的ID

int pthread_equal(pthread_t t1, pthread_t t2);   //比较两个线程ID是否相同,如果相等返回0

3、线程终止

线程自己调用pthread_exit

#include<pthread.h>

void pthread_exit(void * retval);    //线程通过调用pthread_eixt函数终止执行,并返回一个指向某对象的指针,注意:不要返回                                                                           局部变量的指针

4、线程等待

#include <pthread.h>

int pthread_join(pthread_t tid, void **thread_return);  //阻塞调用线程,直到指定的线程终止。tid:等待退出的线程ID return:                                                                                       线程退出返回值得指针 

5、线程分离

线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态(即可结合的,joinable,需要回收),这种情况下,原有的线程等待创建的线程结束;只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。

#include <pthread.h>

int pthread_detach(pthread_t thread);        //成功返回0,失败返回错误值。

对于创建时处于未分离状态的线程,必须调用一次pthread_join()或pthread_detach(),否则线程结束后就会留下没有释放的资源。    

等待线程结束的pthread_join()操作可以由任何一个同组的线程发起,不必是主线程。另外,如果主线程退出,即进程退出,则所有的线程也会随之退出。 

 

三、线程同步

当多个线程共享相同的内存时,需要确保每个线程看到一致的数据。如果每个线程使用的变量都是其他线程不会读取或修改的,那么就不存在一致性问题。同样地,如果变量是只读的,多个线程读取该变量也不会有一致性问题。但是,当某个线程可以修改变量,而其他线程也可以读取或者修改这个变量的时候,就需要对这些线程进行同步,以确保它们在访问变量的存储内容时不会访问到无效的数值。

1、信号量

信号量通常有两种:二进制信号量和计数信号量。二进制信号量只有0和1两种取值,计数信号量有更大的取值范围。

信号量一般用来保护一段代码,使其每次只能被一个执行线程运行,要完成这个工作,可以使用二进制信号量。

有时,希望可以允许有限数目的线程执行一段指定的代码,这时可以使用计数信号量。

(1)初始化

#include <semaphore.h>

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

sem   指向需要初始化的信号量

value指定信号量的初值

pshared表明是在一个进程的多个线程之间共享还是在多个进程之间共享。若pshared为0,信号量被一个进程的多个线程共享,此时应该将信号量(sem_t)置于所有线程可见的位置(全局变量或动态分配)

执行成功返回0,出错返回-1,并设置errno
(2)信号量控制

#include <semaphore.h>

int sem_post(sem_t *sem);   // v

int sem_wait(sem_t *sem);   // p

sem_post的作用是以原子操作的方式给信号量的值加1,sem_wait函数以原子操作的方式将信号量的值减1,但它会等待直到信号量有个非零值才会开始减法操作。例如,对值为2的信号量调用sem_wait,线程将继续执行,但信号量的值会减到1。如果对值为0的信号量调用sem_wait,这个函数就会等待,直到有其它线程增加了该信号量的值使其不再为0为止。

  如果两个线程同时在sem_wait函数上等待同一个信号量变为非零值,那么当该信号量被第三个线程增加1时,只有其中一个等待线程将开始对信号量减1,然后继续执行,另外一个线程还将继续等待

(3)销毁

#include <semaphore.h>

int sem_destroy(sem_t *sem);

这个函数的作用是,用完信号量后对它进行清理,清理该信号量所拥有的资源。如果你试图清理的信号量正被一些线程等待,就会收到一个错误

2、互斥量

互斥量(mutex)从概念上来说类似于一个二进制信号量,即初始值为1的信号量。互斥量被获取之后就不能再被获取,因此对互斥体的获取和释放操作常常称为加锁和解锁操作。

 互斥量只能由获取它的线程进行释放,如果违反这一原则,则结果是未定义的。

互斥量从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥量。
 

线程在取出数据前,必须等待互斥量如果此时有其他进程已经获得该互斥量,那么该线程程阻塞,等待其他先释放掉该互斥量后,该线程才能得到该互斥量

在Linux中,互斥量使用类型pthread_mutex_t表示,在使用前,要对其初始化:

(1)初始化

对于静态分配的互斥量,设置为默认的mutex对象PTHREAD_MUTEX_INITIALIZER

 对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy

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

 //第一个参数指向要初始化的互斥量,第二个参数指向一个描述互斥量属性的结构体,可以设置为NULL,表示默认属性

int pthread_mutex_destroy(pthread_mutex_t *mutex);

(2)操作

对共享资源的访问, 要使用互斥量进行加锁, 如果互斥量已经上了锁, 调用线程会阻塞, 直到互斥量被解锁。

int pthread_mutex_lock(pthread_mutex_t *mutex)

int pthread_mutex_trylock(pthread_mutex_t *mutex)

返回值: 成功则返回0, 出错则返回错误编号。    

trylock是非阻塞调用模式, 如果互斥量没被锁住, trylock函数将对互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量被锁住了,trylock函数将不会阻塞等待而直接返回EBUSY, 表示共享资源处于忙状态。

 pthread_mutex_unlock(pthread_mutex_t *mutex)

用于对mutex参数指向的互斥量进行解锁。如果这时互斥量是未锁状态或不是当前线程所拥有的,则结果未定义。 因此,互斥量必须在同一线程上成对出现。在操作完成后,必须给互斥量解锁。这样其他等待该锁的线程才有机会获得该锁,否则其他线程将会永远阻塞。

(3)互斥量和信号量的差别

Mutex是一把钥匙,只能一个人进入,进去上锁,出来解锁; sem是一间可以容纳N人的房间,如果人不满就可以进去,如果人满,就要等待人出来

mutex要由获得锁的线程来释放(谁获得,谁释放)而semaphore可以由其它线程释放  

初始状态可能不一样:mutex的初始值是1 ,sem的初始值可能是0(或者为1)

3、条件变量

假设有这样一种情况:线程正在等待共享数据内某个条件出现,这时必须先解锁,否则其他线程不可能更改共享数据。一种实现方式是,可以循环检测共享数据,但是在检测前要加锁,检测后又要解锁,这样效率会很低。
          因此,在这种情况下,需要一种方法,使得当线程在等待某些条件的满足时进入睡眠状态,一旦条件满足,线程就应该被唤醒继续执行。这个方法就是使用POSIX条件变量。  

条件变量本身不是锁,但它也能造成线程阻塞, 通常与互斥锁配合使用,给多线程提供一个回合的场所。

(1)初始化

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

cond参数指向要初始化的条件变量,attr参数指向描述条件变量属性的结构体, attr可以为NULL,表示使用默认属性。

(2)等待

当程序中需要等待一个条件变量时,可以用下面的函数:

int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,  const struct timespec *abstime);

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

这些函数都有一个指向互斥量的参数mutex,说明条件变量必须与互斥量搭配使用。调用这些函数时,首先,指定的互斥量将被释放,然后线程将阻塞,等待条件变量的触发。函数返回前,互斥量重新被线程获取。

pthread_cond_timedwait()与pthread_cond_wait()的区别在于,前者有一个由abs time指定的超时时间限制,当线程阻塞的时间超过了这个时间就会自动醒来。 

(3)触发

触发一个条件变量可以用以下函数:

int pthread_cond_broadcast(pthread_cond_t *cond);

int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_signal可以唤醒一个或多个正在等待cond参数所指向的条件变量的线程,而pthread_cond_broadcast则可以唤醒全部正在等待cond参数所指向的条件变量的线程。
注意:POSIX标准只规定pthread_cond_signal函数唤醒至少一个睡眠中的线程,并没有规定只唤醒一个。  

 

(4)销毁

条件变量不用之后,应该用下面的函数进行销毁:

int pthread_cond_destroy(pthread_cond_t *cond);  

  • 2
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值