多线程之线程综合分析 (一)

原创 2018年02月07日 21:52:16

1. 基本概念

  • Anytime you try to share a single resource among multiple users, you have to deal with consistency. With multiple threads of control, we can design our programs to do more than one thing at a time within a single process, with each thread handling a separate task.

  • A thread consists of the information necessary to represent an execution context within a process. This includes a thread ID that identifies the thread within a process, a set of register values, a stack, a scheduling priority and policy, a signal mask, an errno variable, and thread-specific data. Everything within a process is sharable among the threads in a process, including the text of the executable program, the program’s global and heap memory, the stacks, and the file descriptors .

  多线程并发的好处就是可以大大提高程序的效率,这个效率是多方面的。但是万事有利有弊,既然引进了一种技术,必然会带来这种技术中存在的弊端,多线程编程的重点在于发挥多线程本身的优点,想尽办法规避一切可能引发的问题。

  与进程相同的是每个线程必然有一个结构来表示其执行上下文,如上述引用所列。既然是由一个进程产生的线程,必然“构造”本身决定先天共享主thread的代码段,全局变量, 及文件描述符等等资源。为了说明后面的一些线程创建的属性,必然有必要对每个线程创建时,堆栈的变化做以阐述:(以下引用来自stackoverflow)

Each thread has a private stack, which it can quickly add and remove items from. This makes stack based memory fast, but if you use too much stack memory, as occurs in infinite recursion, you will get a stack overflow. Since all threads share the same heap, access to the allocator/deallocator must be synchronized. There are various methods and libraries for avoiding allocator contention.

  每个进程的虚拟地址空间大小都是一定的,每个线程都有自己的private stack,随着线程数的增加,所占用的地址可能会超过可用大小,引起stackoverflow。另一方面,有时候我们也需要显示的指定每个进程的stack size,所以在创建进程的时候,就应该去设置相应的属性,这点在接下来的API中会再次分析。

2. 主要API分析

2.1 Thread Identification

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
Returns: nonzero if equal, 0 otherwise
pthread_t pthread_self(void);
Returns: the thread ID of the calling thread

NOTE:Linux 3.2.0 uses an unsigned long integer for the pthread_t data type

  这两个函数配合使用,可以使主线程分辨清楚哪个线程是哪个,所以就能够把不同的工作分配给不同的线程。这也是最基本的编程手段。

2.2 Thread Creation

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr,void *(*start_rtn)(void *), void *restrict arg);
Returns: 0 if OK, error number on failure

When a thread is created, there is no guarantee which will run first: the newly created thread or the calling thread. The newly created thread has access to the process address space and inherits the calling thread’s floating-point environment and signal mask; however, the set of pending signals for the thread is cleared.

  要理解上面引用的话,先回忆下信号篇,介绍了多进程可能存在的竞态,那个时候是指着不知道父子进程调度的先后顺序说的,现在这里面说的竞态是指:Phtread_Create在函数返回前,被创建的线程就已经在运行了。

  到这里有必要提到一个小实验,关于最后void * arg的,也是一个别人提问的问题,现在我来做实验验证下,然后继续关注attr,在基本概念里面提到stack size的问题。所以目前就这点属性做展开:

2.2.1 Attr inital
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
Both return: 0 if OK, error number on failure

If an implementation of pthread_attr_init allocated any dynamic memory for the attribute object, pthread_attr_destroy will free that memory. In addition, pthread_attr_destroy will initialize the attribute object with invalid values, so if it is used by mistake, pthread_create will return an error code.

2.2.2 Attr Stack
#include <pthread.h>
int pthread_attr_getstack(const pthread_attr_t *restrict attr, void **restrict stackaddr, size_t *restrict stacksize);
int pthread_attr_setstack(pthread_attr_t *attr,void *stackaddr, size_t stacksize);
Both return: 0 if OK, error number on failure

The stackaddr thread attribute is defined as the lowest memory address for the stack.

  上面的引用的话并不代表,StackaddrStack的起始的地方。也就是说如果Stack是从高地址往低地址增加的,那么此时依然以最低地址传递给Stackaddr

#include <pthread.h>
int pthread_attr_getstacksize(const pthread_attr_t *restrict attr, size_t *restrict stacksize);
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
Both return: 0 if OK, error number on failure
int pthread_attr_getguardsize(const pthread_attr_t *restrict attr, size_t *restrict guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
Both return: 0 if OK, error number on failure
  • pthread_attr_setstacksize用来设定默认stack的大小
  • pthread_attr_setstack还可以设置stack为malloc或者mmap开辟的另外的区域,但是其成员stackaddr依然应该应该符合上面论述的情况,即最低地址。
  • guardsize用来防止stackoverflow,但是当我们使用pthread_attr_setstack设置我们自己的stack时,pthread_attr_setguardsize中设置guardsize为0将失效这一机制,如果stack pointer进入guard区域(紧接着线程的stack后端),应用程序将会收到error。

2.3 Thread Termination

  如果一个线程调用exit_Exit_exit,整个进程将会结束,如果一个信号的处理方式是结束进程,则任意一个接到到该信号的线程将会结束整个进程。如果仅仅结束线程,则有以下三种方式:

  • The thread can simply return from the start routine. The return value is the thread’s exit code.
  • The thread can be canceled by another thread in the same process.
  • The thread can call pthread_exit.
#include <pthread.h>
void pthread_exit(void *rval_ptr);
int pthread_join(pthread_t thread, void **rval_ptr);
Returns: 0 if OK, error number on failure

  与上面分析void *一样,如果从void *rval_ptrvoid **rval_ptr设计本身的角度来想,rval_ptr传递一个指针自身的值给pthread_join,这是符合C语言特性的:传递一个变量给一个函数,这个变量的值被copy一份,且被调用函数无法改变原本变量的值。同理,传递一个指针给一个函数,这个指针的值被copy一份,我们可以通过这个值,找到指针所指的变量,并改变该变量的值。但是我们无法改变这个指针的值。

  pthread_join等待thread指定的thread以上述三种方式的一种结束,并获得其结束信息,只不过被canceled的线程会设置返回值为PTHREAD_CANCELED。当我们不关心返回值时,pthread_join中的rval_ptr设置为NULL就可以了。接下来考虑cancel的用法:

#include <pthread.h>
int pthread_cancel(pthread_t tid);
Returns: 0 if OK, error number on failure

Note that pthread_cancel doesn’t wait for the thread to terminate; it merely makes the request. (笔者:注意这里我加黑强调)

2.3.1 Cancel Options
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
Returns: 0 if OK, error number on failure

  state可以被设置为PTHREAD_CANCEL_ENABLE 或者PTHREAD_CANCEL_DISABLEoldstate用来存储以前的设置值,默认值为ENABLE即对cancel的请求响应,否则将pending这一请求,直到再次state被设置为ENABLEcancel函数的响应会在cancellation point处执行。有一些函数属于这个cancellation point,我们自己也可以人为设置这个”检查点”:

#include <pthread.h>
void pthread_testcancel(void);

  以上的描述,cancel的行为并不在调用pthread_cancel时发生,如果想改变这点,可以使用下面的函数:

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
Returns: 0 if OK, error number on failure

  pthread_setcancelstate设置statePTHREAD_CANCEL_DEFERREDPTHREAD_CANCEL_ASYNCHRONOUS,最前面我们看到的cancel被延迟到’检查点’执行,而采用异步的方式,将立即使当前线程canceled。

2.3.2 Detached State
#include <pthread.h>
int pthread_detach(pthread_t tid);
Returns: 0 if OK, error number on failure
  • If we are no longer interested in an existing thread’s termination status, we can use pthread_detach to allow the operating system to reclaim the thread’s resources when the thread exits.
  • By calling pthread_join, we automatically place the thread with which we’re joining in the detached state (discussed shortly) so that its resources can be recovered.
  • If we know that we don’t need the thread’s termination status at the time we create the thread, we can arrange for the thread to start out in the detached state by modifying the detachstate thread attribute in the pthread_attr_t structure.

   上面的话,难理解吗?也不难理解,处于Detached State的线程将在结束时释放资源,并且其termination status并不被获取。而调用pthread_join来获取线程的termination status时,该线程自动被设置为detached statepthread_detach在runtime时调用,来设置detached state。而pthread_attr_t中也可以设置detached state已达到在创立线程之初,就将线程设置为detached state

2.3.3 Attr Detach
#include <pthread.h>
int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr, int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
Both return: 0 if OK, error number on failure

  pthread_attr_setdetachstate可以设置detachstatePTHREAD_CREATE_DETACHED或者PTHREAD_CREATE_JOINABLE两个值。

2.3.4 Thread Cleanup Handlers
#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
  • makes a call to pthread_exit
  • responds to a cancellation request
  • makes a call to pthread_cleanup_pop with a nonzero execute argument

  类似于callback函数,当上述三种情况之一发生的时候被调用,由于其被记录在stack中,所以调用顺序与注册顺序相反。可以强制执行cleanup注册的函数:

makes a call to pthread_cleanup_pop with a nonzero execute argument. In either case, pthread_cleanup_pop removes the cleanup handler established by the last call to pthread_cleanup_push.

  由于pthread_cleanup_push...pop都是由宏定义实现,所以必须在代码中成对存在,不然存在编译错误。也就是说:我们不强制调用时,也应该以pthread_cleanup_pop(0)结尾。,另外需要注意的一个重要地方就是线程正常return,并不会调用这些cleanup函数。具体这里有处需要实验的地方,请看实验部分实验二的代码。

3. 实验部分

3.1 实验一 :

关于 void * arg的深入理解实验:

#include <stdio.h>
#include <err.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define NUM 6
pthread_t ntid[NUM];
void * thr_fn(void *arg)
{
        printf("new thread:%d\n",(int)arg);
        return((void *)0);
}
int main(void)
{
        int err;
        int num;
        for( num=0;num<NUM;num++)
        {       err = pthread_create(ntid+num, NULL, thr_fn,(void *)num);
                if (err != 0)
                        errx(1, "can’t create thread:%d\n",num);
        }
        sleep(10);//睡眠足够长来保证所有子线程都能运行结束
        exit(0);
}

  num使用void *强制转换,然后 thr_fn函数里面强制把arg转换为int,要充分理解传参,实参的意义,还有指针的本质。那么理论上这种写法是可以达到效果的。而且不涉及同一地址,各个进程copy了num的值 ,编译和运行结果如下:

[root@localhost ~]# gcc -pthread -o 11_1 11_1.c                   
11_1.c: In function ‘thr_fn’:
11_1.c:10:27: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
  printf("new thread:%d\n",(int)arg);
                           ^
11_1.c: In function ‘main’:
11_1.c:18:48: warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]
  { err = pthread_create(ntid+num, NULL, thr_fn,(void *)num);
                                                ^
[root@localhost ~]# ./11_1
new thread:0
new thread:1
new thread:3
new thread:2
new thread:5
new thread:4

  其实结果和上面分析的一样,除了那些“警告”比较烦人,但是你知道你做了什么,就可以忽略这些警告。如果想改成没错误版,使用原本程序的意图来处理:(void *)&num, *(int *)arg,然后警告就没有了!这样做的结果会使用地址,然而每个程序都使用一个地址,显然会出现一些问题,虽然在上述程序是没有问题的。

3.2 实验二 :

pthread_cleanup_pop的一种使用效果确认

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <err.h>
void cleanup(void *arg)
{
        printf("cleanup: %s\n", (char *)arg);
}
void * thr_fn1(void *arg)
{
        pthread_cleanup_push(cleanup, "thread 1 first handler");
        pthread_cleanup_push(cleanup, "thread 1 second handler");
        printf("thread 1 push complete\n");
        pthread_cleanup_pop(0);
        pthread_cleanup_pop(0);
        pthread_exit((void *)0);
}
int main(void)
{
        int err;
        pthread_t tid1;
        err = pthread_create(&tid1, NULL, thr_fn1, NULL);
        if (err != 0)
                errx(1, "can’t create thread 1");
        err = pthread_join(tid1, NULL);
        if (err != 0)
                errx(1, "can’t join with thread 2");
        exit(0);
}

  运行结果如下:

[root@localhost ~]# ./11_2    
thread 1 push complete

  可以看出来此时,并没有调用cleanup hanlder相关函数,将程序 pthread_cleanup_pop(0);改为 pthread_cleanup_pop(1);,并添加一行语句,具体改动结果及再次运行结果如下:

[root@localhost ~]# vim 11_2.c
........
printf("thread 1 push complete\n");
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
.......
[root@localhost ~]# gcc -pthread -o 11_2 11_2.c
[root@localhost ~]# ./11_2                       
thread 1 push complete
cleanup: thread 1 second handler
cleanup: thread 1 first handler

  可以看出来此时调用了cleanup hanlder相关函数。但并不是在线程退出的时候被调用!

3.3 实验三 :

pass the structure between the threads properly.

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <err.h>
typedef struct dhuang {
        int a;
        int b;
} Dhuang;
Dhuang var;
void * thr_fn1(void *arg1)
{
        Dhuang * arg=arg1;
        arg->a=1;
        arg->b=2;
        printf("thread 1 dhuang struct:%d,%d\n",arg->a,arg->b);
        return((void *)0);
}
void * thr_fn2(void *arg1)
{
        Dhuang * arg=arg1;
        printf("thread 2 dhuang struct:%d,%d\n",arg->a,arg->b);
        return((void *)0);
}
int main(void)
{
        int err;
        pthread_t tid1,tid2;
        err = pthread_create(&tid1, NULL, thr_fn1, (void *)&var);
        if (err != 0)
                errx(1, "can’t create thread 1");
        err = pthread_join(tid1, NULL);
        if (err != 0)
                errx(1, "can’t join with thread 1");
        err = pthread_create(&tid2, NULL, thr_fn2, (void *)&var);
        if (err != 0)
                errx(1, "can’t create thread 1");
        err = pthread_join(tid2, NULL);
        if (err != 0)
                errx(1, "can’t join with thread 2");
        exit(0);
}

  结果如下:

[root@localhost ~]# ./11_3                       
thread 1 dhuang struct:1,2
thread 2 dhuang struct:1,2

  我们正确的传递了这个结构体,并且即使线程1消亡之后,线程2还能正确访问结构体的相应的值。但是因为我们在程序中显示的顺序执行了这两个线程,所以才不会竞态的发生。如果我们这两个线程是并发的执行的话,对这个结构体同时的读写,可能引发我们不期望的结果!

版权声明:本文为博主原创文章,未经博主允许不得转载。

多线程之使用信号量

引言信号量作为GCD的一部分,常用于多线程或任务间协作,当一个任务的执行过程中需要依赖另一个任务时即可使用信号量。实现原理信号量通过信号计数来实现。其使用即计数过程可分为三个部分:创建信号量、等待信号...
  • l964968324
  • l964968324
  • 2015年09月10日 18:56
  • 438

Swift - 多线程实现方式(3) - Grand Central Dispatch(GCD)

1,Swift继续使用Object-C原有的一套线程,包括三种多线程编程技术:(1)NSThread(2)Cocoa NSOperation(NSOperation和NSOperationQueue)...
  • offbye
  • offbye
  • 2016年02月26日 11:24
  • 2404

java多线程——线程间通信之线程等待唤醒机制

三个方法 wait() notify() notifyAll() 三个方法都使用在同步中,因为要对持有锁(又叫监控)的线程操作。 所以要使用在同步中,因为只有同步才具有锁。 为什么这些操作线...
  • u011402596
  • u011402596
  • 2015年04月10日 01:16
  • 895

java 多线程等待与唤醒机制

java 并发编程网站 :http://ifeve.com/java-7-concurrency-cookbook/ 一: 1:JVM线程状态 NEW, RUNNABLE, BLOC...
  • baiducheng
  • baiducheng
  • 2017年12月25日 16:08
  • 60

java 多线程 future 基本原理

/** * Date:2016年9月7日下午7:56:03 * Copyright (c) 2016, www.bwbroad.com All Rights Reserved. * */ p...
  • xuejianxinokok
  • xuejianxinokok
  • 2016年09月12日 22:23
  • 514

多线程之线程初始

从jdk层面讲解线程线程的初始过程以及线程包含哪些属性
  • breaknull
  • breaknull
  • 2015年08月24日 14:44
  • 1029

Java多线程: CAS

悲观锁与乐观锁悲观锁:悲观锁思想认为如果多个线程中使用共享资源,则它们肯定会同时进行修改从而引起冲突,悲观锁的解决方式是共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。syn...
  • eejron
  • eejron
  • 2016年11月16日 17:38
  • 244

Java多线程之线程分类【案例分析】

因为如果是读写或者计算逻辑类的操作,那么当用户线程结束后,这个时候后台进程也就会跟着结束,所以,这个时候守护线程的操作如果还是未完成,那么就会导致错误的产生。 举个例子:现在我们 在一个ja...
  • uniquewonderq
  • uniquewonderq
  • 2015年08月31日 23:33
  • 376

java多线程同步以及线程间通信详解&消费者生产者模式&死锁&Thread.join()(多线程编程之二)

多线程系列教程: java多线程-概念&创建启动&中断&守护线程&优先级&线程状态(一) java多线程同步以及线程间通信详解&消费者生产者模式&死锁&Thread.join()(二) 本篇我...
  • javazejian
  • javazejian
  • 2016年03月13日 16:58
  • 10279

JAVA基础学习(十二)--多线程一线程之间的通信

线程之间的通信
  • ko0491
  • ko0491
  • 2015年09月18日 16:10
  • 206
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:多线程之线程综合分析 (一)
举报原因:
原因补充:

(最多只允许输入30个字)