目录
| 线程 | 进程 | |
| 标识符类型 | pthread_t | pid_t |
| 获取ID号 | pthread_self() | getpid() |
| 创建 | pthread_create() | fork() |


makefile文件书写:


提示:可以看到主函数中的进程ID号和父进程的ID号是一样的,主函数下线程的ID号和打印函数下的线程ID号是不一样的,说明两个不同的线程共享了同样的进程资源。


#include
- int pthread_create((pthread*thread,pthread_attr_t*attr,
- void*(*start_routine)(void*),void arg))
(看了上面参数的解释感觉还是不知道在讲什么,到底是什么意思,继续看面,后面通过实例来理解)。
例子:
C文件代码:

Makefile文件书写:

提示:如果不使用Makefile文件来管理,也可以直接输入:gcc -lpthread pthread_Create.c -o pthread生成可执行文件pthread.

例子1:使用三种不同的结束线程的方式,查看输出的结果并总结其中的区别。


makefile文件书写:


提示:可以看到各个输出的结果,使用pthread_exit和return返回语句的时候会等待主线程的结束,而使用exit的话不会等待主线程的结束直接终止。
例子2:使用pthread_join和pthread_detach实战
首先知道怎么查看错误码:sudo gedit /usr/inlcude/asm-generic/errno-bash.h


makefile文件书写:

提示:可以看到thread2的retval2值为22,通过查看上面给出的错误表,可以看到22对应的为 :EINVAL
再查看pthread_join手册:man pthread_join
从pthread_join手册可以看到当调用了pthread_detach函数分离线程之后,再调用pthread_join(tid2,&retval2)连接的时候会出错,并且退出码返回的不是2,而是一个计算机中随机的值。
1.初始线程:当程序执行时,首先运行在主函数中,而在线程代码中,这个特殊的执行流程也称为初始线程。
2.当主函数结束时,会导致进程结束,进程内所有的线程也都会结束。但是可以调用pthread_exit函数(后面详解),等待所有的线程结束时进程才终止。
3.大多数情况下,主线程默认是在堆栈上运行的,该堆栈可以增长到足够的长度,但是普通的堆栈是受限制的,一旦溢出将会产生错误。
例子1:定义一个结构体,并且创建一个线程,将结构体传入,最后打印输出主函数参数。

Makefile文件书写:




拓展:可以上面这个例子在主函数的最后使用sleep进行了睡眠,但是如果将该睡眠放置在thread函数中会是什么结果呢?如下:


提示:从输出的结果来判断,由于在thread设置了睡眠,可以发现主函数在thread函数打印出之前就已经终止线程了,所以 thread函数还没有来得及打印结果,线程就已经结束了。
那么解决这个问题了,这个时候就可以使用上面提到的pthread_exit函数让等待所有的线程结束之后再终止。如下:

例子2:主函数打印奇数,创建的线程打印偶数。



提示:如果读者学习过操作系统的话,上面的知识点将非常容易理解。



Makefile文件书写:


提示:从退出码的结果20来看,退出是正常的。
但是如果进行下面的改动之后:将取消线程的后面设置取消类型,这个取消类型设置为立即执行(也就是后面的操作不再执行)。


提示:从输出的结果来看,printf("finally!\n")没有执行,并且退出码也不是20,说明不是正常的退出,说明设置取消和设置取消类型成功。

例子:测试新建的线程结束之后,再向新建的线程发送一个0的信号,如果出错的话,那么将打印出错的信息。



提示:为什么是这样的结果?主要是因为在主线程中睡眠了1秒,所以这个时候新线程已经执行完毕,那么再向新线程发送信息,将会出错!

例子:


Makefile文件书写:


例子:使用注册处理函数和清除处理函数实现一个清除的过程。




提示:如果将pthread_cleanup_pop(0)参数修改为0的时候,是不执行清除操作的,但是如果修改成1的话,那么将执行清除操作:
提示:如果调用pthread_exit的话,也会响应清除请求,比将代码修改为如下面:
![]()
在线程的私有数据之间,尽管名字是相同的,但是线程访问的都是数据的副本。
首先需要创建一个与私有数据相关的键,获取对私有数据的访问权限。
- int pthread_key_create(pthread_key_t*key,void(*destructor)(void*));
- 其中创建的key放在指定的内存单元, destructor是与键相关的析构函数(当调用pthread_exit或者return的时候,会调用析构函数)。当析构函数被调用的时候,只有一个参数,就是与key关联的数据地址。
- 使用键可以和私有数据进行关联,之后就可以通过键来查找数据,所有的线程都可以访问创建的键。
- 将键与私有数据进行关联
- void*pthread_setspecific(pthread_key_t key,const void*value);
- 获取私有数据,如果没有关联,则返回空
- void*pthread_getspecific(pthread_key_t key);


提示:可以设置不同的键值,之间是不相互影响的。
提示:对于大部分创建线程的情况,使用默认的属性即可。
线程的绑定需要:轻进程LWP(Light Weight Process)
- 位于用户层和系统层之间
- 系统对于线程资源的分配和对线程的控制是通过轻进程实现的
- 一个轻进程可以控制一个或者多个线程
- 非绑定:默认属性情况下,启动多少轻进程,哪些轻进程来空间哪些线程是由系统来控制的(非绑定)
- 绑定:某个线程固定的“绑”在一个轻进程之上,并且被绑定的线程具有较高的响应速度。
- pthread_attr_t attr;
- pthread_t tid;
- //初始化属性,均为默认属性
- pthread_attr_init(&attr);
- pthread_attr_setscope(&attr,PTHREAD_SCOPE_SYSTEM);
- //创建一个线程,thread是定义的一个函数
- pthread_create(&id,&attr,(void*)thread,NULL);
分离属性决定一个线程 以什么样的方式终止:
- 默认属性情况下为非分离属性,也就是原有的线程等待创建的线程结束;
- 默认情况下只有当pthread_join函数返回时,创建的线程才算终止,才能释放自己的资源。
- 如果设置为分离的属性的话,不会被其他的线程所等待,自己运行结束了,线程也就结束了,马上释放资源
例子:通过设置分离属性和一个非设置分离属性(默认)线程,输出查看设置分离属性和非分离属性的结果。




提示:可以看到线程1在设置了分离的属性之后,连接是失败的。
对于线程,虚拟地址空间大小是固定的,进程只有一个栈;但是对于线程,同样的虚拟地址被所有的线程共享,如果应用程序使用了太多的线程,那么导致线程累计和超过可用的虚拟地址空间,那么这个时候需要减少线程默认的栈大小;但是如果线程分配了大量的自动变量或者线程的栈太深,那么这个时候需要使得默认的栈更大。




比较常用的属性是线程的优先级
- 存放的结构:sched_param
- 调用函数:pthread_attr_getschedparam和pthread_attr_setschedparam.
- 总是首先获取优先级,获取优先级之后再是设置优先级。
- #include<pthread.h>
- #include<sched.h>
-
- pthread_t tid;
- pthread_attr_t attr;
- sched_param param;
-
- int newprio=20;
-
- pthread_attr_init(&attr);
- //首先获取优先级
- pthread_attr_getschedparam(&attr,¶m);
-
- //获取优先级之后再设置优先级
- param.sched_priority=newprio;
- pthread_attr_setschedparam(&attr,¶m);
-
- //创建线程,其中thread是定义的一个函数,thparam是thread函数的参数
- pthread_create(&tid,&attr,(void*)thread,thparam);
由于多线程共享同一个进程的资源和地址,因此对这些资源的操作,必须要考虑线程之间资源访问的唯一性问题。
- 线程同步可以使用互斥锁和信号量机制(操作系统之间有详细讲解)来解决线程之间数据的共享和通信问题。
- 互斥锁
- 锁定和非锁定
- 条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。
- 当条件变量被用来阻塞一个线程,当条件不满足时,线程解开相应的互斥锁并等待条件发生变化。
- 一旦其他的某个线程改变了条件变量,将通知相应的条件变量唤醒一个或者多个正在被此条件变量阻塞的线程。
- 这些线程将重新锁定互斥锁并重新测试条件是否满足。
例子:假设两个线程没有同步,输出结果:


例子1:依然是上面的一个例子进行修改。


线程互斥量属性pthread_mutexattr_t类型的数据表示。
进程共享属性两种值:
- PTHREAD_PROCESS_PRIVATE:默认值,同一个进程中的多个线程访问同一个同步对象。
- PTHREAD_PROCESS_SHARED:可以使得互斥量在多个进程中进行同步,如果互斥量在多进程的共享内存区域,那么这个属性的互斥量可以同步多进程。
| 互斥量类型 | 没有解锁时再次加锁 | 不占用解锁 | 已解锁时再次解锁 |
| PTHREAD_MUTEX_NORMLA | 死锁 | 未定义 | 未定义 |
| PTHREAD_MUTEX_ERRORCHEK | 返回错误 | 返回错误 | 返回错误 |
| PTHREAD_MUTEX_RECURSIVE | 允许 | 返回错误 | 返回错误 |
| PTHREAD_MUTEX_DEFAULT | 未定义 | 未定义 | 未定义 |
注意下面的程序编译方式:sudo gcc -o pDemo01 pDemo01.c -lrt


使用Makefile文件:


例子:验证两个线程可以同时读锁。



提示:
1.可以看到当线程1拥有了读锁之后,线程2也可以拥有读锁。
2.但是对于写加锁状态的话,当两个线程都是写状态的时候,只能有一个线程在执行,只有当该线程执行完毕之后,那么另一个线程才能开始执行。
3.如果是一个写加锁,另一个是读加锁的话,也是只能有一个线程在执行,只有当该线程执行完毕之后,那么另一个线程才能开始执行。
例子1:交替打印奇数和偶数



例子2:下面的综合例子消费者和生产者就是最好的应用。
著名的生产者-消费者问题模型的实现,主程序中分别启动生产者线程和消费者 线程。生产者线程不断顺序地将 0 到 50 的数字写入共享的循环缓冲区,同时消费者线程 不断地从共享的循环缓冲区读取数据。
- #include <stdio.h>
- #include <stdlib.h>
- #include <time.h>
- #include <pthread.h>
- #define BUFFER_SIZE 16
- /* 设置一个整数的圆形缓冲区 */
- struct prodcons {
- int buffer[BUFFER_SIZE]; /* 缓冲区数组 */
- pthread_mutex_t lock; /* 互斥锁 */
- int readpos, writepos; /* 读写的位置*/
- pthread_cond_t notempty; /* 缓冲区非空信号 */
- pthread_cond_t notfull; /*缓冲区非满信号 */
- };
- /*--------------------------------------------------------*/
- /*初始化缓冲区*/
- void init(struct prodcons * b)
- {
- pthread_mutex_init(&b->lock, NULL);
- pthread_cond_init(&b->notempty, NULL);
- pthread_cond_init(&b->notfull, NULL);
- b->readpos = 0;
- b->writepos = 0;
- }
- /*--------------------------------------------------------*/
- /* 向缓冲区中写入一个整数*/
- void put(struct prodcons * b, int data)
- {
- pthread_mutex_lock(&b->lock);
- /*等待缓冲区非满 when the buffer is not full and you can write a number*/
- while ((b->writepos + 1) % BUFFER_SIZE == b->readpos) {
- printf("wait for not full\n");
- pthread_cond_wait(&b->notfull, &b->lock);
- }
- /*写数据并且指针前移*/
- b->buffer[b->writepos] = data;
- b->writepos++;
- if (b->writepos >= BUFFER_SIZE) b->writepos = 0;
- /*设置缓冲区非空信号*/
- pthread_cond_signal(&b->notempty);
- pthread_mutex_unlock(&b->lock);
- }
- /*--------------------------------------------------------*/
- /*从缓冲区中读出一个整数 */
- int get(struct prodcons * b)
- {
- int data;
- pthread_mutex_lock(&b->lock);
- /*等待缓冲区非空when the buffer is not empty and you can read a number*/
- while (b->writepos == b->readpos) {
- printf("wait for not empty\n");
- pthread_cond_wait(&b->notempty, &b->lock);
- }
-
- /* 读数据并且指针前移 */
- data = b->buffer[b->readpos];
- b->readpos++;
- if (b->readpos >= BUFFER_SIZE) b->readpos = 0;
- /* 设置缓冲区非满信号*/
- pthread_cond_signal(&b->notfull);
- pthread_mutex_unlock(&b->lock);
- return data;
- }
- /*--------------------------------------------------------*/
- #define OVER (-1)
- struct prodcons buffer;
- /*--------------------------------------------------------*/
- void * producer(void * data)
- {
- int n;
- for (n = 0; n < 50; n++) {
- printf(" put-->%d\n", n);
- put(&buffer, n);
- }
- put(&buffer, OVER);
- printf("producer stopped!\n");
- return NULL;
- }
- /*--------------------------------------------------------*/
- void * consumer(void * data)
- {
- int d;
- while (1) {
- d = get(&buffer);
- if (d == OVER ) break;
- printf(" %d-->get\n", d);
- }
- printf("consumer stopped!\n");
- return NULL;
- }
- /*--------------------------------------------------------*/
- int main(void)
- {
- pthread_t th_a, th_b;
- void * retval;
- init(&buffer);
- pthread_create(&th_a, NULL, producer, 0);
- pthread_create(&th_b, NULL, consumer, 0);
- /* 等待生产者和消费者结束 */
- pthread_join(th_a, &retval);
- pthread_join(th_b, &retval);
- return 0;
- }
像互斥量只能初始化一次,一般的初始化方法都是放在主函数main中,但是对于库函数不能这样做,如果要实现一次性初始化,可以使用下面的方法:
- 首先定义一个pthread_once_t类型的变量,使用PTHREAD_ONCE_INIT进行初始化
- pthread_once_t once=PTHREAD_ONCE_INIT;
- 初始化函数书写:
void init_routine(){
//初始化互斥量
//初始化读写锁
}
- 调用函数
- int pthread_once(pthread_once_t*once_control,void(*init_routine)(void))
- 函数功能:该函数使用PTHREAD_ONCE_INIT初始化once_control变量,保证init_routine函数在本进程中执行时,仅仅执行一次。在多个线程的情况下,即使pthread_once函数被调用了多次,init_routine函数也只会执行一次。
- PTHREAD_ONCE_INIT如果为0,表示pthread_once从没有执行过,init_routine函数就会执行;
- PTHREAD_ONCE_INIT如果为1,表示pthread_once都必须等待其中一个激发“已经执行一次”信号,因此所有的pthread_once都会处于阻塞当中,init_routine函数就不会执行;
- PTHREAD_ONCE_INIT如果为2,表示pthread_once已经执行过一次了,init_routine函数以后都不会执行了;




提示:如果将once的值设置为1,那么将处于阻塞状态。
教程主要来源
《嵌入式Linux操作系统应用于开发》