Bootstrap

【Linux】线程与同步互斥相关知识详细梳理

目录

1. 线程概念

2. 线程优势

3. 线程劣势

4. 线程控制

4.1 POSIX线程库

4.2 线程操作

        

5. 线程互斥 

        5.1 互斥相关概念

        5.2 互斥量mutex

5.3 互斥量实现原理 

6. 线程同步

        6.1 同步概念与竞态条件        

        6.2 条件变量

6.3 条件变量使用规范及细节


1. 线程概念

        什么是线程:

        在⼀个程序里的⼀个执行路线就叫做线程(thread)。

        更准确的定义是:线程是“⼀个进程内部的控制序列”。⼀切进程至少都有⼀个执行线程。

        线程在进程内部运行,本质是在进程地址空间内运行。
        在Linux系统中,在CPU眼中,管理线程的方式也比传统的进程更加轻量化。(同样使用PCB管理,但更加轻量化)
        透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
        总的来说,进程是线程的集合,一个进程可以含有多个线程,线程是粒度最小的执行流。

2. 线程优势

        1.创建⼀个新线程的代价要比创建⼀个新进程小得多。


        2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多:
               最主要的区别是线程切换时,虚拟内存空间是不变的,但是进程切换时要加载新的内存空间。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种将寄存器中的内容切换出的过程伴随着显著的性能损耗。
               另外⼀个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址⼀瞬间都作废了。还有⼀个显著的区别是当你改变虚
拟内存空间的时候,处理的页表缓冲 TLB (快表)会被全部刷新,这将导致内存的访问在⼀段时间内相当的低效。但是在线程的切换中,不会出现这个问题。


        3.线程占用的资源要比进程少很能充分利用多处理器的可并行数量。


        4.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务


        5.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。


        6.I/O密集型应用,为了提搞性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

3. 线程劣势

        1.性能损失
        ⼀个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同⼀个处理器(无阻塞的计算密集型线程会频繁申请调度,占用大量执行时间)。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

        2.健壮性降低
        编写多线程需要更全面更深入的考虑,在⼀个多线程程序里,因时间分配上的细微偏差或者
因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的
        3.缺乏访问控制
        进程是访问控制的基本粒度,在⼀个线程中调用某些OS函数会对整个进程造成影响。
        4.编程难度提高

        编写与调试⼀个多线程程序比单线程程序困难得多

4. 线程异常

        单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。

        线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
 

4. 线程控制

        说完了概念,下面来实操一下把。

4.1 POSIX线程库

        与线程有关的函数构成了⼀个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。
        要使用这些函数库,要通过引入头文件 <pthread.h>。
        链接这些线程函数库时要使用编译器命令的“-lpthread”选项。

4.2 线程操作

        

pthread_create

  • 用于创建一个新线程。
  • 原型
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
    

  • 参数
    • thread:指向线程标识符的指针,线程创建成功后会保存线程 ID。
    • attr:线程属性(通常设置为 NULL,表示默认属性)。
    • start_routine:线程执行的函数(入口函数)。
    • arg:传递给 start_routine 的参数。
  • 返回值:成功返回 0,失败返回错误代码。

(pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错
误代码通过返回值返回,pthreads同样也提供了线程内的errno变量,以⽀持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小) 

        示例:

#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(1);
    }
}
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);
    }
}

pthread_exit

  • 使当前线程终止执行,并返回一个值给其他线程。
  • 原型
void pthread_exit(void *retval);
  • 参数retval:线程的退出状态。 

pthread_join

  • 等待指定线程终止,并回收线程资源。
  • 原型
    int pthread_join(pthread_t thread, void **retval);
    
  • 参数
    • thread:要等待的线程 ID。
    • retval:指向线程退出状态的指针。

        (若不想得到退出状态,传入NULL/nullptr即可)

        示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread1(void *arg)
{
    printf("thread 1 returning ... \n");
    int *p = (int *)malloc(sizeof(int));
    *p = 1;
    return (void *)p;
}
void *thread2(void *arg)
{
    printf("thread 2 exiting ...\n");
    int *p = (int *)malloc(sizeof(int));
    *p = 2;
    pthread_exit((void *)p);
}
void *thread3(void *arg)
{
    while (1)
    { //
        printf("thread 3 is running ...\n");
        sleep(1);
    }
    return NULL;
}
int main(void)
{
    pthread_t tid;
    void *ret;
    // thread 1 return
    pthread_create(&tid, NULL, thread1, NULL);
    pthread_join(tid, &ret);
    printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);
    free(ret);
    // thread 2 exit
    pthread_create(&tid, NULL, thread2, NULL);
    pthread_join(tid, &ret);
    printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);
    free(ret);
    // thread 3 cancel by other
    pthread_create(&tid, NULL, thread3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &ret);
    if (ret == PTHREAD_CANCELED)
        printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n",
               tid);
    else
        printf("thread return, thread id %X, return code:NULL\n", tid);
}

运行结果: 

pthread_detach

  • 将线程与主线程分离,使主线程不再等待该线程终止,可以自动清理线程资源。
  • 原型
    int pthread_detach(pthread_t thread);
    
  • 参数
    • thread:要分离的线程 ID。

        默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
        如果不关心线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

        分离的线程无法join:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run(void *arg)
{
    pthread_detach(pthread_self());//自己分离自己
    printf("%s\n", (char *)arg);
    return NULL;
}
int main(void)
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0)
    {
        printf("create thread error\n");
        return 1;
    }
    int ret = 0;
    sleep(1); // 等一下,不然线程可能来不及分离
    if (pthread_join(tid, NULL) == 0)
    {
        printf("pthread wait success\n");
        ret = 0;
    }
    else
    {
        printf("pthread wait failed\n");
        ret = 1;
    }
    return ret;
}

5. 线程互斥 

        5.1 互斥相关概念

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

        5.2 互斥量mutex

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

        示例: 

// 操作共享变量会有问题的售票系统代码
#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)
    {
        //临界区 ticket是临界资源
        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, (void*)"thread 1");
    pthread_create(&t2, NULL, route, (void*)"thread 2");
    pthread_create(&t3, NULL, route, (void*)"thread 3");
    pthread_create(&t4, NULL, route, (void*)"thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}

 

可以看到,正常情况下票数为零时,就会停止减少,但是这时却减到了负数。这是因为线程对于共享资源的操作顺序是不确定的,可能在检查(ticket>0)刚刚进入自减逻辑时,ticket就被别的线程更改,此时就在ticket不满足if条件的情况下继续执行了自减操作,导致了ticket变为负数。

        要解决这种问题,就需要互斥量mutex,也就是锁。

pthread_mutex_init

  • 初始化互斥锁(mutex)。
  • 原型
    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
    
  • 参数
    • mutex:指向互斥锁的指针。
    • attr:互斥锁属性(通常设置为 NULL)。

        也可以直接定义全局锁,静态分配。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

pthread_mutex_lock

  • 锁定互斥锁,确保线程在访问共享资源时不会发生竞争。
  • 原型
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    

pthread_mutex_unlock

  • 解锁互斥锁,允许其他线程访问被锁定的资源。
  • 原型:
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    

pthread_mutex_destroy 

  • 销毁互斥锁,释放资源。
  • 原型
int pthread_mutex_destroy(pthread_mutex_t *mutex);

       使用示例:

#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); //解锁
        }
        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, (void*)"thread 1");
    pthread_create(&t2, NULL, route, (void*)"thread 2");
    pthread_create(&t3, NULL, route, (void*)"thread 3");
    pthread_create(&t4, NULL, route, (void*)"thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    pthread_mutex_destroy(&mutex); //释放锁
}

        顾名思义,给临界区加锁可以保证一次只有一个线程拿到锁,临界区里最多只会有一个执行流,这样临界资源就不会被错误操作,同时,在执行流出临界区后,一定要释放锁,不然其他线程就会一直等待,出现死锁问题。 (若持有锁的线程申请自己的锁,也会死锁)

        使用互斥量锁时一定要注意图示规范:

        

5.3 互斥量实现原理 

         下面是互斥量的逻辑:

         一次只有一个执行流可以拿到mutex的“1”,因此达到加锁的效果。其中movb已经xchgb都是原子性的操作。

6. 线程同步

        6.1 同步概念与竞态条件
        

        同步:在保证数据安全的前提下,让线程能够

int pthread_cond_signal(pthread_cond_t *cond);

按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
        竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。

        6.2 条件变量

pthread_cond_init

  • 初始化条件变量。
  • 原型
    int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
    

    或定义全局的条件变量,静态分配。

    pthread_cond_t PTHREAD_COND_INITIALIZER

pthread_cond_wait

  • 使线程等待条件变量,直到条件成立。
  • 原型
    int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
    

pthread_cond_signal

  • 唤醒等待条件变量的一个线程。
  • 原型
int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_broadcast 

  • 唤醒所有等待条件变量的线程。
  • 原型
    int pthread_cond_broadcast(pthread_cond_t *cond);
    

    示例:

    #include <iostream>
    #include <string.h>
    #include <unistd.h>
    #include <pthread.h>
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    void *active(void *arg)
    {
        std::string name = static_cast<const char *>(arg);
        while (true)
        {
            pthread_mutex_lock(&mutex);
            pthread_cond_wait(&cond, &mutex);
            std::cout << name << " 活动..." << std::endl;
            pthread_mutex_unlock(&mutex);
        }
    }
    int main(void)
    {
        pthread_t t1, t2;
        pthread_create(&t1, NULL, active, (void *)"thread-1");
        pthread_create(&t2, NULL, active, (void *)"thread-2");
        sleep(3); // 可有可⽆,这⾥确保两个线程已经在运⾏
        while (true)
        {
            // 对⽐测试
            // pthread_cond_signal(&cond); // 唤醒⼀个线程
            pthread_cond_broadcast(&cond); // 唤醒所有线程
            sleep(1);
        }
        pthread_join(t1, NULL);
        pthread_join(t2, NULL);
    }

        利用条件变量的特性就可以基于阻塞队列实现一个简易的生产消费者模型,后续我会再出一个文章来叙述详细实现。

6.3 条件变量使用规范及细节

        聪明的你一定已经发现,phtread_cond_wait()函数中的一个参数为mutex,这是因为线程在条件上进行等待时,要先释放相应的锁资源,不然先lock了再wait,那不就死锁了吗,同时要注意,线程在被唤醒时,会再次申请锁资源,申请成功后才会继续运行。

        条件等待示例:

pthread_mutex_lock(&mutex);
while (条件为假)
    pthread_cond_wait(cond, mutex);
修改条件
    pthread_mutex_unlock(&mutex);

        说明:条件判断一定要为while循环判断,不然可能线程被唤醒前满足条件,而唤醒时不满足条件,而导致线程错误的执行逻辑,这中情况也叫做“伪唤醒”,while循环则可保证线程出循环时一定满足正确条件,从而防止“伪唤醒”

        给条件发送信号代码:

pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

 


 

 

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;