Linux编程–线程

1、什么是线程

  • LWP:light weight process轻量级进程,本质上仍是进程(Linux下)
  • 进程独享地址空间拥有PCB,线程拥有独立的PCB,但共享地址空间
  • 进程是最小的资源单位,线程是最小的执行单位
  • cpu根据线程来划分时间轮片,多线程能争取到更多的cpu资源

2、线程的实现原理

  • 创建线程的底层函数与进程一样,都是使用clone函数;
  • 三级映射(三级页表):进程—>页目录—>页表—>物理页面(内存单元)—>mmu—>虚拟地址空间
    • 不同进程有不同的页目录,从而有不同的物理页面,不同的地址空间;
    • 不同的线程有不同的PCB,但指向同一个页目录,指向相同的地址空间
  • 线程可以看作寄存器和栈的集合
    • 不同线程有不同的用户栈空间(保存局部变量和临时值[esp基址指针][ebp栈顶指针])
    • 不同的内核栈空间(寄存器的值,为进程切换提供现场保护)
    • 栈由一个个栈帧组成
  • 线程共享资源
    • 文件描述符表
    • 各种信号处理方式(尽量不要将线程与信号交织在一起)
    • 当前工作目录
    • 用户id与组id
    • 内存地址空间(除栈以外)
  • 线程非共享资源
    • 线程id
    • 处理器现场和栈指针(内核栈)
    • 独立的栈空间(用户空间栈)
    • errno变量(全局变量)
    • 信号屏蔽字
    • 调度优先级
  • 线程优、缺点
    • 优点:
    1. 提高程序并发性
    2. 开销小
    3. 数据通信、共享数据方便
    • 缺点:
    1. 库函数,不稳定
    2. 调试、编写困难、gdb不支持
    3. 对信号支持不好
    • 优点相对突出,缺点不是硬伤

3、查看线程

parallels@ubuntu:~$ ps -Lf 7775
UID        PID  PPID   LWP  C NLWP STIME TTY      STAT   TIME CMD
paralle+  7775     1  7775 16   59 09:55 tty2     Sl+    0:04 /usr/lib/firefox/firefox -new-window
paralle+  7775     1  7792  0   59 09:55 tty2     Sl+    0:00 /usr/lib/firefox/firefox -new-window
paralle+  7775     1  7793  0   59 09:55 tty2     Sl+    0:00 /usr/lib/firefox/firefox -new-window
paralle+  7775     1  7796  0   59 09:55 tty2     Sl+    0:00 /usr/lib/firefox/firefox -new-window
paralle+  7775     1  7798  0   59 09:55 tty2     Sl+    0:00 /usr/lib/firefox/firefox -new-window
paralle+  7775     1  7799  0   59 09:55 tty2     Sl+    0:00 /usr/lib/firefox/firefox -new-window
paralle+  7775     1  7800  0   59 09:55 tty2     Sl+    0:00 /usr/lib/firefox/firefox -new-window

4、线程控制原语

  • man page安装
    • 验证:man -k pthread
    • sudo apt-get undate
    • sudo apt-get install manpages-posix manpages-posix-dev
  • pthread_self函数
    • 获取线程id,其作用相当于进程中的getpid函数
    • pthread_t pthread_self(void)
    • 返回值:This function always succeeds, returning the calling thread’s ID.
  • pthread_create
    • 创建线程,其作用相当于进程中的fork函数
    • int pthread_create(pthread_t thread, const pthread_attr_t *attr, void *(start_routine) (void *), void *arg);
    • 返回值:On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.
  • 涉及到线程控制原语,在编译时需要加-pthread

4.1、创建线程的基本实现

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

// 线程主逻辑函数(创建线程需要执行的主体)
void* callback(void *arg){
    printf("callback pthread = %lu, pid = %u, ppid = %u\n", pthread_self(), getpid(), getppid());
    return NULL;
}
int main(){
    pthread_t thread;
    printf("main pthread = %lu, pid = %u, ppid = %u\n", pthread_self(), getpid(), getppid());
    sleep(10);
        // 第2个参数,创建线程的属性默认可以传null,第四个参数为线程主体函数参数
        int ret = pthread_create(&thread, NULL, callback, NULL);
    // 创建成功则返回0,失败则返回错误代码
    if (ret != 0){
        printf("pthread create error: %d\n", ret);
        exit(1);
    }else{
        printf("create pthread success: %lu\n", thread);
    }
    // 创建线程成功后,默认会立即执行线程主体函数,前提是进程还在,所以sleep等待线程执行
    sleep(10);
    return 0;
}

执行结果:

parallels@ubuntu:~/Linux/pthread$ ./pthread.out
main pthread = 139814966679360, pid = 4726, ppid = 1918
create pthread success: 139814958167808
callback pthread = 139814958167808, pid = 4726, ppid = 1918

4.2、循环创建多线程

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

void *thread_func(void *arg){
    int i = (int)(long)arg;
    printf("%dth thread is created, tid = %lu\n", i, pthread_self());
}

int main(){
    pthread_t tid;
    int i = 0;
    for (i = 0; i < 5; i++){
        // 循环创建线程,并将序号通过主逻辑函数的参数带过去
        tid = pthread_create(&tid, NULL, thread_func, (void *)(long)i);
        sleep(1);
        if (tid !=0){// 失败
            // fprintf可将信息从标准文件描述符中输出,strerror函数将错误代码转成相关字符串描述
            fprintf(stderr, "pthread_creat error: %s\n", strerror(tid));
        }
    }
    return 0;
}

执行结果:

parallels@ubuntu:~/Linux/pthread$ ./m_pthread.out
0th thread is created, tid = 140010892678912
1th thread is created, tid = 140010884286208
2th thread is created, tid = 140010875893504
3th thread is created, tid = 140010867500800
4th thread is created, tid = 140010859108096
parallels@ubuntu:~/Linux/pthread$

4.3、线程的退出pthread_exit(void *retval)

  • exit( )函数会将进程结束,慎重使用!!
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

void* callback(void *arg){
    sleep(1);
    printf("callback pthread = %lu, pid = %u, ppid = %u\n", pthread_self(), getpid(), getppid());
    return NULL;
}
int main(){
    pthread_t p;
    printf("main pthread = %lu, pid = %u, ppid = %u\n", pthread_self(), getpid(), getppid());
           int ret    = pthread_create(&p, NULL, callback, NULL);
    if (ret != 0){
        fprintf(stderr, "pthread create error: %s\n", strerror(ret));
        exit(1);
    }
    // 退出当前线程,如果没有退出当前线程,main函数结束后进程将结束,其他子线程将被强制结束
    pthread_exit((void*)(long)1);
}

4.4、线程回收pthread_join( )函数

  • int pthread_join(pthread_t thread, void **retval);
  • 线程的pthread_join函数相当于进程的wait及waitpid函数
  • pthread_join – join with a terminated thread
  • pthread_join事实上是加入线程,将某线程加入到当前线程(有回收的效果)
  • 第1个参数是指定线程id,第2个参数接收线程退出时传出的结果
  • waitpid事实上也是类似的效果,只是waitpid只能传出整型数据
  • 线程结束后会自动释放,进程不会。
4.4.1、线程回收示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

// 自定义结构体
struct exit_t{
    int a;
    int b;
};
// 线程回调函数
void *callback(void* arg){
    struct exit_t* retval = (struct exit_t*)arg;
    retval->a = 10;
    retval->b = 20;
    pthread_exit(retval);
}
int main(){
    pthread_t tid;
    // 定义一个结构体用于接收线程结束时数据的传出
    struct exit_t* retval = malloc(sizeof(struct exit_t));
    // 创建线程,并将定义好的结构体指针传入
    pthread_create(&tid, NULL, callback, retval);
    // 线程结束时将数据返回,join函数接收该数据
    pthread_join(tid, (void **)&retval);
    // 输出
    printf("pthread_joined: a = %d, b = %d\n", retval->a, retval->b);
    // 释放堆空间
    free(retval);
    return 0;
}

执行结果

pthread_joined: a = 10, b = 20
4.4.2、多线程回收示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

int var = 100;
void* callback(void* arg){
    int i = (int)(long)arg;
    switch(i){
        case 0:return (void *)(long)var;
        case 1:var = 200;break;
        case 2:break;
        case 3:var = 300;break;
        default: break;
    }
    return (void *)(long)var;
}

int main(){
    pthread_t tid[5];
    int retval[5];
    int i = 0;
    // 创建5个子线程
    for(i = 0; i < 5; i++){
        pthread_create(&tid[i], NULL, callback, (void *)(long)i);
    }
    // 回收5个子线程
    for(i = 0; i < 5; i++){
        // (void*)强转成int,那(void**)只需要传入一个int型地址即可
        // 容易误解为(void**)必须传入一个指针类型的地址
        pthread_join(tid[i], (void **)&retval[i]);
    }
    // 输出结果
    for(i = 0; i < 5; i++){
        printf("pthread %d return %d\n", i, retval[i]);
    }
    return 0;
}

执行结果

parallels@ubuntu:~/Linux/pthread$ ./m_pthread_join.out
pthread 0 return 100
pthread 1 return 200
pthread 2 return 200
pthread 3 return 300
pthread 4 return 300
parallels@ubuntu:~/Linux/pthread$

4.5、线程分离函数pthread_detach

  • 一般情况下,线程终止后,其终止状态会一直保留到其他线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止会被立即回收它占用的所有资源,而不保留终止状态。
  • 不能对一个detach状态的线程调用pthread_join函数
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void *callback(void *arg){

}
int main(){
    pthread_t tid;
    pthread_create(&tid, NULL, callback, NULL);
    //pthread_detach(tid);
    int ret = pthread_join(tid, NULL);
    printf("%d\n", ret);
    return 0;
}

4.6、线程退出函数pthread_cacel

  • pthread_cancel – send a cancellation request to a thread
  • int pthread_cancel(pthread_t thread);
  • 线程取消不是实时的,有一定的延时,需要等待线程达到某个取消点
  • 可粗略地认为一个系统调用即为一个取消点,如果线程没有取消点,可通过pthread_testcancel函数调用设置一个取消点
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

void *callback1(void *arg){
    int n = 3;
    while(n--){
        printf("pthread 1 runloop %d\n", 3-n);
        sleep(1);
    }
    return (void *)1;
}
void *callback2(void *arg){
    int n = 3;
    while(n--){
        printf("pthread 2 runloop %d\n", 3-n);
        sleep(1);
    }
    pthread_exit((void *)2);
    return NULL;
}
void *callback3(void *arg){
    int n = 3;
    while(n){
        //printf("pthread 3 runloop %d\n", 3-n);
        //sleep(1);
        // 为pthread_cancel函数服务,如果没有相关系统调用,pthread_cancel找不到调用时机
        // 人为增加取消点,退出线程
        pthread_testcancel();
    }
    return NULL;
}

int main(){
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, callback1, NULL);
    pthread_join(tid1, NULL);

    pthread_create(&tid2, NULL, callback2, NULL);
    pthread_detach(tid2);

    pthread_create(&tid3, NULL, callback3, NULL);
    pthread_cancel(tid3);
    pthread_join(tid3, NULL);

    sleep(3);
    printf("end------------------\n");
    return 0;
}

5、控制原语对比

进程 线程
创建 fork pthread_creat
回收 wait、waitpid pthread_join (pthread_detach不能join)
线束、取消 kill pthread_cancel(需要取消点,系统调用或pthread_testcancel函数设置)
获取id getpid pthread_self

6、线程属性

typedef struct __pthread_attr_s
{
    int __detachstate;  

    int __schedpolicy;

    struct __sched_param __schedparam;

    int __inheritsched;

    int __scope;

    size_t __guardsize;
    int __stackaddr_set;

    void *__stackaddr;
    size_t __stacksize;表示堆栈的大小。

}pthread_attr_t;

6.1、线程属性设置线程分离

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

void *callback(void *arg){
    return NULL;
}

int main(){
    pthread_t tid;
    pthread_attr_t attr;
    // 1、线程属性初始化
    int ret = (&attr);
    if (ret != 0){
        fprintf(stderr, "pthread_attr_init error:%s - %d\n", strerror(ret), ret);
    }
    // 2、线程属性设置分离
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    // 3、使用属性创建线程
    ret = pthread_create(&tid, &attr, callback, NULL);
    if (ret != 0){
        fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
        exit(1);
    }
    ret = pthread_join(tid, NULL);
    if (ret != 0){
        fprintf(stderr, "pthread_join error:%s-%d\n", strerror(ret), ret);
        exit(1);
    }else{
        printf("pthread_join success!!\n");
    }
    pthread_attr_destroy(&attr);
    return 0;
}

执行结果:

parallels@ubuntu:~/Linux/pthread$ ./pthread_attr.out
pthread_join error:Invalid argument-22
parallels@ubuntu:~/Linux/pthread$

PS:如果线程函数执行速度非常快,有可能在pthread_create函数返回之前就终止了,而终止后线程号和系统资源可能移交给其他线程使用,这样调用pthread_create得到的线程就是错误的线程号,要避免这种情况一方面可使用sleep或pthread_cond_timedwait函数来解决

6.2、线程栈及栈大小修改

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);

int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);

int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);

int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);

int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);

7、NPTL(Native POSIX Thread Library)

  • 是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同时也符合 POSIX 的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。与 LinuxThreads 一样,NPTL 也实现了一对一的模型。
  • 查看当前pthread版本,getconf GNU_LIBPTHREAD_VERSION

8、线程使用注意事项

  1. 主线程退出其他线程不退出,主线程应该调用pthread_exit
  2. 避免僵尸线程
    • pthread_join
    • pthread_detach
    • pthread_create指定他离属性
    • 被join线程可能在join函数返回前就释放相关内存空间,所以不应该返回线程栈中的值
  3. malloc和mmap申请的内存可以被其他线程释放
  4. 应该避免多线程模型中调用fork,除非exec。因为子进程中只有调用fork的线程存在,其他线程均pthread_exit
  5. 信号是复杂语义很难和多线程共存,应避免在多线程中引入信号机制

Leave a Reply