Linux--多线程

一,线程概念 

1,什么是线程

a,在一个程序里的一个执行路线就叫做线程(thread)

b,一切进程都至少有一个线程

c,线程在进程内部运行,其本质是在进程地址空间内运行

d,在Linux系统中,CPU眼里,看到的PCB比传统的进程更加轻量化

e,透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

 2,线程的优点

1,创建一个新线程的代价要比创建一个新进程小得多
2,与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3,线程占用的资源要比进程少很多
4,能充分利用多处理器的可并行数量
5,在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6,计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7,I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

3,线程缺点

1,健壮性降低:在多线程程序里,由于时间分配的细微偏差或者因为共享了不该共享的变量,从而导致不良影响。线程是不安全的,缺乏保护

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

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

4,编程难度提高:不易调试,

4,线程异常

1,如果任何一个线程发生异常,那么整个进程都会崩溃掉。

2,线程是进程的执行分支,如果线程出异常,就类似与进程抛异常,也会终止掉整个进程,进程终止,所有线程也会终止。

5,线程用途

1,合理使用多线程,能提高CPU密集型程序的执行效率。

2,合理使用多线程,能提高IO密集型程序的用户体验。

二,进程VS线程

1,进程是资源分配的基本单位

2,线程是调度的基本单位

3,线程之间共享数据,但也有自己的一部分资源

        a.线程ID

        b.一组寄存器

        c.栈

        d.errno

        f.信号屏蔽字

        g.调度优先级

三,线程控制

1,线程的创建

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

参数:thread 线程ID

           attr:设置线程属性,设置为NULL,表示按照默认的属性

          start_routine:函数地址,线程做事的地方。

           arg :传给线程启动时的参数,NULL就是不传入数据。

pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回

pthread_t 实际上是地址
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小

#include<stdio.h>
#include<pthread.h> 
#include<unistd.h>
int count=0;
void Rountinue()
{
	while(count<5) 
	{
		printf(" i am pthread:%d\n",getpid());	
		count++;
		sleep(1);
	}
	pthread_exit((void*)19);
}
int main()
{
	pthread_t tid;//申请线程ID, 
	pthread_create(&tid,NULL,(void*)Rountinue,NULL);//创建线程
	
	
	//主线程做什么 
	while(1)
	{
		printf("i am main thread :pid:%d:tid:%d\n",getpid(),pthread_self());//pthread_self,获得自身的轻量级id号 
		sleep(1); 
	}
	void *ret=NULL;
	pthread_join(tid, &ret);
	printf("%d pthread quit ....quit code:%d\n",tid,(int)ret);
	return 0;
} 
	

我们可以通过ps-aL查看线程的唯一标识号,

1,我们可以看到pid是相同的,所以可以推出 OS调度时是通过LWP进行调度的,并非PID,

2,在linux中线程与LWP是唯一对应的.
3,线程也需要等待,就如同子进程必须被等待一样,线程也可以不被Join,但需要把线程剥离出来。

4,线程很多,也需要被管理,先描述再组织,linux不提供真正的线程,只提供轻量级进程LWP,意味着OS只需要对线程的执行流进行管理,供用户使用的接口等待其他数据,所以应该由谁来管理呢,由线程库进行管理

2,线程终止

1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2. 线程可以调用pthread_ exit终止自己。
3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

pthread_exit(void *value_ptr)

参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

pthread_cancel函数

int pthread_cancel(pthread_t thread);

参数
thread:线程ID 返回值:成功返回0;失败返回错误码

3,分离线程

1,线程在退出以后需要被等待,否则无法释放资源。

2,如果不担心线程的返回值,join是一种负担,我们可以告诉系统,当线程退出后,自动释放资源。

int pthread_detach(pthread_t thread);
//也可以自己分离自己
pthread_detach(pthread_self());

线程分离与线程的join是冲突的,一个线程不能既等待,又分离。

四,线程互斥

1,临界资源

        如下代码,当进程跑起来以后,发现最后打印的结果有乱码,这是因为当前有--操作,比如当一号线程去--操作时,二号进程也进来了,同时对ticket--,此时有可能存在减了两次,但值实际变化了一次,

所以:我们将多执行流共享的资源叫做临界资源

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>

int tickets=10000;
void *getTicket(void* arg)
{
	int number=(int)arg;
	  while(1)
	  {
	      if(tickets>0)
	      {
	         usleep(100);
	         printf("thread[ %d ]抢票:  %d\n",number,tickets--);
	      }
	      else
	      { 
	         break;
	      }
	  }
}
int main()
{
	pthread_t tid[5];
	for(int i=0;i<5;i++)
	{
		pthread_create(&tid[i],NULL,getTicket,(void*)i);
	}
	for(int i=0;i<5;i++)
	{
		pthread_join(tid[i],NULL);
	}
	return 0;
}

2,临界区:每个线程内部,访问临界资源的代码,叫做临界区

3,互斥:任何时刻,互斥保证有且仅有一个线程访问临界资源,通常对临界区起保护作用

4,原子性:该操作只有两态,完成和未完成

二,互斥量

1,大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,其他线程无法共享变量。

但有时候,线程需要共享变量,通过共享变量,来完成一些线程间的交互。

但多线程之间的并发操作会带来一些问题。

如下代码:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>

int tickets=10000;
void *getTicket(void* arg)
{
	int number=(int)arg;
	  while(1)
	  {
	      if(tickets>0)
	      {
	         usleep(100);
	         printf("thread[ %d ]抢票:  %d\n",number,tickets--);
	      }
	      else
	      { 
	         break;
	      }
	  }
}
int main()
{
	pthread_t tid[5];
	for(int i=0;i<5;i++)
	{
		pthread_create(&tid[i],NULL,getTicket,(void*)i);
	}
	for(int i=0;i<5;i++)
	{
		pthread_join(tid[i],NULL);
	}
	return 0;
}

 实际上,本应该在减到1的时候停止,然而并没有,

这是因为在--操作时,线程可能切换到其他线程,其他线程--过后,再过来执行,--操作以后实际值却变化了一次,--操作本来也不是一个原子操作。

为了解决这种问题,需要做到三点:

1,多线程操作临界区资源时,一次只允许一个线程进入,其余线程等待

2,代码必须具有互斥行为,

3,如果线程没在临界区执行,那么该线程不能阻止其他其他线程进入临界区。

此时,互斥锁登场,解决了这种问题

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>

pthread_mutex_t lock;//申请锁,全局的 
int tickets=10000;
void *getTicket(void* arg)
{
	int number=(int)arg;
	  while(1)
	  {
	      pthread_mutex_lock(&lock);//加锁 
	      if(tickets>0)
	      {
	         usleep(100);
	         printf("thread[ %d ]抢票:  %d\n",number,tickets--);
	         pthread_mutex_unlock(&lock);//解锁 
	      }
	      else
	      {
	         pthread_mutex_unlock(&lock);
	         break;
	      }
	  }
}
int main()
{
	pthread_t tid[5];
	pthread_mutex_init(&lock,NULL); //锁初始化 
	for(int i=0;i<5;i++)
	{
		pthread_create(&tid[i],NULL,getTicket,(void*)i);
	}
	for(int i=0;i<5;i++)
	{
		pthread_join(tid[i],NULL);
	}
	pthread_mutex_destroy(&lock);//销毁锁 
	return 0;
} 

对临界区资源进行加锁,一次只允许一个线程访问。

此时又提出一个问题:

1,当线程1加锁以后,是否有可能当前线程1被切走,后序线程进来的时候还能申请到锁吗?

        完全有可能被切走,但走的时候线程1是带着锁走的,其他线程过来加锁的时候只能等待线程1释放掉锁才能申请到锁。

2,谁来保护锁呢,锁是否需要保护吗?

        锁本身也是临界资源,也需要保护,所以申请锁的过程必须是原子的,那么锁是如何实现原子性的呢?

        我们通过伪代码来看一下

       

 每个线程都可以交换,但必须有顺序,一个加锁以后,其他线程交换只能挂起等待

CPU中的寄存器不是被所有的线程共享,但内存中的数据是共享的。

三,进程等待队列的理解

四,可重入VS线程安全 

 概念:

        线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

        重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

函数可重入与线程安全对比

1,函数可重入,那就是线程安全的

2,函数不可重入,那就不能被多个线程使用,有可能引发线程安全的问题

3,如果一个函数有全局变量,那么这个线程既不可重入,也不是线程安全的

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

五,常见锁

1,死锁

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

死锁的必要条件

互斥条件:一个资源一次只能被一个执行流使用

请求与保持:一个执行流因为请求资源被阻塞时,对获得的的资源不释放

不剥夺条件:一个执行流在未执行完之前,不能强行剥夺

循环等待:若干执行流形成头尾相接的循环等待资源的关系

破坏死锁的条件

就是破坏四个必要条件,实际上是破坏两个

破坏请求与保持:释放获得的资源,等不阻塞时再把资源申请回来(线程等待)

剥夺执行流的资源。

2,线程同步

为什么需要线程同步,为了避免单一线程竞争力很强,不停的加锁,解锁,线程同步就是通过某种特定顺序,使得每个线程都能都能访问CPU资源,避免饥饿问题。

此时通过条件变量即可实现同步

通过以下代码我们看看;

#include<iostream>
#include<unistd.h>
#include<cstdlib> 
#include<pthread.h>
using namespace std;

pthread_mutex_t lock;
pthread_cond_t control; 
void *CTRL(void*arg)
{
	pthread_mutex_lock(&lock);
	while(1)
	{
		cout<<" i want control other thread"<<endl;
		for(int i=0;i<10;i++)
		{
			sleep(1); 
		}
		pthread_cond_signal(&control);//发出信号,让等待的线程进入运行
		pthread_mutex_unlock(&lock);
	}
}
void *RUN(void *arg)
{
	pthread_mutex_lock(&lock);
	while(1)
	{
		pthread_cond_wait(&control,&lock);//等待信号发出,运行当前线程
		cout<<" i want do something"<<endl;
		for(int i=0;i<10;i++)
		{
			sleep(1); 
		}
		break;
	}
	pthread_mutex_unlock(&lock);
}
int main()
{
	pthread_t ctrl,run;
	pthread_mutex_init(&lock,nullptr);
	pthread_cond_init(&control,nullptr);
	
	pthread_create(&ctrl,nullptr,CTRL,nullptr);
	pthread_create(&run,nullptr,RUN,nullptr);
	
	pthread_join(ctrl,nullptr);
	pthread_join(run,nullptr); 
	
	return 0;
} 

通过一个线程来控制另一个线程。通过一个线程信号发出信号,另一个线程等待信号成功,执行他的代码

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

想找后端开发的小杜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值