Linux --- 多线程

一.线程的基本概念

1.线程概念
进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;

线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。

2.线程的优点

  • (1)创建一个新线程的代价要比创建一个新进程小得多

  • (2)与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

  • (3)线程占用的资源要比进程少很多

  • (4)能充分利用多处理器的可并行数量

  • (5)在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

  • (6)计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

  • (7)I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

3.线程的缺点

  • (1)性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享一个处理器,如果计算密集型线程的数量比可用的处理器多,那么就会造成较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

  • (2)健壮性降低:编写多进程需要更全面深入的考虑,在一个多线程程序里,因事件分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

  • (3)缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些os函数会对整个进程造成影响。

  • (4)编程难度提高:编写与调试一个多线程程序比单线程程序困难得多。

4.线程异常

  • (1)单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随之崩溃。
  • (2)线程是进程得执行分支,线程出异常,就类似于进程出异常,进而触发信号机制,终止进程,进程终止,该进程内得所以线程也就会随之退出。

5.线程用途

  • (1)合理得使用多线程,能提高CPU密集型程序得执行效率
  • (2)合理得使用多线程,能提高IO密集型程序的用户体验。

6.进程与线程之间的区别

  • (1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。

  • (2)进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)

  • (3)进程是资源分配的最小单位,线程是CPU调度的最小单位;

  • (4)系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/o设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销。

  • (5)通信:由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预

  • (6)进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。

  • (7)进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉

  • (8)进程适应于多核、多机分布;线程适用于多核

二.线程控制

1.创建线程

//功能:创建一个新的线程 
//原型    
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void*), void *arg); 
//参数    
	//thread:返回线程ID    
	//attr:设置线程的属性,
	//attr为NULL表示使用默认属性    
	//start_routine:是个函数地址,线程启动后要执行的函数    
	//arg:传给线程启动函数的参数 
//返回值:成功返回0;失败返回错误码
//创建一个线程的基本操作。
  1 #include<iostream>
  2 #include<pthread.h>
  3 #include<string>
  4 #include<sys/types.h>
  5 #include<unistd.h>
  6 
  7 
  8 using namespace std;
  9 
 10 void *thread_routine(void* arg)//调用函数指针
 11 {
 12     string str = (char*) arg;
 13     while(1)
 14     {
 15         cout << str << " run "  << "pid : " << getpid() << endl;
 16         sleep(1);
 17     }
 18 }
 19 int main()//两个线程的pid相同
 20 {
 21     pthread_t tid;
 22     pthread_create(&tid, NULL, thread_routine, (void*) "thread 1");//创建线程
 23     while(1)
 24     {
 25         cout << "main thread run " << " pid : " << getpid() << endl;
 26         sleep(2);
 27     }
 28     return 0;
 29 }

在这里插入图片描述
对于线程而言:
LMP:线程ID,也就是gettid()的返回值。
NLWP:线程组内线程的个数。

2.线程等待(默认是阻塞式的)
线程join只关心线程结果是否正确,默认线程是成功的(没有出异常的)。

//功能:等待线程结束 
//原型    
	int pthread_join(pthread_t thread, void **value_ptr); 
//参数    
	//thread:线程ID    
	//value_ptr:它指向一个指针,后者指向线程的返回值 
//返回值:成功返回0;失败返回错误码

3.线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
//功能:线程终止 
//原型    
	void pthread_exit(void *value_ptr); 
//参数    
	value_ptr:value_ptr不要指向一个局部变量。 
//返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
//需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,
//不能在线程函数 的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了
  1 #include<iostream>
  2 #include<pthread.h>
  3 #include<string>
  4 #include<sys/types.h>
  5 #include<unistd.h>
  6 #include<cstdio>
  7 #include<stdlib.h>
  8 using namespace std;
  9 
 10 void *thread_routine(void* arg)
 11 {
 12     while(1)
 13     {
 14         printf("Hello I am a new thread,my name is : %s\n", (char*)arg);
 15         sleep(1);
 16         break;
 17     }
 18     //return (void*)11;
 19     //exit(3);
 20     pthread_exit((void*)3);//线程退出
 21 }
 22 int main()
 23 {
 24     pthread_t tid;
 25     pthread_create(&tid, NULL, thread_routine, (void *)"thread 1");//创建线程
 26     printf("Hello I am main thread: %p\n", tid );
 27 
 28     void * ret;
 29     pthread_join(tid, &ret);//线程等待,jion的第二个参数是void* 的返回值
 30     printf("ret : %d\n", ret);//打印退出码
 31     return 0;//main函数return代表进程退出。
 32 }

4.线程取消

//功能:取消一个执行中的线程 
//原型    
	int pthread_cancel(pthread_t thread); 
//参数    
	thread:线程ID 
//返回值:成功返回0;失败返回错误码

5.获取线程自身的id

  • (1)pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是 一回事。
  • (2)前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要 一个数值来唯一表示该线程。
  • (3)pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于 NPTL线程库的范畴。
  • (4)线程库的后续操作,就是根据该线程ID来操作线程的。 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
pthread_t pthread_self(void);

pthread_t到底是什么类型?取决于实现,对于目前实现的NPTL实现而言,pthread_t类型的线程ID,本质上就是一个进程地址空间的地址。

6.线程分离

  • (1)默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资 源,从而造成系统泄漏。
  • (2) 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程 资源。
int pthread_detach(pthread_t thread);

新线程可以取消主线程,但是主线程不会退出,主线程就会变成僵尸进程,没有人回收。
整个进程的退出交给bash。
当不关心线程的运行状态或者是线程的资源回收交给操作系统的话,就叫做线程分离。

三.线程互斥

1.概念

  • (1)临界资源:多线程执行流共享的资源就叫做临界资源。
  • (2)临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
  • (3)互斥:任何时候,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
  • (4)原子性:不会被任何调度机制打断的操作,该操作只有两个状态,要么完成,要么未完成
  1 #include<iostream>
  2 #include<pthread.h>
  3 #include<string>
  4 #include<sys/types.h>
  5 #include<unistd.h>
  6 #include<cstdio>
  7 #include<stdlib.h>
  8 using namespace std;
  9 
 10 int tickets = 100;
 11 
 12 void* GetTicket(void* args)
 13 {
 14     while(1)
 15     {
 16         if(tickets > 0)
 17         {
 18             usleep(10000);
 19             printf("get a ticket no . is : %d\n",tickets--);
 20         }
 21         else
 22         {
 23             printf("%s ... quit\n", (char*)args);
 24             break;
 25         }
 26     }
 27 }
 28 int main()
 29 {
 30     pthread_t tid1, tid2, tid3, tid4;
 31     pthread_create(&tid1, NULL, GetTicket, (void *)"thread 1");
 32     pthread_create(&tid2, NULL, GetTicket, (void *)"thread 2");
 33     pthread_create(&tid3, NULL, GetTicket, (void *)"thread 3");
 34     pthread_create(&tid4, NULL, GetTicket, (void *)"thread 4");
 35 
 36     pthread_join(tid1, NULL);
 37     pthread_join(tid2, NULL);
 38     pthread_join(tid3, NULL);
 39     pthread_join(tid4, NULL);
 40     return 0;
 41 }

对于上面的代码编程,最后的结果不是正确的,为什么会这样?主要有以下几点:
(1)if语句判断条件为真以后,代码可以并发的切换到其他线程。
(2)usleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
(3)–ticket操作本身就不是一个原子操作。

上面操作不是原子性,而是对应三个操作
(1)load:将共享变量ticket从内存中加载到寄存器中。
(2)updata:更新寄存器中的值,执行-1操作。
(3)store:将新值,从寄存器写回共享变量ticket的内存地址。

要解决以上问题,需要做到三点:
(1)代码必须有互斥行为,当代码进入临界区执行时,不允许其他线程进入临界区。
(2)如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
(3)如果线程不在临界区中执行,那么该线程不能组织其他线程进入临界区。
要做到三点,本质上就是需要一把锁,Linux上提供的这把锁叫互斥量。

2.初始化互斥量的两种方法

//(1)静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

//(2)动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);    
参数:        
	//mutex:要初始化的互斥量        
	//attr:NULL

3.互斥量的销毁
使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量 已经销毁的互斥量,
要确保后面不会有线程再尝试加锁。

int pthread_mutex_destroy(pthread_mutex_t *mutex)

4.互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex); 
int pthread_mutex_unlock(pthread_mutex_t *mutex); 
//返回值:成功返回0,失败返回错误号

调用pthread_lock时,可能会遇到以下情况:
(1)互斥量处于未锁状态,那么函数会将互斥量锁定,同时返回成功。
(2)发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock会陷入阻塞(执行流被挂起),等待互斥量解锁。

//将上面的代码修改如下,加上锁,就保证了原子性。
  1 #include<iostream>
  2 #include<pthread.h>
  3 #include<string>
  4 #include<sys/types.h>
  5 #include<unistd.h>
  6 #include<cstdio>
  7 #include<stdlib.h>
  8 using namespace std;
  9 
 10 int tickets = 100;
 11 pthread_mutex_t lock;//设置一个全局的锁
 12 
 13 void* GetTicket(void* args)
 14 {
 15     while(1)
 16     {
 17         pthread_mutex_lock(&lock);//加锁
 18         if(tickets > 0)
 19         {
 20             usleep(10000);
 21             printf("get a ticket no . is : %d\n",tickets--);
 22             pthread_mutex_unlock(&lock);//解锁
 23         }
 24         else
 25         {
 26             printf("%s ... quit\n", (char*)args);
 27             pthread_mutex_unlock(&lock);//解锁
 28             break;
 29         }
 30     }
 31 }
 32 int main()
 33 {
 34     pthread_t tid1, tid2, tid3, tid4;
 35     pthread_mutex_init(&lock,NULL);//初始化互斥量(锁)
 36     pthread_create(&tid1, NULL, GetTicket, (void *)"thread 1");
 37     pthread_create(&tid2, NULL, GetTicket, (void *)"thread 2");
 38     pthread_create(&tid3, NULL, GetTicket, (void *)"thread 3");
 39     pthread_create(&tid4, NULL, GetTicket, (void *)"thread 4");
 40 
 41     pthread_join(tid1, NULL);
 42     pthread_join(tid2, NULL);
 43     pthread_join(tid3, NULL);
 44     pthread_join(tid4, NULL);
 45		pthread_mutex_destroy(&lock);//销毁锁。
 46     return 0;
 47 }

cpu内部有若干寄存器:通用寄存器,状态寄存器,指令寄存器。
上下文信息:线程在运行期间运行到什么地方,运行的状态等。

四.可重入和线程安全

1.概念:

  • (1)线程安全:多个线程并发同一段代码时,不会出现不同的结果,常见对全局变量或者静态变量进行操作,并且在没有锁保护的情况下,会出现该问题。
  • (2)重入:同一个函数被不同的执行流调用,当前一个进程还没有执行完,就有其他的执行流再次进入,我们将其称为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

2.常见的线程不安全的情况

  • (1)不保护共享变量的函数
  • (2)函数状态随着被调用,状态发生变化的函数
  • (3)返回指向静态变量指针的函数
  • (4)调用线程不安全函数的函数。

3.常见的线程安全的情况

  • (1)每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • (2)类或者接口对于线程来说都是原子操作
  • (3)多个线程之间的切换不会导致该接口的执行结果存在二义性

4.常见的不可重入的函数

  • (1)不使用全局变量或静态变量
  • (2)不使用用malloc或者new开辟出的空间
  • (3)不调用不可重入函数
  • (3)不返回静态或全局数据,所有数据都有函数的调用者提供 。
  • (4)使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。

5.常见可重入的情况

  • (1)不使用全局变量或静态变量
  • (2)不使用用malloc或者new开辟出的空间
  • (3)不调用不可重入函数
  • (4)不返回静态或全局数据,所有数据都有函数的调用者提供
  • (5)使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

6.可重入与线程安全联系

  • (1)函数是可重入的,那就是线程安全的
  • (2)函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • (3)如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

7.可重入与线程安全区别

  • (1)可重入函数是线程安全函数的一种
  • (2)线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • (3)如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生 死锁,因此是不可重入的

五.死锁

1.概念:
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资 源而处于的一种永久等待状态。

死锁四个必要条件

  • (1)互斥条件:一个资源每次只能被一个执行流使用
  • (2)请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • (3)不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • (4)循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁

  • (1)破坏死锁的四个必要条件
  • (2)加锁顺序一致
  • (3)避免锁未释放的场景
  • (4)资源一次性分配

避免死锁算法

  • 死锁检测算法(了解)
  • 银行家算法(了解)

六.Linux线程同步

1.条件变量:

  • (1)当一个线程互斥地访问某个变量时,它可能发现在其他线程改变状态之前,它什么都做不了。
  • (2)例如,一个线程访问队列时,发现队列为空,它只能等待,直到其他线程将一个节点添加到队列中,这种情况都需要用到条件变量。

2.同步概念与竞态条件
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问 题,叫做同步 。
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。

3.条件变量函数
(1)初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); 
//参数:    
	//cond:要初始化的条件变量    
	//attr:NULL

(2)销毁

int pthread_cond_destroy(pthread_cond_t *cond)

(3)等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);    
//参数:        
	//cond:要在这个条件变量上等待        
	//mutex:互斥量,

(4)唤醒等待

 int pthread_cond_broadcast(pthread_cond_t *cond);   
 int pthread_cond_signal(pthread_cond_t *cond);

七.生产者和消费者模型

1.概念:生产者消费者模型就是通过一个容器来解决生产者和消费者的强耦合问题,生产者和消费者彼此之间不直接通讯,而是通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力,这个阻塞队列就是用来给生产者和消费者解耦的。

能够进行生产和消费的只能是进程或者线程。
交易场所是内存空间,货物就是数据。

(2)三种关系(两类角色和一个交易场所)
生产者和生产者(互斥)
消费者和消费者(互斥)
生产者和消费者(同步与互斥)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章