Linux----多线程(上)

Posted 4nc414g0n

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux----多线程(上)相关的知识,希望对你有一定的参考价值。

多线程(上)

1)引入

①线程概念

什么是线程:

  1. 在一个程序里的一个执行路线就叫做线程(thread)更准确的定义是:线程是“一个进程内部的控制序列”一切进程至少都有一个执行线程
  2. 线程在进程内部运行,本质是在进程地址空间内运行
  3. 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  4. 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

如果我们创建”进程”,不独立创建地址空间,户级页表,甚至不进行I/O将程序的数据和代码加载到内存,我们只创建task struct, 然后让新的PCB,指向和老的PC指向同样的mm_struct,然后,通过合理的资源分配(当前进程的资源) ,让每个task struct都能使用进程的一-部分资源, 此时 每个PCB被CPU调度的时候,执行的‘粒度“比原始进程执行的’粒度’会更小一些

②重新认识进程

站在OS的角度:进程是承担系统资源分配的基本单位 一个进程创建好后,内部可能存在多个执行流(线程)
以往我们所认识的进程:承担系统资源的基本实体,但内部只有一个执行流

③Linux线程和其他平台的线程

站在CPU的角度进程:没有任何区别实际上,CPU执行的时候,进程([可能] 执行的“进程流”)已经比历史的进程更加轻量化


Linux下,其实是没有真正意义上面的线程概念的,而是用进程来模拟的轻量级进程,所以Linux不可能直接在OS层面提供线程的系统调用接口,顶多是轻量级进程调度接口
Windows下,系统存在大量的进程(一个进程对应多个线程),所以为了管理这些大量的线程,windows必须描述线程为TCB 并组织起来,这样往往会比较复杂(一定会有大量的线程相关操作的系统调用接口)

④线程异常

单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

⑤Linux进程和线程的区别

进程的多个线程共享 同一地址空间,因此代码段、数据段都是共享的,除此之外,各线程还共享以下进程资源和环境:

  1. 文件描述符表(注意:多进程是不同的文件描述符表,但内容可以一样
  2. 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  3. 当前工作目录
  4. 用户id和组id

线程自己会拥有的数据:

  1. 线程ID
  2. 一组寄存器
  3. errno
  4. 信号屏蔽字
  5. 调度优先级

⑥总结

进程本质是承担分配系统资源的基本实体
线程是0S调度的基本单位


线程优点:

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

线程缺点:

  1. 性能损失:
    一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变
  2. 健壮性降低:
    编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的
  3. 缺乏访问控制:
    进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
  4. 编程难度提高:
    编写与调试一个多线程程序比单线程程序困难得多

总结:因为所有的PCB都共享地址空间,理论上,每个”线程“都能看到进程的所有的资源,线程并不是越多越好
带来的优点:线程间通信,成本特别低,
带来的缺点:一 定存在大量的临界资源,势必可能需要使用各种互斥和同步机制


2)线程控制

①线程创建

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);


功能:创建一个新的线程
返回值:成功返回0;失败返回错误码


thread:输出型参数返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:回调函数,是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数(回调函数)的参数


编译时要加上-lpthrea选项,$(CXX) -o $@ $^ $(LDFLAGS) -lpthread
创建一个线程:

void *thread_run(void *args)

       while(1)
               cout<<(char*)args<<endl;
               sleep(1);
       

int main(int argc, char *argv[])

       pthread_t tid;
       pthread_create(&tid, nullptr, thread_run, (void*)"thread1");
       while(1)
               cout<<"main thread ..."<<endl;
               sleep(1);
       
       return 0;

ps axj是查看进程
在Linux中查看轻量级进程的命令:ps -aL

其中LWP表示:执行流是一个轻量级进程,标识其唯一性,CPU在调度的时候,以LWP为准,在进程只有一个线程的时候,LWP==PID

②线程ID及进程地址空间布局

pthread_self 线程ID

pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中,该线程ID和前面说的线程ID不一样:

  1. 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程
  2. pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴 线程库的后续操作,就是根据该线程ID来操作线程的

线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID


#include <pthread.h>
pthread_t pthread_self(void);


功能:获得线程自身的ID

pthread线程库和pthread_t

pthread库(-lpthread)

  1. Linux操作系统没有真正意义上面的线程,是用进程模拟的! —轻量级进程, Linux操作系统本身不会直接提供类似的线程创建,终止,等待,分离等相关system call 接口,但是会提供创建轻量级进程的接口----vfork
  2. 但是用户需要所谓的线程创建,终止,等待,分离等相关接口,所以,为了更好的适配,系统基于轻量级进程的接口,模拟封装了一个用户原生线程库,pthread.
  3. 线程id, 状态,优先级,其他属性用来进行用户级线程管理(TCB, 不用内核维护,而在用户空间维护

上面所谓的用户层线程地址(ID),指的是pthread库(映射到共享区)中的某一个起始位置(地址)
(pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址)
注意

  1. 主线程直接使用进程地址空间的栈,而子线程使用pthread库的线程栈
  2. 在Linux中用户级线程(tid)和内核级线程(LWP)是1:1对应的
  3. 定义的全局变所有的线程都可以访问
  4. 对全局变量做的修改比如: ++, --,由于是临界资源,有可能会有风险 所以应该保证操作是原子性的

③线程终止

pthread_exit

#include <pthread.h>
void pthread_exit(void *retval);


功能:终止线程
返回值:返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了


void *retval:同return返回值,不要指向一个局部变量


编译时要加上-lpthrea选项,$(CXX) -o $@ $^ $(LDFLAGS) -lpthread
终止线程:

void *ThreadRoutine(void *args)

       int i=*(int*)args;
       delete (int*)args;
       int cnt=5;
       while(cnt)
               cout<<"Thread_index i "<<i<<"cnt="<<cnt<<endl;
               sleep(1);
               cnt--;
       
       //return nullptr;
       pthread_exit((void*)10);

int main(int argc, char *argv[])

#define NUM 5
       pthread_t tids[NUM];
       for(auto i=0;i<NUM;i++)
               int *p=new int(i);
               pthread_create(tids+i, nullptr, ThreadRoutine, p);
       
       while(1)
       
               cout<<"main thread ..."<<endl;
               sleep(1);
       
       return 0;

return nullptr

ThreadRoutine使用return nullptr结束线程,效果一样

pthread_cancel

#include <pthread.h>
int pthread_cancel(pthread_t thread);


功能:取消一个执行中的线程
返回值:成功返回0;失败返回错误码)


threadl:线程id


代码如下:

void *ThreadRoutine(void *args)

       int i=*(int*)args;
       delete (int*)args;
       int cnt=5;
       while(true)
               cout<<"Thread_index i "<<i<<"cnt="<<cnt<<endl;
               sleep(1);
               
               cnt--;
       
       //return nullptr;
       //pthread_exit((void*)10);

int main(int argc, char *argv[])

#define NUM 5
       pthread_t tids[NUM];
       for(auto i=0;i<NUM;i++)
               int *p=new int(i);
               pthread_create(tids+i, nullptr, ThreadRoutine, p);
       
       sleep(5);
       for(int i=0;i<NUM;i++)
       
               pthread_cancel(tids[i]);
               cout<<"Thread "<<tids[i]<<" has been canceled"<<endl;
               sleep(1);
       
       while(1)
       
               cout<<"main thread ..."<<endl;
               sleep(1);
       
       return 0;


注意不建议使用,如果在子线程中取消主线程会造成主线程defunc,僵尸状态

pthread_t main_thread;//main函数中用pthread_self()获取主线程线程id
void *ThreadRoutine(void *args)

       int i=*(int*)args;
       delete (int*)args;
       int cnt=5;
       while(true)
               cout<<"Thread_index i "<<i<<"cnt="<<cnt<<endl;
               sleep(1);
               pthread_cancel(main_thread);
               cnt--;
       

总结

注意:

  1. 不可以使用exit()结束子线程
  2. 线程一般终止之后, 必须进行等待 main thread,如果不等待,会造成和进程退出类似的效果(僵尸进程)

④线程等待

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);


功能:等待线程结束
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止
thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数

注意:

  1. main thread join的时候不需要考虑线程崩溃问题,因为出现错误会直接中断此进程

threadl:线程id
value_ptr:输出型参数,它指向一个指针,后者指向线程的返回值


代码验证如下:

pthread_t tids[NUM];
void *ThreadRoutine(void *args)

       int cnt =rand()%5+5;//随机时间的等待
       while(cnt)
               cout<<"thread: "<< pthread_self()<<"| cnt: "<<cnt<<"is running..."<<endl;
               cnt--;
               sleep(1);
       
       //pthread_cancel(pthread_self());
       //sleep(3);
       return (void*)11;

int main(int argc, char *argv[])

       srand((unsigned long)time(nullptr));
       for(auto i=0; i<NUM;i++)
               pthread_create(&tids[i], nullptr, ThreadRoutine, nullptr);
       
       //sleep(3);
       sleep(1);
       for(auto i=2;i<NUM;i++)
             pthread_cancel(tids[i]);
             cout<<"cancel "<<tids[i]<<" success"<<endl;
       
       cout<<"main thread join ..."<<endl;
       void *st=nullptr;
       for(auto i=0;i<NUM;i++)
               if(0==pthread_join(tids[i],&st))
                       cout<<"thread "<<tids[i]<<"| exit code: "<<(int*)st<<" quit join success..."<<endl;
               
       
       cout<<"main thread join over..."<<endl;
       return 0;


其中0xff为-1,即PTHREAD_ CANCELED


将线程取消(pthread_cancel)在ThreadRoutine中进行:,同时线程取消后立马退出

void *ThreadRoutine(void *args)

       int cnt =rand()%5+5;//随机时间的等待
       while(cnt)
               cout<<"thread: "<< pthread_self()<<"| cnt: "<<cnt<<"is running..."<<endl;
               cnt--;
               sleep(1);
       
       pthread_cancel(pthread_self());
       //sleep(3);
       return (void*)11;


观察到并未调用pthread_cancel函数
解释:

  1. cancel本身具有一定的延时性,可能并不是被立即受理,建议在线程执行中cancel最好(main->other thread)
  2. 上面的情况可能是新线程被创建了,但并未被调度,所以一定要让新线程先完全跑起来在进行pthread_cancel

在return前sleep(3)秒结果正常

       pthread_cancel(pthread_self());
       //sleep(3);
       return (void*)11;


⑤线程分离

更改ThreadRoutine函数,execl进程替换

void *ThreadRoutine(void *args)

       cout<<"using execl..."<<endl;
       execl("/bin/ls","ls","-a","-l",nullptr);
       cout<<"execl over"<<endl;
       return (void*)11;


可以发现并没有打印execl over,同时主线程的后续线程创建并没有继续执行,进程替换会替换掉整个进程,main函数也会


是否join

  1. 默认情况下,新创建的线程是joinable的, 线程退出后,需要对其进行pthread_ join操作, 否则无法释放资源,从而造成系统泄漏
  2. 如果不关心线程的返回值,join是一种负担, 这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源
  3. 分离的本质,是让主线程不用在join新线程,从而可以让新线程退出的时候,白动回收资源

pthread_detach

#include <pthread.h>
int pthread_detach(pthread_t thread);


功能:可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
注意:joinable和分离是冲突的,一个线程不能既是joinable又是分离的


threadl:线程id
测试代码:

void *ThreadRoutine(void *args)

        pthread_detach(pthread_self());
        return (void*)11;

int main()

 //...pthread_create
 	int ret=pthread_join(tids[i],&st);
    if(0==ret)
          cout<<"thread "<<tids[i]<<"| exit code: "<<(int*)st<<" quit join success..."<<endl;
    else
          cout<<"thread join wrong: "<<ret<<endl;
 

只要返回不为0就是join失败
注意

  1. 如果一个线程被设置为分离状态亥线程不应该被join,如果join, 结果是未定义,join出错
  2. 即便线程被设置为分离状态,但是如果该线程依旧出错崩溃,还是会影响主线程和其他正常线程.

以上是关于Linux----多线程(上)的主要内容,如果未能解决你的问题,请参考以下文章

多线程同步机制

多线程同步机制

java 多线程怎么深入?

Linux下各种锁的理解和使用及总结解决epoll惊群问题(面试常考)

Java 多线程分析----CAS操作和阻塞

Java 多线程进阶-并发协作控制