@(C Plus Plus)
经过前两次面试,发现多线程成了面试官的必选,要么让写一个pthread的程序,要么写一个读者写者问题的伪代码,好久没复习了,先把Linux下多线程的东西复习一下。
简介
进程— 资源分配的最小单位
线程— 程序执行的最小单位
进程是一个程序的一个实例,拥有自己独立的各种段(数据段,代码段等等),每次创建一个进程需要从操作系统分配这些资源给他,消耗一定的时间,在linux下C语言创建一个进程使用fork()
函数;
线程是一个轻量级的进程,除了自己少数的资源,不用用其他资源,且一个进程可以创建多个线程,这些线程共享进程的资源,创建线程的时间要比创建进程少很多,(几十分之一),从函数角度是使用clone()
创建。
使用线程处理文件I/O或者socket处理都是非常有优势的,将一个大人物分解成若干个小任务,每个线程处理一个任务,线程之间切换不需要花很多时间,而且线程之间数据交换很方便,共享存储区。
C语言中使用多线程的函数
创建线程
int pthread_create(pthread_t * tid, const pthread_attr_t * attr, void * ( * func) (void * ), void * arg);
其返回值是一个整数,若创建进程成功返回0,否则,返回其他错误代码,也是正整数。
创建线程需要的参数:
- 线程变量名:
pthread_t *
类型,是标示线程的id,一般是无符号整形,这里也可以是引用类型,目的是用于返回创建线程的ID - 线程的属性指针:制定线程的属性,比如线程优先*级,初始栈大小等,通常情况使用的都是指针。
- 创建线程的程序代码:一般是函数指针,进程创建后执行该函数指针只想的函数。
- 程序代码的参数:若线程执行的函数包含由若干个参数,需要将这些参数封装成结构体,并传递给它指针。
创建线程的函数的形式如下:
结束线程
结束进程的函数定义如下:
void pthread_exit (void *status);
参数是指针类型,用于存储线程结束后返回状态。
线程等待
int pthread_join (pthread_t tid, void ** status);
- 第一个参数表示要等待的进程的id;
- 第二参数表示要等待的进程的返回状态,是个二级指针。
线程创建后怎么执行,新线程和老线程谁先执行这些不是程序来决定,而是由操作系统进行调度的,但是在编程的时候我们常常需要多个线程配合工作,比如在结束某个线程之前,需要等待另外一个线程的处理结果(返回状态等信息),这时候就需要使用线程等待函数,这个函数的定义如下:
其他关于进程的函数
返回当前线程ID
pthread_t pthread_self (void);
用于返回当前进程的ID制定线程变成分裂状态
int pthread_detach (pthread_t tid);
参数是指定线程的ID,指定的ID的线程变成分离状态;若指定线程是分离状态,则 如果线程退出,那么它所有的资源都将释放,如果线程不是分离状态,线程必须保留它的线程ID、退出状态,直到其他线程对他调用的pthread_join()
函数
参考实例一
代码如下:
1 | #include <stdio.h> |
这个代码比较简单,就是演示这几个常用函数的使用。
这里是纯C语言程序,在Linux下的编译命令是gcc main.c -test -lpthread
,运行程序是./test
,后面的程序同样
多线程的同步与互斥
锁机制
多线程之间可能需要互斥的访问一些全局变量,这就需要互斥的来访问,这些需要共享访问的字段被称作是临界资源,访问临界资源的程序段称作是临界区。
实现线程间的互斥与同步机制的是锁机制,下面是常用的锁机制的函数和类。
pthread_mutex_t mutex
锁对象pthread_mutex_init(&mutex,NULL)
在主线程中初始化锁为解锁状态pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
编译时初始化锁位解锁状态pthread_mutex_lock(&mutex)
: 访问临界区加锁操作pthread_mutex_unlock(&mutex)
: 访问临界区解锁操作
参考实例二(不加锁访问互斥全局变量)
1 | #include <stdio.h> |
编译运行结果,多运行几次,发现结果都不一样。这就是因为对于全局变量,没有添加互斥锁,导致的问题。
参考实例三 (访问全局变量添加互斥锁)
1 | #include <stdio.h> |
添加互斥锁后,就发现,多次运行的结果都是一样的。
- 其实这里的加锁不是对共享变量(全局变量)或者共享内存进行保护,这里的加锁实际上是对临界区的控制,所谓的临界区就是访问临界资源的那一段代码,这段代码对临界资源进行多种操作,正确的情况是不允许这段代码执行到一半,处理器使用权就被其他线程抢走,所以这段代码具有原子性,即要么执行,要么不执行,不能执行到一半就被抢走处理权,这样就会造成共享数据被污染。
- 还有一点,添加锁来控制临界区是有代价的,这个代价表现出来就是时间的额外开销,内部过程是因为要保护现场,会利用一些资源,也需要处理器处理的时间。
信号量机制
锁机制使用是有限制的,锁只有两种状态,即加锁和解锁,对于互斥的访问一个全局变量,这样的方式还可以对付,但是要是对于其他的临界资源,比如说多台打印机等,这种方式显然不行了。
信号量机制在操作系统里面学习的比较熟悉了,信号量是一个整数计数器,其数值表示空闲临界资源的数量。
当有进程释放资源时,信号量增加,表示可用资源数增加;当有进程申请到资源时,信号量减少,表示可用资源数减少。这个时候可以把锁机制认为是0-1信号量。
关于信号量机制的函数。
int sem_init(sem_t * sem, int pshared, unsigned int value);
初始化信号量
- 成功返回0,失败返回-1;
- 参数sem:表示指向信号结构的指针。
- 参数pshared:不是0 的时候该信号量在进程间共享,否则只能在当前进程的所有线程间共享。
- 参数value:信号量的初始值。
int sem_wait(sem_t *sem);
信号量减一操作,有线程申请资源
- 成功返回0,否则返回-1
- 参数sem:指向一个信号量的指针
int sem_post(sem_t *sem);
信号量加一操作,有线程释放资源
- 成功返回0,否则返回-1
- 参数sem:指向一个信号量指针
int sem_destroy(sem_t *sem);
销毁信号量。
- 成功返回0,否则返回-1
- 参数sem:指向一个信号量的指针。
参考实例四(生产者消费者)
1 | #include <stdio.h> |
这段代码是经典的生产者消费者问题,只有当生产者把资源放入存储区,消费者才能取得。