linux 应用开发笔记---【线程】

发布时间:2023年12月21日

1.概念:

线程是参与系统调度的最小单位,它被包含在进程中,是进程的实际运行单位

一个进程可以创建多个线程,多个线程并发运行,每个线程执行不同的任务

2.如何创建线程

当一个程序启动的时候,一个进程被os创建,同时一个线程也立刻开始运行,这个线程被叫做主线程,main()函数是主线程的入口函数

任何一个进程都包含了一个主线程,只有主线程的进程被称为单线程进程

多线程:除了主线程以外,还包含其他的线程,其他线程通常是由主线程创建来的

主线程的作用:

1.创建其他的子线程

2.执行清理工作

3.线程的特点:

线程是程序执行的最基本的单位,进程不能运行,真正运行的是进程中的线程,同一个进程的多个线程共享这个线程的全部系统资源,但同意进程的多个线程有各自的调用栈,自己的寄存器环境,自己的线程本地存储

1.线程不单独存在,被包含在进程里面

2.线程是参与系统调度的基本单位

3.可并发执行,同一个进程的多个线程可以同时执行【宏观

4.共享进程资源,所以线程拥有相同的地址空间【进程的地址空间】这意味着,线程可以访问该地址空间的每一个虚地址,此外,还可以访问进程所拥有的已打开文件、定时器、信号量等等

4.线程与进程

进程创建多个子进程可以实现并发多任务【本质上是多线程进程】,多线程同样也可以实现并发处理多任务的需求

多进程编程:

进程间切换开销大,多个进程同时运行,微观上是轮流切换运行,进程间切换开销远大于同一进程的多个线程切换的开销

进程间的通信比较麻烦,同一进程的多个线程通信非常方便

线程创建的速度远大于进程创建的速度

5.并发和并行

?

并发:交替做不同的事情【相比于串行,不再等待上一个任务完成之后再做下一个任务】

并行:同时做不同的事情

6.线程ID

线程和进程一样,都有属于自己的ID,

pthread_t pthread_self(void);


创建ID,返回当前线程的线程ID
int pthread_equal(pthread_t t1, pthread_t t2);



判断两个线程ID是否相等,相等返回非零, 不相等返回0

7.创建线程

在创建第一个进程的时候,也会自动生成一个线程,也就是主线程,主线程可以使用库函数 pthread_create()负责创建一个新的线程,创建出来的新线程被称为主线程的子线程

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
        printf("新线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
        return (void *)0;
}
int main(void)
{
        pthread_t tid;
        int ret;
        ret = pthread_create(&tid, NULL, new_thread_start, NULL);
        if (ret) {
                fprintf(stderr, "Error: %s\n", strerror(ret));
                exit(-1);
        }
                printf("主线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
                sleep(1);
                exit(0);
}

运行结果:

8.终止线程:

? 线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
? 线程调用 pthread_exit() 函数;
? 调用 pthread_cancel() 取消线程
注意:如果进程中的任意线程调用 exit()、_exit()或者_Exit(),那么将会导致整个进程终止
void pthread_exit(void *retval);



该返回值可以通过另一个线程调用pthread_join()来获取

如果在start函数中return也可以通过pthread_join()来获取






pthread_exit()和在线程start函数中和return的作用是一样的,但是区别在于可以在任意位置终止线程
进程中调用wait()函数阻塞等待子进程的结束,然后回收子进程
线程中调用pthread_join()函数来阻塞等待线程的终止,并且获取线程的退出码,回收线程资源
int pthread_join(pthread_t thread, void **retval);



pthread_t thread:指定等待的线程ID

retval:如果不为NULL,则返回的是start函数的return的返回值或者通过pthread_exit()的指定返回值,将返回值复制到retval所指向的内存空间




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

调用pthread_join()会等待指定的线程结束后回收线程,但多个线程同时尝试调用pthread_join()等待指定线程的终止,那结果将是不确定的

如果线程终止了,但是却没有pthread_join()函数去回收线程,则该线程变为了僵尸线程,会导致系统资源的浪费,如果积累太多,则程序无法创建新的线程

和wait()的进程相比的区别:
1.线程之间的关系是对等。每个进程的任意线程均可以调用pthread_join()来等待另一个线程的结束。和父子进程不同的是,父进程通过fork()创建了子进程,也只可以wait()这个子进程,不可以等待其他的。

2.不可以用非阻塞的方式调用pthread_join()

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
        printf("新线程 start\n");
        sleep(2);
        printf("新线程 end\n");
        pthread_exit((void *)10);
}
int main(void)
{
        pthread_t tid;
        void *tret;
        int ret;
        ret = pthread_create(&tid, NULL, new_thread_start, NULL);
        if (ret) {
                fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
                exit(-1);
        }
        ret = pthread_join(tid, &tret);
        if (ret) {
                fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
                exit(-1);
        }
        printf("新线程终止, code=%ld\n", (long)tret);
        exit(0);
}

运行结果:

9.取消线程

int pthread_cancel(pthread_t thread);
线程可以设置自己不被取消或者控制如何被取消,所以pthread_cancel()并不会等待线程终止,只是向线程发送一个请求

取消状态以及类型

int pthread_setcancelstate(int state, int *oldstate);



? PTHREAD_CANCEL_ENABLE:线程可以取消,这是新创建的线程取消性状态的默认值,所以
新建线程以及主线程默认都是可以取消的。
? PTHREAD_CANCEL_DISABLE:线程不可被取消,如果此类线程接收到取消请求,则会将请求
挂起,直至线程的取消性状态变为 PTHREAD_CANCEL_ENABLE


eg. pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);


当发送一个终止线程的请求时,线程时不会终止的,因为这个是“线程不可以被取消”
int pthread_setcanceltype(int type, int *oldtype);


? PTHREAD_CANCEL_DEFERRED:取消请求到来时,线程还是继续运行,取消请求被挂起,直
到线程到达某个取消点为止,这是所有新建线程包括主线程默认的取消性类型


? PTHREAD_CANCEL_ASYNCHRONOUS:可能会在任何时间点(也许是立即取消,但不一定)
取消线程

取消点:
取消点其实就是一系列函数,当执行到这些函数的时候,才会真正响应取消请求,这些函数就是取消点

比如在执行到for,while永久循环的时候,线程就不会被请求终止

线程的可取消性检测
上述提到了当执行到一些永久循环的时候,当发送请求的时候,是无法终止线程,所以
void pthread_testcancel(void);

eg.

static void *new_thread_start(void *arg)
{
 printf("新线程--start run\n");
 for ( ; ; ) {
 pthread_testcancel();
 }
 return (void *)0;
}

10.分离线程

概念:也就是当线程结束的时候,希望线程终止的时候自动回收线程资源并且将其移除

int pthread_detach(pthread_t thread);


线程的分离过程是不可逆的,处于分离状态的线程,当其终止后,则会自动的回收线程资源

一旦分离,就可以通过pthread_join()来获取其终止状态


pthread_detach(pthread_self());    //分离自己

11.线程清理函数

void pthread_cleanup_push(void (*routine)(void *), void *arg);

将线程加入到清理函数栈,方法和栈一样,先入后出


void pthread_cleanup_pop(int execute);

当execute为0的时候,只会去清理函数栈中的最顶层的函数移除,如果非0,可以手动清除函数

12.线程属性

创建的每一个线程都可以配置线程栈的大小以及分配空间

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);


eg. 

/* 对 attr 对象进行初始化 */
 pthread_attr_init(&attr);
 /* 设置栈大小为 4K */
 pthread_attr_setstacksize(&attr, 4096);
    ....
    ....
 /* 销毁 attr 对象 */
 pthread_attr_destroy(&attr);

进程中创建的每一个线程都有自己的栈地址空间,被称为线程栈。也就是代表了每个线程都有属于自己的局部变量

可重入函数:

一个函数被同一个进程的多个不同的执行流同时调用【宏观】

线程同步?

作用:为了对共享资源的访问进行保护,解决数据一致性的问题,防止多个线程对共享资源的并发访问,所以要进行保护

互斥锁

当访问共享资源之前对对互斥锁进行上锁,在访问资源完成后,然后释放互斥锁(解锁),上锁后,任何其他试图再次对互斥锁已经加锁线程都会被阻塞,直到线程释放互斥锁。以此类推,别的企图继续加锁的设备必须要阻塞等待,上一个线程对共享资源解锁后,才可以进行加锁

互斥锁初始化:

1.使用 PTHREAD_MUTEX_INITIALIZER 宏初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2 、使用 pthread_mutex_init() 函数初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

加锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);


当线程未被上锁,则直接上锁,若已经被上锁,则调用该函数,会被阻塞,等待互斥锁解锁后才可以上锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);


对于锁定状态的互斥锁进行解锁
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int g_count = 0;
static void *new_thread_start(void *arg)
{
        int loops = *((int *)arg);
        int l_count, j;
        for (j = 0; j < loops; j++) {
        pthread_mutex_lock(&mutex); //互斥锁上锁
        
        l_count = g_count;
        l_count++;
        g_count = l_count;
        pthread_mutex_unlock(&mutex);//互斥锁解锁
        }
        return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
        pthread_t tid1, tid2;
        int ret;
        /* 获取用户传递的参数 */
        if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
        else
        loops = atoi(argv[1]);
        /* 初始化互斥锁 */
        pthread_mutex_init(&mutex, NULL);
        /* 创建 2 个新线程 */
        ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
        if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
        }
        ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
        if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
        }
        /* 等待线程结束 */
        ret = pthread_join(tid1, NULL);
        if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
        }
        ret = pthread_join(tid2, NULL);
        if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
        }
        /* 打印结果 */
        printf("g_count = %d\n", g_count);
        exit(0);
}

运行结果:???V

pthread_mutex_trylock()函数

当互斥锁已经被其他线程锁住的时候,调用该函数,不会导致线程阻塞,而是直接返回错误码。

int pthread_mutex_trylock(pthread_mutex_t *mutex);
 while(pthread_mutex_trylock(&mutex)); //以非阻塞方式上锁
{
 l_count = g_count;
 l_count++;
 g_count = l_count;
 pthread_mutex_unlock(&mutex);//互斥锁解锁
}

销毁互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

被销毁的互斥锁则不可以进行上锁和解锁,需要再次初始化才可以

互斥锁死锁

当一个线程A一直锁住资源A,但想同时锁住线程B的资源B【线程A处于阻塞态】,线程B又想锁住线程A锁住的共享资源,这会导致两个线程都处于阻塞态,也就是产生了死锁

条件变量:

条件变量用于自动阻塞线程,知道某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的
? 一个线程等待某个条件满足而被阻塞;
? 另一个线程中,条件满足时发出“信号”
生产者和消费者模型:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int g_avail = 0;
/* 消费者线程 */
static void *consumer_thread(void *arg)
{
        for ( ; ; ) {
                pthread_mutex_lock(&mutex);//上锁
                while (g_avail > 0)
                g_avail--; //消费
                pthread_mutex_unlock(&mutex);//解锁
        }
        return (void *)0;
        }
        /* 主线程(生产者) */
int main(int argc, char *argv[])
{
        pthread_t tid;
        int ret;
        /* 初始化互斥锁 */
        pthread_mutex_init(&mutex, NULL);
        /* 创建新线程 */
        ret = pthread_create(&tid, NULL, consumer_thread, NULL);
        if (ret) {
                fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
                exit(-1);
        }
        for ( ; ; ) {
                pthread_mutex_lock(&mutex);//上锁
                g_avail++; //生产
                pthread_mutex_unlock(&mutex);//解锁
        }
        exit(0);
}
主线程生产者一直生产,新线程也一直在循环判断,会导致CPU资源的浪费?
当采用条件变量,当条件未到达,线程处于休眠状态,当满足条件,线程会被唤醒

条件变量初始化

1.宏初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

2.函数初始化

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

attr指针:指向一个 pthread_condattr_t 类型对象,pthread_condattr_t 数据类型用于描述条件变量的属性【和互斥锁一样】

销毁条件变量:

int pthread_cond_destroy(pthread_cond_t *cond);

通知和等待条件变量:

发送信号给一个线程或者多个线程,通知某个共享变量的状态发生了改变

int pthread_cond_broadcast(pthread_cond_t *cond);

可以唤醒所有等待的线程

int pthread_cond_signal(pthread_cond_t *cond);

唤醒至少一个线程,更高效

等待:在收到一个通知之前一直处于阻塞状态

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

cond:条件
mutex:互斥锁的对象

条件变量的判断条件【必须while循环

? 当有多于一个线程在等待条件变量时,任何线程都有可能会率先醒来获取互斥锁,率先醒来获取到 互斥锁的线程可能会对共享变量进行修改,进而改变判断条件的状态。譬如示例代码 12.3.2 中, 如果有两个或更多个消费者线程,当其中一个消费者线程从 pthread_cond_wait() 返回后,它会将全局共享变量 g_avail 的值变成 0 ,导致判断条件的状态由真变成假
? 可能会发出虚假的通知

自旋锁

访问共享资源之前对自旋锁上锁,在访问完成后,释放自旋锁,互斥锁基于自旋锁实现的,自旋锁更为底层

和互斥锁的区别:

互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁在无法获取到锁时,将会在原地“自旋”等待
缺点:
当未获得锁的时候,会一直占用CPU的资源。试图对同一自旋锁两次必然会导致死锁,但是互斥锁却不会
使用场景:
自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;
因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能 被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了 CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁

?自旋锁初始化:

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);


pshared:
? PTHREAD_PROCESS_SHARED:共享自旋锁。该自旋锁可以在多个进程中的线程之间共享
? PTHREAD_PROCESS_PRIVATE:私有自旋锁。只有本进程内的线程才能够使用该自旋锁

销毁自旋锁:

int pthread_spin_destroy(pthread_spinlock_t *lock);

自旋锁加锁和解锁?

int pthread_spin_lock(pthread_spinlock_t *lock);


int pthread_spin_trylock(pthread_spinlock_t *lock);     非阻塞加锁



int pthread_spin_unlock(pthread_spinlock_t *lock);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static pthread_spinlock_t spin;//定义自旋锁
static int g_count = 0;

static void *new_thread_start(void *arg)
{
        int loops = *((int *)arg);
        int l_count, j;
        for (j = 0; j < loops; j++) {
                pthread_spin_lock(&spin); //自旋锁上锁
                
                l_count = g_count;
                l_count++;
                g_count = l_count;
                pthread_spin_unlock(&spin);//自旋锁解锁
        }
        return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
        pthread_t tid1, tid2;
        int ret;
        /* 获取用户传递的参数 */
        if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
        else
        loops = atoi(argv[1]);
        /* 初始化自旋锁(私有) */
        pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
        /* 创建 2 个新线程 */
        ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
        if (ret) {
                fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
                exit(-1);
        }
        ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
        if (ret) {
                fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
                exit(-1);
        }
        /* 等待线程结束 */
        ret = pthread_join(tid1, NULL);
        if (ret) {
                fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
                exit(-1);
        }
        ret = pthread_join(tid2, NULL);
        if (ret) {
                fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
                exit(-1);
        }
        /* 打印结果 */
        printf("g_count = %d\n", g_count);
        /* 销毁自旋锁 */
        pthread_spin_destroy(&spin);
        exit(0);
}

运行结果:

自旋锁会比互斥锁快一点

读写锁

三种状态:
1.读模式下的加锁状态(读加锁状态)

2.写模式下的加锁状态(写加锁状态)

3.不加锁

规则:

? 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞
? 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止

读写锁初始化

1.宏初始化

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

2.函数初始化

int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

销毁读写锁

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

读写锁上锁和解锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
写加锁模式:其他线程调用会失败
读加锁模式:其他线程调用会成功

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
写加锁模式:其他线程调用会失败
读加锁模式:其他线程调用会失败



非阻塞加锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);


int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
解锁
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_rwlock_t rwlock;//定义读写锁
static int g_count = 0;
static void *read_thread(void *arg)
{
        int number = *((int *)arg);
        int j;
        for (j = 0; j < 10; j++) {
                pthread_rwlock_rdlock(&rwlock); //以读模式获取锁
                printf("读线程<%d>, g_count=%d\n", number+1, g_count);
                pthread_rwlock_unlock(&rwlock);//解锁
                sleep(1);
        }
        return (void *)0;
}
static void *write_thread(void *arg)
{
        int number = *((int *)arg);
        int j;
        for (j = 0; j < 10; j++) {
                pthread_rwlock_wrlock(&rwlock); //以写模式获取锁
                printf("写线程<%d>, g_count=%d\n", number+1, g_count+=20);
                pthread_rwlock_unlock(&rwlock);//解锁
                sleep(1);
        }
        return (void *)0;
}
static int nums[5] = {0, 1, 2, 3, 4};
int main(int argc, char *argv[])
{
        pthread_t tid[10];
        int j;
        /* 对读写锁进行初始化 */
        pthread_rwlock_init(&rwlock, NULL);
        /* 创建 5 个读 g_count 变量的线程 */
        for (j = 0; j < 5; j++)
        pthread_create(&tid[j], NULL, read_thread, &nums[j]);
        /* 创建 5 个写 g_count 变量的线程 */
        for (j = 0; j < 5; j++)
        pthread_create(&tid[j+5], NULL, write_thread, &nums[j]);
        /* 等待线程结束 */
        for (j = 0; j < 10; j++)
        pthread_join(tid[j], NULL);//回收线程
        /* 销毁自旋锁 */
        pthread_rwlock_destroy(&rwlock);
        exit(0);
}

运行结果:

?读写锁的属性? ??

int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
获取读写锁的共享属性


int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
设置读写锁的共享属性



pshared:
? PTHREAD_PROCESS_SHARED:共享读写锁。该读写锁可以在多个进程中的线程之间共享;
? PTHREAD_PROCESS_PRIVATE:私有读写锁。只有本进程内的线程才能够使用该读写锁,这是
文章来源:https://blog.csdn.net/weixin_63032791/article/details/135069466
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。