技术学习

Linux之路——多线程

@toc

Linux中的线程与多线程

在一个程序里的一个执行路线就叫做线程(thread).更准确的定义是:线程是“一个进程内部的控制序列”. 一切进程至少都有一个执行线程, 线程在进程内部运行, 本质是在进程地址空间内运行. 在Linux系统中, 在CPU眼中, 看到的PCB都要比传统的进程更加轻量化. 透过进程虚拟地址空间, 可以看到进程的大部分资源, 将进程资源合理分配给每个执行流, 就形成了线程执行流.
多线程就是在一个进程中含有多个线程, 分别执行不同的动作. 比如在写博客时用网页播放器播放《念诗之王》. 合理运用多线程可以让我们的用户体验得到提升. 也可以提高CPU的计算效率

线程的特点

优点:
1. 创建一个新线程所需的资源要比创建一个新进程小得多, 与进程之间的切换相比, 线程之间的切换需要操作系统做的工作要少很多.
2. 线程占用的资源要比进程少很多, 能充分利用多处理器的可并行数量, 在等待慢速I/O操作结束的同时, 程序可执行其他的计算任务.
3. 计算密集型应用, 为了能在多处理器系统上运行, 将计算分解到多个线程中实现.
4. I/O密集型应用, 为了提高性能, 将I/O操作重叠. 线程可以同时等待不同的I/O操作.
缺点:
5. 多个计算密集型会占用较多CPU资源, 超过核数导致性能损失, 增加CPU的开销.
6. 线程间没有保护, 可能会共享了不应该共享的变量. 使程序的安全性降低.
7. 线程中调用某些系统函数, 会对整个进程生效, 线程中出现异常, 可能会影响整个进程.

线程的共享资源

线程共用进程的地址空间, 其中的数据段, 代码段都是共享的, 如果定义了一个函数, 在各线程中都可以调用, 如果电脑椅了全局变量, 那么所有线程都可以访问.
同时文件描述符表, 每种信号的处理方式, 当前的工作目录, 用户ID和组ID都是共享的.

线程控制

线程的控制函数大多数以pthread开头, 包含在<pthread.h>头文件中.

创建线程

#include<pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg)
//编译时需要加上-lpthread选项

参数:

  • thread:返回线程ID
  • attr:设置线程的属性,attr为NULL表示使用默认属性
  • start_routine:是个函数地址,线程启动后要执行的函数
  • arg:传给线程启动函数的参数
  • 返回值:成功返回0;失败返回错误码

创建线程的代码:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *rout(void *arg) {
    int i;
    for( ; ; ) {
        printf("I'am thread 1\n");
        sleep(2);
    }
}
int main( void ){
    pthread_t tid;
    int ret;
    if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {
        fprintf(stderr, "pthread_create : %s\n", strerror(ret));
        exit(EXIT_FAILURE);
    }
    int i;
    for(; ; ) {
        printf("I'am main thread\n");
        sleep(1);
    }
    return 0;
}
//执行结果为:每打印两个main thread 就打印一个thread1
//pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)
//而是将错误代码通过返回值返回

获取线程ID

pthread_t pthread_self(void);

pthread_t在Linux下的本质就是进程地址空间上的一个地址. 指向共享区中的动态库中的一个地址.

终止线程

终止线程有三种方式:

  1. 从线程函数中return返回, 但是对于主进程来说, main返回就意味着结束进程.
  2. 线程调用pthread_exit(); 终止自己.
  3. 线程调用pthread_cancel()终止同一进程的一个线程.

线程等待

线程函数执行完毕后, 其空间并没有完全释放, 而是依旧留在地址空间中. 调用线程等待可以让线程挂起直到线程退出

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

调用pthread_join函数的线程将挂起等待, 直到id为thread的线程终止.
– thread:线程ID
– value_ptr:它指向一个指针,后者指向线程的返回值(不是本函数的返回值), 对于不同的进程结束方式, 得到的终止状态不同.
– 返回值: 成功返回0, 失败返回错误码.

分离线程

新创建的线程都是jionable的线程退出后需要对其进行等待, 否则无法释放资源, 造成系统泄漏.
有时候我们并不关心线程的返回值, 所以我们可以让系统在线程结束后, 自动释放资源.

int pthread_detach(pthread_t thread);//对同进程的线程进行分离
pthread_detach(pthread_self());//对自己进行分离

线程互斥

  • 临界资源: 多线程执行流共享的资源就叫做临界资源
  • 临界区: 每个线程内部, 访问临界资源的代码,就叫做临界区
  • 互斥: 任何时刻, 互斥保证有且只有一个执行流进入临界区, 访问临界资源, 通常对临界资源起保护作用
  • 原子性: 不会被任何调度机制打断的操作, 该操作只有两态, 要么完成, 要么未完成

互斥量

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程 之间的交互。但多个线程并发的操作共享变量,会带来一些问题。
比如:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg){
    char *id = (char*)arg;
    while ( 1 ) {
        if ( ticket > 0 ) {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        } else {
            break;
        }
    }
}
int main( void ){
    pthread_t t1, t2, t3, t4;

    pthread_create(&t1, NULL, route, "thread 1");
    pthread_create(&t2, NULL, route, "thread 2");
    pthread_create(&t3, NULL, route, "thread 3");
    pthread_create(&t4, NULL, route, "thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}
一次执行结果:
thread 4 sells ticket:100
...
thread 4 sells ticket:1
thread 2 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2

要解决这个问题需要满足三个条件:

  • 代码必须要有互斥行为: 当代码进入临界区执行时, 不允许其他线程进入该临界区.
  • 如果多个线程同时要求执行临界区的代码, 并且临界区没有线程在执行, 那么只能允许一个线程进入该临界区.
  • 如果线程不在临界区中执行, 那么该线程不能阻止其他线程进入临界区.

初始化互斥量

初始化互斥量有两种方法:

  1. 方法1, 静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  1. 方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t
*restrict attr);

参数:
– mutex:要初始化的互斥量
– attr:NULL

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex;
void *route(void *arg){
    char *id = (char*)arg;
    while ( 1 ) {
        pthread_mutex_lock(&mutex);
        if ( ticket > 0 ) {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
            // sched_yield(); 放弃CPU
        } 
        else {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
}
int main( void )
{
    pthread_t t1, t2, t3, t4;
    pthread_mutex_init(&mutex, NULL);

    pthread_create(&t1, NULL, route, "thread 1");
    pthread_create(&t2, NULL, route, "thread 2");
    pthread_create(&t3, NULL, route, "thread 3");
    pthread_create(&t4, NULL, route, "thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    pthread_mutex_destroy(&mutex);
}

死锁

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

死锁的四个条件

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

如何避免死锁

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

留言

您的电子邮箱地址不会被公开。 必填项已用*标注