Linux多线程第一讲——线程的创建和基本使用

发布于:2023-01-13 ⋅ 阅读:(555) ⋅ 点赞:(0)

从这一期开始,我会分模块的讲述多线程的知识内容,前期有我对于进程相关文章,对于操作系统的学习中,进程和线程相关知识在工作,面试中都是极为重要的内容,需要掌握。

往前内容:

fork函数详解_草东i的博客-CSDN博客_fork()函数https://blog.csdn.net/weixin_51609435/article/details/124849719

进程之间的通信(管道详解)_草东i的博客-CSDN博客https://blog.csdn.net/weixin_51609435/article/details/125946599

进程间的通信终章之【消息队列,信号量,共享内存】_草东i的博客-CSDN博客https://blog.csdn.net/weixin_51609435/article/details/126164984话不多说,我们开始多线程知识的讲解!

什么是线程?

线程,又被称为轻量级进程,是操作系统CPU调度执行的最小单位线程就是进程内部的一条执行路径,或者一个执行序列;一个进程必须至少包含一个线程也可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

一个正在运行的软件就是一个进程,一个进程可以同时运行多个任务( 每个运行任务就是一个线程), 可以简单的认为进程是线程的集合。

一个进程可以包含多个线程,main函数执行的线程称为主线程,主线程是进程执行的入口。在创建线程时,需要指定线程的执行序列,即函数,故称为函数线程
主线程创建第一个线程,这个线程也可以创建其他线程,这几个线程会并发执行。

我们在实现函数调用时,只需要传入函数名即可。而函数线程,我们需要给定一个函数地址,告诉创建的线程从哪个函数开始执行,如下:

void* fun(void* arg)
int main()
{
    pthread_create(fun);//此处的fun是函数地址,表示创建出来的线程执行这个函数。
}

多线程与单线程

单线程:只有一条线程在执行任务,这种情况在我们日常的生活学习中很少遇到
多线程:创建多条线程同时执行任务。由于各个线程的控制流彼此独立,使得各个线程之间的代码是乱序执行的。由此要引发线程调度和同步问题,在后续的博文中我们会陆续讲解。
注意:多线程并不是在任何情况下都能提升效率,比如说对于单核CPU计算密集型任务。因为线程本身创建和切换的开销;但是对于频繁IO操作的序列,多线程可以有效的并发,比如生产者、消费者问题。
当增加一个线程的时候,增加的额外开销要小于该线程能够消除的阻塞时间,使用多线程才是有必要的。

进程与线程

进程是一个动态的概念,就是一个程序正在执行的过程

二者的区别:

根本区别:进程是操作系统分配资源的最小单位,线程是任务调动和执行的最小单位。

在开销方面:每个进程都有独立的代码和数据空间,程序切换会有较大的开销。线程之间共享代码和数据,每个线程都有自己独立的栈和调度器,线程之间的切换的开销较小。

所处环境:一个操作系统中可以运行多个进程,一个进程中有多个线程同时执行。

内存分配方面:系统在运行时会为每个进程分配内存,系统不会单独为每个线程分配内存。

包含关系:创建进程时系统会自动创建一个主线程由主线程完成,进程中有多线程时,由多线程共同执行完成。

线程的实现

在操作系统中,线程的实现有以下三种方式:

用户态线程实现:

需要用户自己写一个执行系统作为调度,即线程的实现在用户态完成的,由线程库进行线程的创建,销毁等操作在用户态线程,每一个线程都是同一权限下的,不会出现夺走CPU控制权的现象,就会出现一个线程占用CPU时间过长,故线程之间必须进行合作,对CPU控制权进行分配

内核感知不到线程,只知道它是一个进程,是一种1-n的关系,这也表示该系统不支持线程,只知道有进程存在。

这种用户态线程的优点和缺点:
优点:

  1. 灵活性,内核不用知道线程的存在,所以在任何操作系统上都能应用。
  2. 线程切换快,因为切换在用户态进行,无需陷入内核态。
  3. 不用修改操作系统,实现简单。

缺点:

  1. 编程程序变得诡异,因为用户态线程需要合作才能运转,所以我们需要考虑什么时候让出CPU给别的线程使用,这个时机的选择就很难了。
  2. 如果一个线程阻塞,操作系统只会看到进程阻塞,故把CPU控制权交给另一个进程,这样则会造成整个进程阻塞。
  3. 用户程序相对复杂。

内核态线程实现:

操作系统管理线程,实现内核态线程,要保持维护线程的各种资料,即将线程控制块存放在操作系统内核空间,这样操作系统内核就同时有进程控制块,和线程控制块

每个用户线程就是一个内核线程,线程的是实现在内核态,内核知道用户态有几条线程,用户态线程和内核态线程数目对应,一种n-n关系。表示线程是由内核支持的。操作系统可以对线程进行各种类似进程的管理,如线程调度,线程的资源分配,出现阻塞进行处理等措施。

内核态线程的优点和缺点:
优点:

  1. 用户编程保持简单,因为线程的复杂性由操作系统承担,用户程序员在编程时无需管理线程的调度即无需担心执行,挂起等操作。
  2. 如果一个线程执行阻塞操作,操作系统可以从容的调度另一个线程执行,因为操作能够监控所有的线程。
  3. 线程之间无需合作得到CPU控制权,因为操作系统通过周期性的时钟中断把控制权夺过来,重新分配。

缺点:

  1. 会占用内核稀缺的内存资源,一旦内核空间溢出,操作系统将停止运转。
  2. 切换效率低,每次线程切换都需要陷入内核态。
  3. 需要修改操作系统,加入线程管理。
     

组合级线程实现:

用户态和内核态都存在缺陷,现代操作系统使用的是将两者结合起来。用户态的执行系统负责进程内部线程在非阻塞是的切换;内核态的操作系统负责阻塞线程的切换,将这种方式称作组合级线程。

其中内核态线程数量较少,而用户态线程数量多,每个内核态线程可以服务一个或多个用户态线程,换句话说,用户态线程被多路复用到内核态线程上

共有3条线程,将其分为两组,一组2个,一组1个,每一组线程使用一个内核线程 ,这样,该进程使用两个内核线程,形成n-m的关系如果一个线程阻塞,则与其一组的线程皆阻塞,但另外一组可以继续运行。

因此在我们分配线程时:

  • 将需要执行阻塞操作的线程设为内核态线程。
  • 不会阻塞操作的线程设为用户态线程。

这样我们便可以获得用户态,内核态线程的优点,避免缺点。

注意:在不同的平台中,线程的实现是不一样的;

Linux中线程的实现:

Linux 实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念。

Linux 把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特

别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进

程。每个线程都拥有唯一隶属于自己的 task_struct,所以在内核中,它看起来就像

是一个普通的进程(只是线程和其他一些进程共享某些资源,如地址空间)

Linux系统中线程的接口

1)pthread_create创建线程

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

pthread_create()用于创建线程 

thread: 接收创建的线程的 ID 

attr: 指定线程的属性,一般设置线程属性为NULL; 

start_routine: 指定线程函数,这个线程函数的参数为void *,返回值也为void *; 这是一个函数指针; 

arg: 给线程函数传递的参数(线程刚启动,线程函数的参数为void*,给它传参就是 void*) 

//成功返回 0, 失败返回错误码 

2)pthread_join

int pthread_join(pthread_t thread, void **retval); 

pthread_join()等待 thread 指定的线程退出,线程未退出时,该方法阻塞//有点像 

父进程等待子进程结束的wait,或者说合并线程; 

retval:接收 thread 线程退出时,指定的退出信息 

 3)pthread_exit

int pthread_exit(void *retval); 

pthread_exit()退出线程 

retval:指定退出信息 

4)pthread_detach()线程分离

在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用 pthread_join() 只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。

在线程库函数中为我们提供了线程分离函数 pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用 pthread_join() 就回收不到子线程资源了。

#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);

线程的创建使用

示例1:

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

void *thread_fun(void *arg)
{
    printf("hello \n");
}
int main()
{
    pthread_t id;
    pthread_create(&id,NULL,thread_fun,NULL);
    printf("hello main\n");
    return 0;
}

运行要加-lpthread ,绑定线程库(是一个共享库)

ldd main 查看main的共享库

修改上述代码;示例2:

创建线程:主线程输出10次提示信息,创建的线程输出10次提示信息。

我们只需要在主线程中调用pthread_create函数创建线程,实现函数线程fun,主线程打印信息即可。fun函数线程打印信息即可。那么代码如下:

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


void *thread_fun(void *arg)
{
    for(int i=0;i<10;++i)
    {
    printf("hello \n");
    sleep(1);//改为sleep(5),主函数运行完后直接退出,fun中的hello运行不完10次
    }
}
int main()
{
    pthread_t id;
    pthread_create(&id,NULL,thread_fun,NULL);//创建一个新线程  并发运行着
    int i=0;
    for(;i<10;++i)
    {
        printf("main run\n");
        sleep(1);
    }
    return 0;   //主函数运行结束后,进程直接退出,线程没有运行完也直接退出
}

sleep()会阻塞当前线程),让出CUP的使用目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会。所以不会占用cpu。所以sleep的使用是很关键的。

将上面fun函数改为休眠5秒,主函数结束打印。fun函数没有打印完也会结束

如果想要合并或者等待一个线程结束,我们需要引入pthread_join函数

 #include <stdio.h> 

 #include <stdlib.h> 

 #include <unistd.h> 

 #include <string.h> 

 #include <assert.h>

 #include <pthread.h> 

void *fun(void *arg) 

{ 
 	int i=0; 

 for(;i<10;i++) 

{ 

 printf("fun run\n"); 

 sleep(1); 

} 
 // pthread_exit("fun over"); 
 pthread_exit(NULL); 
} 


int main() 

{ 

 pthread_t id; 

 pthread_create(&id,NULL,fun,NULL); 

 int i=0; 

 for(;i<5;i++) 

{ 

 printf("main run\n"); 

 sleep(1); 

} 

// char *s=NULL; 

//pthread_join(id,(void **)&s); 

//printf("join:s=%s\n",s); 

 pthread_join(id,NULL); 

 exit(0); 

} 

运行结果我们会发现主函数线程打印结束后,依旧会等待函数线程打印完毕,程序才会结束

总结:

到此Linux多线程第一期结束,主要理解什么是线程以及线程的基本使用创建,感谢观看!后续更新线程相关安全,同步,锁等问题。


网站公告

今日签到

点亮在社区的每一天
去签到