介绍:什么是线程,线程的优点是什么?
线程在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, 0 , 0 ); //信号量初始化 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而使真个程序崩溃。