介绍:什么是线程,线程的优点是什么?

线程在unix下,被称为轻量级的进程,线程虽然不是进程,但却可以看作是unix进程的表亲,同一进程中的多条线程将共享该进程中的全部资源,如虚拟地址空间,文件描述服,和信号处理等等。但同一进程中的多个线程有各自的调用占(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。一个进程可以有很多线程,每条线程执行不同的任务。

线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O 等会产生堵塞的情况的表现性能。在unix系统中,一个进程包含很多东西,包括可执行的程序,以及一大堆诸如文件描述符地址空间等资源。在很多情况下,完成相关任务的不同代码间需要交换数据。如果采用多进程的方式,那么通信就需要在用户空间和内核空间进频繁的切换,开销很多大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据交换)变得高效非常。

hello world(线程创建 , 结束, 等待)

 

创建线程:pthread_create()

线程创建函数包含四个变量:1、一个线程变量名,被创建线程的标识。2、线程的属性指针,缺省为NULL即可。3、被创建线程的程序代码 4、程序的代码的参数:for example :

-pthread_t thrd1;-pthread_attr_t attr; -void thread_function(void argument);-char *some_argument;

1
2
int pthread_create(pthread_t *thread,pthread_attr_t *attr ,void *(*func), void *arg);
返回值 0 成功 ,返回 errcode 失败

 

结束线程:pthread_exit()

线程结束调用pthread_exit(void * retval);//retval用于存放进程结束的退出状态。

 

线程等待:pthread_join()

pthread_create()调用成功后,新线程和老线程谁先执行,谁后执行用户是不知道的,这一块取决于操作系统对县城的调度,如果我们需要等待指定线程结束,需要使用pthread_join()函数,这个函数实际上类似于多进程编程中的waitpid。举例:假设A线程调用pthread_join

试图去操作B线程,该函数将A线程阻塞,直到B线程退出,当B线程退出以后,A线程会收集B线程的返回码。该函数:

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

参数:thread所等待的进程,retval指向某个存储线程返回值的变量

返回值: 0 成功,返回 errcode 错误。

 

调用示例: pthread_join(thrd1,NULL);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/*************************************************************************
    > File Name: thread_hello_world.c 
    > Author: couldtt(fyby)
    > Mail:  fuyunbiyi@gmail.com
    > Created Time: 2013年12月14日 星期六 11时48分50秒
 ************************************************************************/
 
#include <stdio.h> 
#include <stdlib.h> 
#include <pthread.h> 
 
void print_message_function (void *ptr); 
 
int main() 
    int tmp1, tmp2; 
    void *retval; 
    pthread_t thread1, thread2; 
    char *message1 = "thread1"
    char *message2 = "thread2"
 
    int ret_thrd1, ret_thrd2; 
 
    ret_thrd1 = pthread_create(&thread1, NULL, (void *)&print_message_function, (void *) message1); 
    ret_thrd2 = pthread_create(&thread2, NULL, (void *)&print_message_function, (void *) message2); 
 
    // 线程创建成功,返回0,失败返回失败号 
    if (ret_thrd1 != 0) { 
        printf("线程1创建失败\n"); 
    else 
        printf("线程1创建成功\n"); 
    
 
    if (ret_thrd2 != 0) { 
        printf("线程2创建失败\n"); 
    else 
        printf("线程2创建成功\n"); 
    
 
    //同样,pthread_join的返回值成功为0 
    tmp1 = pthread_join(thread1, &retval); 
    printf("thread1 return value(retval) is %d\n", (int)retval); 
    printf("thread1 return value(tmp) is %d\n", tmp1); 
    if (tmp1 != 0) { 
        printf("cannot join with thread1\n"); 
    
    printf("thread1 end\n"); 
 
    tmp2 = pthread_join(thread1, &retval); 
    printf("thread2 return value(retval) is %d\n", (int)retval); 
    printf("thread2 return value(tmp) is %d\n", tmp1); 
    if (tmp2 != 0) { 
        printf("cannot join with thread2\n"); 
    
    printf("thread2 end\n"); 
 
 
void print_message_function( void *ptr ) { 
    int i = 0
    for (i; i<5; i++) { 
        printf("%s:%d\n", (char *)ptr, i); 
    
}

编译:

gcc thread_hello_world.c -o test -lpthread 注意:一定要加上-lpthread,要不然会报错,因为源代码里引用了pthread.H里的东西,所以在gcc进行连接的时候,必须要找到这些库的二进制实现代码。

结果分析:

1、这段代码我运行了两次,可以看到,两次运行的结果是不一样的,从而说明,新线程和老线程谁先执行,谁后执行用户是不知道的,这一块取决于操作系统的调度。

2、这是一个非常简单的例子,hello world 级别的。只使用来掩饰下linux 下 C多线程的使用,在实际应用中由于多线程往往会访问共享的资源(典型的是访问同一个全局变量),因此多个线程间存在着竞争的关系,这就需要对多个线程进行同步,对其访问的数据予以保护。

 

多线程的同步与互斥:

 

方式一:锁

    在主线程中初始化锁为解锁状态

        pthread_mutex_t mutex;

        pthread_mutex_init(&mutex,NULL);

    在编译时初始化锁为解锁状态:

        锁初始化 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

    访问对象时的加锁操作与解锁操作

        加锁 pthread_mutex_lock(&mutex);

        释放锁 pthread_mutex_unlock(&mutex);

不加锁数据不同步

 

我们先来看一个不加锁,多个线程访问同一段数据的程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
1 /*************************************************************************
 2     > File Name: no_mutex.c
 3     > Author: couldtt(fyby)
 4     > Mail: fuyunbiyi@gmail.com
 5     > Created Time: 2013年12月15日 星期日 17时52分24秒
 6  ************************************************************************/
 7
 8 #include <stdio.h>
 9 #include <stdlib.h>
10 #include <pthread.h>
11
12 int sharedi = 0;
13 void increse_num(void);
14
15 int main(){
16     int ret;
17     pthread_t thrd1, thrd2, thrd3;
18
19     ret = pthread_create(&thrd1, NULL, (void *)increse_num, NULL);
20     ret = pthread_create(&thrd2, NULL, (void *)increse_num, NULL);
21     ret = pthread_create(&thrd3, NULL, (void *)increse_num, NULL);
22
23     pthread_join(thrd1, NULL);
24     pthread_join(thrd2, NULL);
25     pthread_join(thrd3, NULL);
26
27     printf("sharedi = %d\n", sharedi);
28
29     return 0;
30
31 }
32
33 void increse_num(void) {
34     long i,tmp;
35     for(i=0; i<=100000; i++) {
36         tmp = sharedi;
37         tmp = tmp + 1;
38         sharedi = tmp;
39     }
40 }

编译运行之后,(不加锁),我们可以知道,每次运行的结果都会不一样,而其运行结果也不符合我们的预期,出现了错误的结果。原因是三个线程竞争访问全局变量sharendi,并且都没有进行相应的同步。

举个例子:当线程thrd1访问到 sharendi的时候,sharendi的值是1000,然后线程thrd1将sharendi的值累加到了1001,可是线程thrd2取得sharendi的时候,sharendi的值是1000,这时候线程thrd2对sharendi的值进行加1的操作,变成了1001,可是这个时候,sharendi的值已经被线程thrd1加到1001了,然而,thrd2并不知情1,所以又将sharendi的值赋值为了1001,从而导致了结果的错误。

    这样来看,我们就需要一个线程互斥的机制来保护sharendi这个变量,让同一时刻,只有一个线程能够访问到这个变量,从而使它的值能够保证正确的变化。

加锁,数据同步

通过枷锁,保证sharendi变量在进行变更的时候,只有一个线程能够收到,并在该线程对其进行操作的时候,其它线程无法对其进行访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
1 /*************************************************************************
 2     > File Name: mutex.c
 3     > Author: couldtt(fyby)
 4     > Mail: fuyunbiyi@gmail.com
 5     > Created Time: 2013年12月15日 星期日 17时52分24秒
 6  ************************************************************************/
 7
 8 #include <stdio.h>
 9 #include <stdlib.h>
10 #include <pthread.h>
11
12 int sharedi = 0;
13 void increse_num(void);
14
15 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
16
17 int main(){
18     int ret;
19     pthread_t thrd1, thrd2, thrd3;
20
21     ret = pthread_create(&thrd1, NULL, (void *)increse_num, NULL);
22     ret = pthread_create(&thrd2, NULL, (void *)increse_num, NULL);
23     ret = pthread_create(&thrd3, NULL, (void *)increse_num, NULL);
24
25     pthread_join(thrd1, NULL);
26     pthread_join(thrd2, NULL);
27     pthread_join(thrd3, NULL);
28
29     printf("sharedi = %d\n", sharedi);
30
31     return 0;
32
33 }
34
35 void increse_num(void) {
36     long i,tmp;
37     for(i=0; i<=100000; i++) {
38     /*加锁*/
39         if (pthread_mutex_lock(&mutex) != 0) {
40            perror("pthread_mutex_lock");
41            exit(EXIT_FAILURE);
42         }
43         tmp = sharedi;
44         tmp = tmp + 1;
45         sharedi = tmp;
46     /*解锁锁*/
47         if (pthread_mutex_unlock(&mutex) != 0) {
48             perror("pthread_mutex_unlock");
49             exit(EXIT_FAILURE);
50         }
51     }
52 }

结果分析:加锁

这一次我们是正确的,锁有效的的保护了我们的数据安全,然而:

1、锁保护的并不是我们的共享变量(或者是说共享内存),对于共享的内存而言,用户是无法直接对其保护的,因为那是物理内存,无法阻止其它程序的代码访问。事实上,锁所以对关键区域进行了保护,在本例中,是因为所有的线程都遵循一个原则,那就是在进入关键区域前加同一把锁,再退出关键区域前释放同一把锁。

2、加锁时会带来额外开销的,加锁的代码去运行速度不如不加锁的快,所以,在使用锁的时候,要合理,再不需要对关键区域进行保护的场景下,我们便不要画蛇添足,为其加锁了。

 

方式二:信号量

所有一个横明显的缺点就是他只有两种状态:锁定,不锁定

信号量本质上是一个非负数的整数计数器,他也被用来控制对公共资源的访问。当公共资源增加的时候,调用信号量增加函数sem_post()对其进行增加,当公共资源减少的时候,调用函数sem_wait()来减少信号量。其实,我们是可以把锁当作一个0-1信号量的。

它是在semaphore.h中定义的,信号量的数据结构为sem_t ,本质上,他是一个long型整数。

 

相关函数:

    初始化信号量: int sem_inin(sem_t *sem,int pshared, unsigned int value);

        成功返回0,失败返回-1

        参数:

        sem:指向信号量结构的一个指针

        pshared:不是0 的时候,该信号量在线程间共享,否则只能为当前进程的所有线程们共享。

        value:信号量的初始值

    信号量减一操作,当sem = 0的时候该函数会堵塞 int sem_wait(sem_t * sem);

        成功返回0 ,失败返回-1

        参数:

        sem:指向信号量的一个指针

    信号量加一操作:int   sem_post(sem_t * sem);

        参数与返回同上

    销毁信号量:=: int sem_destroy(sem_t * sem);

        参数与返回同上

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
1 /*************************************************************************
 2     > File Name: sem.c
 3     > Author: couldtt(fyby)
 4     > Mail: fuyunbiyi@gmail.com
 5     > Created Time: 2013年12月15日 星期日 19时25分08秒
 6  ************************************************************************/
 7
 8 #include <stdio.h>
 9 #include <unistd.h>
10 #include <pthread.h>
11 #include <semaphore.h>
12
13 #define MAXSIZE 10
14
15 int stack[MAXSIZE];
16 int size = 0;
17 sem_t sem;
18
19 // 生产者
20 void provide_data(void) {
21     int i;
22     for (i=0; i< MAXSIZE; i++) {
23         stack[i] = i;
24         sem_post(&sem); //为信号量加1
25     }
26 }
27
28 // 消费者
29 void handle_data(void) {
30     int i;
31     while((i = size++) < MAXSIZE) {
32         sem_wait(&sem);
33         printf("乘法: %d X %d = %d\n", stack[i], stack[i], stack[i]*stack[i]);
34         sleep(1);
35     }
36 }
37
38 int main(void) {
39
40     pthread_t provider, handler;
41
42     sem_init(&sem, 00); //信号量初始化
43     pthread_create(&provider, NULL, (void *)handle_data, NULL);
44     pthread_create(&handler, NULL, (void *)provide_data, NULL);
45     pthread_join(provider, NULL);
46     pthread_join(handler, NULL);
47     sem_destroy(&sem); //销毁信号量
48
49     return 0;
50 }

 

运行结果:

信号量的使用,因为信号量机制的存在,所以代码在handle_data的时候,如果sem_wait(&sem);时,sem为0 ,那么代码会堵塞在sem_wait()上面,从而避免了在stack中访问错误的index而使真个程序崩溃。