前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux笔记(18)| 线程基础(二)

Linux笔记(18)| 线程基础(二)

作者头像
飞哥
发布2020-11-25 10:02:19
5730
发布2020-11-25 10:02:19
举报

上一节里讲了线程的基本概念,和进程的关系等等。这一节来深入一些,讲一讲具体的一些知识。

这一节主要会涉及到线程的创建、阻塞和退出、线程的分离、设置线程属性以及线程的私有数据。

1、线程的创建:

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

这个函数用来创建一个线程,一共有四个参数,第一个参数是线程的标识符,和进程一样,每个线程都有自己的标识符,这是一个输出型参数。第二个参数是线程的属性(线程的属性后面再分析,一般默认即可),第三个参数是一个函数指针,就是创建线程之后这个线程跳到哪里去执行。第四个参数是给第三个参数里的函数指针传参的,因为第三个参数,也就是start_routine它里面的参数只有一个void*,如果要给这个函数指针指向的函数传参,就要通过arg参数,如果有多个参数,可以用结构体封装,然后把结构体的地址放在arg处,最后就会传给start_routine的参数void * 。

如果要获取线程自身的标识符,可以使用函数:

pthread_t pthread_self(void);

如果要比较两个线程的标识符是否相等,可以使用函数:

int pthread_equal(pthread_t tid1,pthread_t tid2);

如果相等返回一个非0值,否则返回一个0值。

另外需要注意的就是编译和线程相关的代码,需要在gcc后加上-l选项,指明要链接的库

gcc test.c -o test -lpthread

2、线程的退出:

void pthread_exit(void*retval);

无返回值,参数retval是线程退出时的状态,其实就是用户传给这个函数的一个参数,或者说提示信息。

线程的阻塞:

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

这个函数是等待回收某个线程,和进程里面的waitpid很相似,第一个参数是线程的标识符,表示要等待回收哪个线程,第二个参数是接收线程退出时的状态,就是上面的retval。成功返回0,失败返回其他值。

为什么要等待回收呢?比如主线程创建了一个子线程,然后子线程去完成一些事情,如果没有等待回收,那么很可能子线程还没有做完它的事情,主线程就退出了,而主线程一退出,子线程就会被强制退出,这不是我们想要的,所以主线程需要等待回收子线程,然后主线程再退出,子线程还没有退出的时候,主线程就会被阻塞。

3、取消和清理线程:

 int pthread_cancel(pthread_t thread);

这个函数用来取消同一进程中的其他线程。

void pthread_cleanup_push(void (*routine)(void *)
void pthread_cleanup_pop(int execute);

这两个函数是线程的清理处理。好像用的不多。

4、分离线程:

int pthread_detach(pthread_t thread);

在Linux中,线程一般有分离和非分离的状态,在默认情况下是非分离的状态,父线程维护子线程的某些信息并等待子线程的退出,如果没有显式调用join函数,子线程退出时父线程维护的信息可能没有得到及时的释放,继而导致堆栈空间不足。而对于分离状态的线程来说,线程结束后,不会有其他的线程等待它,资源会及时释放。

要使线程分离,可以调用上面的函数,成功返回0,失败返回错误编号。

5、线程属性

在第一个线程创建的函数里面,第二个参数是线程的属性,我们一般填入NULL,这样就是采用默认的属性,但是如果我们希望对线程的属性进行设置,也是可以的。

属性的初始化和销毁:

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

首先要调用初始化函数,设置完属性后要调用销毁函数。

它里面只有一个参数,是一个属性的对象,我们需要先定义这样一个对象(或者说变量),然后把它的地址放进去就行了。

初始化完了之后就可以对这个属性对象进行设置了,比如,可以设置属性对象的分离状态

pthread_t threadid;
pthread_attr_t thread_attr;
pthread_attr_init(&thread_attr);
pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED);
pthread_create(&threadid,&thread_attr,threaddeal,NULL);

这样创建出来的线程就是不是默认状态的非分离,而是分离的了。

当然,还有很多其他的属性可以设置,比如设置堆栈大小,堆栈地址,作用域,继承调度,调度策略,调度参数等,这里就不展开了,一般使用默认的就好了。

6、线程的私有数据(难点)

每个线程都有一些属于自己的数据,当线程对这些数据进行操作的时候可以独立地访问他们,而不用担心其他线程和自己争夺所有权,这种数据被称为线程的私有数据。

在分配私有数据之前,需要创建一个键(key),通过这个键才能获取对线程私有数据的访问权。

int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *));

这个函数的第一个参数就是返回一个key,它是一个输出型参数,第二个参数是一个函数指针,它是一个析构函数,当线程退出的时候,如果数据地址已经被置为一个非NULL的值,它会自动跳转到这里去执行,和C++中的析构函数很相似。

如果要取消键关联,可以调用下面的函数

int pthread_key_delete(pthread_key_t key);

线程可以为线程私有数据分配多个键,每个键都可以有一个析构函数与之关联,各个键的析构函数可以不同。

有些线程可能看到某个键值,而其他的线程可能看到一个不同的值,这是一种竞争,如果要解决这种进程,可以使用pthread_once函数

int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));

这个函数可以保证某些初始化代码至多只能被执行一次,第一个参数指向静态的或外部的变量,初始化为PTHREAD_ONCE_INIT。

比如我们可以这样写:

pthread_once_t once_control = PTHREAD_ONCE_INIT;
pthread_once(&once_control,fun);

这可以保证fun只被执行一次。在多线程编程环境下,尽管pthread_once()调用会出现在多个线程中,init_routine()函数仅执行一次,究竟在哪个线程中执行是不定的,是由内核调度来决定。

接下来就是键关联函数:

int pthread_setspecific(pthread_key_t key, const void *pointer);

这个就是将键值和线程私有数据的地址绑定起来,绑定之后,就可以通过键值来获取私有数据的地址了。

私有数据地址获取函数:

void * pthread_getspecific(pthread_key_t key);

这个就是通过键值来获取私有数据的地址,如果没有私有数据和键关联,那么就会返回NULL,否则返回私有数据的地址。

接下来写一个代码,主线程先创建一个键key,然后通过key访问自己的私有数据,然后创建两个子线程,子线程用同样的key去访问私有数据,虽然key相同,但是子线程访问的依然是自己的私有数据,而不是主线程的数据。

程序大致的逻辑就是,主线程先创建一个key,然后检查这个key有没有绑定某个地址,没有就用malloc函数开辟一段内存,然后将这个key和这个地址绑定起来,之后就可以通过这个key访问自己的私有数据了。

子线程用同样的key,去检查key有没有绑定地址,会发现没有绑定,这是因为刚刚绑定的是主线程的私有数据的地址,现在子线程当然是没有绑定的。子线程也用malloc函数开辟一段内存,将这段内存和key绑定起来,之后就可以通过key来访问自己的私有数据了。

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

static pthread_key_t str_key;
static pthread_once_t str_alloc_key_once=PTHREAD_ONCE_INIT;
static void str_alloc_key();
static void str_alloc_destroy_accu(void *accu);



char* str_accumulate(const char* s)
{
  char* accu;
  pthread_once(&str_alloc_key_once,str_alloc_key);
  accu=(char*)pthread_getspecific(str_key);
  if(!accu)
  {
    accu=malloc(1024);
    if(!accu){
      perror("malloc failed");
      return NULL;
    }
    pthread_setspecific(str_key,(void*)accu);
    printf("Thread %lx allocating buffer at %p\n",pthread_self(),accu);
  }
  strcat(accu,s);
  return accu;
}

static void str_alloc_key()
{
  pthread_key_create(&str_key,str_alloc_destroy_accu);
  printf("Thread %lx:alloced key %d\n",pthread_self(),str_key);
}

static void str_alloc_destroy_accu(void *accu)
{
  printf("Thread %lx :freeing buffer at %p\n",pthread_self(),accu);
  free(accu);
}

void *threadDeal(void* arg)
{
  char* str;
  str=str_accumulate("Result of ");
  str=str_accumulate((char*)arg);
  str=str_accumulate(" thread");
  printf("Thread %lx :\"%s\"\n",pthread_self(),str);
  return NULL;
}


int main(int argc,char* argv[])
{
  char *str;
  pthread_t th1,th2;
  str=str_accumulate("Result of ");
  pthread_create(&th1,NULL,threadDeal,(void*)"first");
  pthread_create(&th2,NULL,threadDeal,(void*)"second");
  str=str_accumulate("initial thread");
  printf("Thread %lx %s \n",pthread_self(),str);
  pthread_join(th1,NULL);
  pthread_join(th2,NULL);
  return 0;
}

函数执行结果如下:

这段代码需要好好分析才能看懂它的打印结果,但是实际上它的逻辑不是很复杂。由于操作系统调度的原因,同样的代码打印结果顺序可能有点不一样也是正常的。

关于线程,还有非常重要的一点就是线程的同步,这个到下一节再讲。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 电子技术研习社 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档