多线程编程(Linux C)
多线程编程可以说每个程序员的基本功,同时也是开发中的难点之一,本文以Linux C为例,讲述了线程的创建及常用的几种线程同步的方式,最后对多线程编程进行了总结与思考并给出代码示例。
一、创建线程
多线程编程的第一步,创建线程。创建线程其实是增加了一个控制流程,使得同一进程中存在多个控制流程并发或者并行执行。
线程创建函数,其他函数这里不再列出,可以参考pthread.h
。
#include<pthread.h>
int pthread_create(
pthread_t *restrict thread, /*线程id*/
const pthread_attr_t *restrict attr, /*线程属性,默认可置为NULL,表示线程属性取缺省值*/
void *(*start_routine)(void*), /*线程入口函数*/
void *restrict arg /*线程入口函数的参数*/
);
代码示例:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
char* thread_func1(void* arg) {
pid_t pid = getpid();
pthread_t tid = pthread_self();
printf("%s pid: %u, tid: %u (0x%x)\n", (char*)arg, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid);
char* msg = "thread_func1";
return msg;
}
void* thread_func2(void* arg) {
pid_t pid = getpid();
pthread_t tid = pthread_self();
printf("%s pid: %u, tid: %u (0x%x)\n", (char*)arg, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid);
char* msg = "thread_func2 ";
while(1) {
printf("%s running\n", msg);
sleep(1);
}
return NULL;
}
int main() {
pthread_t tid1, tid2;
if (pthread_create(&tid1, NULL, (void*)thread_func1, "new thread:") != 0) {
printf("pthread_create error.");
exit(EXIT_FAILURE);
}
if (pthread_create(&tid2, NULL, (void*)thread_func2, "new thread:") != 0) {
printf("pthread_create error.");
exit(EXIT_FAILURE);
}
pthread_detach(tid2);
char* rev = NULL;
pthread_join(tid1, (void *)&rev);
printf("%s return.\n", rev);
pthread_cancel(tid2);
printf("main thread end.\n");
return 0;
}
二、线程同步
有时候我们需要多个线程相互协作来执行,这时需要线程间同步。线程间同步的常用方法有:
- 互斥
- 信号量
- 条件变量
我们先看一个未进行线程同步的示例:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#define LEN 100000
int num = 0;
void* thread_func(void* arg) {
for (int i = 0; i< LEN; ++i) {
num += 1;
}
return NULL;
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, (void*)thread_func, NULL);
pthread_create(&tid2, NULL, (void*)thread_func, NULL);
char* rev = NULL;
pthread_join(tid1, (void *)&rev);
pthread_join(tid2, (void *)&rev);
printf("correct result=%d, wrong result=%d.\n", 2*LEN, num);
return 0;
}
运行结果:correct result=200000, wrong result=106860.
。
【1】互斥
这个是最容易理解的,在访问临界资源时,通过互斥,限制同一时刻最多只能有一个线程可以获取临界资源。
其实互斥的逻辑就是:如果访问临街资源发现没有其他线程上锁,就上锁,获取临界资源,期间如果其他线程执行到互斥锁发现已锁住,则线程挂起等待解锁,当前线程访问完临界资源后,解锁并唤醒其他被该互斥锁挂起的线程,等待再次被调度执行。
“挂起等待”和“唤醒等待线程”的操作如何实现?每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先在把自己加入等待队列中,然后置线程状态为睡眠,然后调用调度器函数切换到别的线程。一个线程要唤醒等待队列中的其它线程,只需从等待队列中取出一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤醒的线程。
主要函数如下:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr); /*初始化互斥量*/
int pthread_mutex_destroy(pthread_mutex_t *mutex); /*销毁互斥量*/
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
用互斥解决上面计算结果错误的问题,示例如下:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#define LEN 100000
int num = 0;
void* thread_func(void* arg) {
pthread_mutex_t* p_mutex = (pthread_mutex_t*)arg;
for (int i = 0; i< LEN; ++i) {
pthread_mutex_lock(p_mutex);
num += 1;
pthread_mutex_unlock(p_mutex);
}
return NULL;
}
int main() {
pthread_mutex_t m_mutex;
pthread_mutex_init(&m_mutex, NULL);
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, (void*)thread_func, (void*)&m_mutex);
pthread_create(&tid2, NULL, (void*)thread_func, (void*)&m_mutex);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_mutex_destroy(&m_mutex);
printf("correct result=%d, result=%d.\n", 2*LEN, num);
return 0;
}
运行结果:correct result=200000, result=200000.
如果在互斥中还嵌套有其他互斥代码,需要注意死锁问题。
产生死锁的两种情况:
- 一种情况是:如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,产生死锁。
- 另一种典型的死锁情形是:线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。
如何避免死锁:
- 不用互斥锁(这个很多时候很难办到)
- 写程序时应该尽量避免同时获得多个锁。
- 如果一定有必要这么做,则有一个原则:如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁,则不会出现死锁。 (比如一个程序中用到锁1、锁2、锁3,它们所对应的Mutex变量的地址是锁1<锁2<锁3,那么所有线程在需要同时获得2个或3个锁时都应该按锁1、锁2、锁3的顺序获得。如果要为所有的锁确定一个先后顺序比较困难,则应该尽量使用
pthread_mutex_trylock
调用代替pthread_mutex_lock
调用,以避免死锁。)
【2】条件变量
条件变量概括起来就是:一个线程需要等某个条件成立(而这个条件是由其他线程决定的)才能继续往下执行,现在这个条件不成立,线程就阻塞等待,等到其他线程在执行过程中使这个条件成立了,就唤醒线程继续执行。
相关函数如下:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
举个最容易理解条件变量的例子,“生产者-消费者”模式中,生产者线程向队列中发送数据,消费者线程从队列中取数据,当消费者线程的处理速度大于生产者线程时,会产生队列中没有数据了,一种处理办法是等待一段时间再次“轮询”,但这种处理方式不太好,你不知道应该等多久,这时候条件变量可以很好的解决这个问题。下面是代码:
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>
#define LIMIT 1000
struct data {
int n;
struct data* next;
};
pthread_cond_t condv = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER;
struct data* phead = NULL;
void producer(void* arg) {
printf("producer thread running.\n");
int count = 0;
for (;;) {
int n = rand() % 100;
struct data* nd = (struct data*)malloc(sizeof(struct data));
nd->n = n;
pthread_mutex_lock(&mlock);
struct data* tmp = phead;
phead = nd;
nd->next = tmp;
pthread_mutex_unlock(&mlock);
pthread_cond_signal(&condv);
count += n;
if(count > LIMIT) {
break;
}
sleep(rand()%5);
}
printf("producer count=%d\n", count);
}
void consumer(void* arg) {
printf("consumer thread running.\n");
int count = 0;
for(;;) {
pthread_mutex_lock(&mlock);
if (NULL == phead) {
pthread_cond_wait(&condv, &mlock);
} else {
while(phead != NULL) {
count += phead->n;
struct data* tmp = phead;
phead = phead->next;
free(tmp);
}
}
pthread_mutex_unlock(&mlock);
if (count > LIMIT)
break;
}
printf("consumer count=%d\n", count);
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, (void*)producer, NULL);
pthread_create(&tid2, NULL, (void*)consumer, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
条件变量中的执行逻辑:
关键是理解执行到int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)
这里时发生了什么,其他的都比较容易理解。执行这条函数前需要先获取互斥锁,判断条件是否满足,如果满足执行条件,则继续向下执行后释放锁;如果判断不满足执行条件,则释放锁,线程阻塞在这里,一直等到其他线程通知执行条件满足,唤醒线程,再次加锁,向下执行后释放锁。(简而言之就是:释放锁-->阻塞等待-->唤醒后加锁返回)
上面的例子可能有些繁琐,下面的这个代码示例则更为简洁:
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>
#define NUM 3
pthread_cond_t condv = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER;
void producer(void* arg) {
int n = NUM;
while(n--) {
sleep(1);
pthread_cond_signal(&condv);
printf("producer thread send notify signal. %d\t", NUM-n);
}
}
void consumer(void* arg) {
int n = 0;
while (1) {
pthread_cond_wait(&condv, &mlock);
printf("recv producer thread notify signal. %d\n", ++n);
if (NUM == n) {
break;
}
}
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, (void*)producer, NULL);
pthread_create(&tid2, NULL, (void*)consumer, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
运行结果:
producer thread send notify signal. 1 recv producer thread notify signal. 1
producer thread send notify signal. 2 recv producer thread notify signal. 2
producer thread send notify signal. 3 recv producer thread notify signal. 3
【3】信号量
信号量适用于控制一个仅支持有限个用户的共享资源。用于保持在0至指定最大值之间的一个计数值。当线程完成一次对该semaphore
对象的等待时,该计数值减一;当线程完成一次对semaphore
对象的释放时,计数值加一。当计数值为0时,线程挂起等待,直到计数值超过0.
主要函数如下:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t * sem);
int sem_destroy(sem_t * sem);
代码示例如下:
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>
#include<semaphore.h>
#define NUM 5
int queue[NUM];
sem_t psem, csem;
void producer(void* arg) {
int pos = 0;
int num, count = 0;
for (int i=0; i<12; ++i) {
num = rand() % 100;
count += num;
sem_wait(&psem);
queue[pos] = num;
sem_post(&csem);
printf("producer: %d\n", num);
pos = (pos+1) % NUM;
sleep(rand()%2);
}
printf("producer count=%d\n", count);
}
void consumer(void* arg){
int pos = 0;
int num, count = 0;
for (int i=0; i<12; ++i) {
sem_wait(&csem);
num = queue[pos];
sem_post(&psem);
printf("consumer: %d\n", num);
count += num;
pos = (pos+1) % NUM;
sleep(rand()%3);
}
printf("consumer count=%d\n", count);
}
int main() {
sem_init(&psem, 0, NUM);
sem_init(&csem, 0, 0);
pthread_t tid[2];
pthread_create(&tid[0], NULL, (void*)producer, NULL);
pthread_create(&tid[1], NULL, (void*)consumer, NULL);
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
sem_destroy(&psem);
sem_destroy(&csem);
return 0;
}
信号量的执行逻辑:
当需要获取共享资源时,先检查信号量,如果值大于0,则值减1,访问共享资源,访问结束后,值加1,如果发现有被该信号量挂起的线程,则唤醒其中一个线程;如果检查到信号量为0,则挂起等待。
可参考源码sem_post.c
三、多线程编程总结与思考
最后,我们对多线程编程进行总结与思考。
- 第一点就是在进行多线程编程时一定注意考虑同步的问题,因为多数情况下我们创建多线程的目的是让他们协同工作,如果不进行同步,可能会出现问题。
- 第二点,死锁的问题。在多个线程访问多个临界资源时,处理不当会发生死锁。如果遇到编译通过,运行时卡住了,有可能是发生死锁了,可以先思考一下是那些线程会访问多个临界资源,这样查找问题会快一些。
- 第三点,临界资源的处理,多线程出现问题,很大原因是多个线程访问临界资源时的问题,一种处理方式是将对临界资源的访问与处理全部放到一个线程中,用这个线程服务其他线程的请求,这样只有一个线程访问临界资源就会解决很多问题。
- 第四点,线程池,在处理大量短任务时,我们可以先创建好一个线程池,线程池中的线程不断从任务队列中取任务执行,这样就不用大量创建线程与销毁线程,这里不再细述。
参考文档:pthread.h - threads
多线程编程(Linux C)的更多相关文章
- Linux下的多线程编程
1 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的 Unix也支持线程的概念,但是在一个进程(proces ...
- Linux下多线程编程
一.为什么要引入线程? 使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式.在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维 ...
- Linux高性能server规划——多线程编程(在)
多线程编程 Linux主题概述 线程模型 线程是程序中完毕一个独立任务的完整执行序列.即一个可调度的实体. 依据执行环境和调度者的身份.线程可分为内核线程和用户线程.内核线程,在有的系统上也称为LWP ...
- linux下C++的多线程编程
1. 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的Unix也支持线程的概念,但是在一个进程(proces ...
- C语言多线程编程
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUT ...
- [转]Linux 的多线程编程的高效开发经验
Linux 平台上的多线程程序开发相对应其他平台(比如 Windows)的多线程 API 有一些细微和隐晦的差别.不注意这些 Linux 上的一些开发陷阱,常常会导致程序问题不穷,死锁不断.本文中我们 ...
- 转载自~浮云比翼:Step by Step:Linux C多线程编程入门(基本API及多线程的同步与互斥)
Step by Step:Linux C多线程编程入门(基本API及多线程的同步与互斥) 介绍:什么是线程,线程的优点是什么 线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可 ...
- Linux多线程编程(不限Linux)【转】
——本文一个例子展开,介绍Linux下面线程的操作.多线程的同步和互斥. 前言 线程?为什么有了进程还需要线程呢,他们有什么区别?使用线程有什么优势呢?还有多线程编程的一些细节问题,如线程之间怎样同步 ...
- 【转】 Linux下的多线程编程
作者:gnuhpc 出处:http://www.cnblogs.com/gnuhpc/原文链接:http://www.cnblogs.com/gnuhpc/archive/2012/12/07/280 ...
- Linux 的多线程编程的高效开发经验(转)
http://www.ibm.com/developerworks/cn/linux/l-cn-mthreadps/ 背景 Linux 平台上的多线程程序开发相对应其他平台(比如 Windows)的多 ...
随机推荐
- Mybatisの常见面试题
Mybatis -面试问题 最近准备系统的学一下Mybatis,之前只有粗略的看了下,选了十个常见的面试题 1. #{}和${}的区别是什么? #{}是预编译处理,${}是字符串替换. Mybatis ...
- Arrays工具类常用方法演示
java.util.Arrays是JDK中操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法. 下面我们以int类型数组为例,学习下常用的方法,其他类型数组都差不多. 1.equals( ...
- windows安装TensorFlow和Keras遇到的问题及其解决方法
安装TensorFlow在Windows上,真是让我心力交瘁,想死的心都有了,在Windows上做开发真的让人发狂. 首先说一下我的经历,本来也就是起初,网上说python3.7不支持TensorFl ...
- 关于sql 中delete, truncate,drop的理解与总结
关于文章,本文是在网上查找资料整理而来,方便以后记忆 delete 命令删除的数据可以恢复, truncate 命令删除的数据是不可以恢复的,全删的那种 drop 命令删除的数据不可恢复,连表结构都删 ...
- 2018.10.20 2018NOIP冲刺之酒厂选址
题目传送门 明显能够看出有一个建图求路程的优化 然而发现10000*10000爆空间QAQ 为了做一些初始化方面的优化 我们发现了一个叫做前缀和的东西 可以在环上查到两个之间的最短距离 同时还要做一些 ...
- 2018.8.19 2018暑假集训之maxnum
昨天去做志愿服务去了没写成Q_Q 今天再来一道 先放题面描述 设有n个正整数(n<=20),将它们联成一排,组成一个最大的多位数 设有n个正整数(n<=20),将它们联成一排,组成一个最大 ...
- django基础知识之状态保持session:
状态保持 http协议是无状态的:每次请求都是一次新的请求,不会记得之前通信的状态 客户端与服务器端的一次通信,就是一次会话 实现状态保持的方式:在客户端或服务器端存储与会话有关的数据 存储方式包括c ...
- vim与系统剪切板之间的复制粘贴
背景 vim各种快捷建溜得飞起,然而与系统剪切板之间的复制粘贴一直都是我的痛. 每次需要从vim中拷贝些文字去浏览器搜索,都需要用鼠标选中vim的文字后,Ctrl+c.Ctrl+v,硬生生掐断了纯键盘 ...
- kuangbin专题 专题一 简单搜索 Find a way HDU - 2612
题目链接:https://vjudge.net/problem/HDU-2612 题意:‘@’表示KTV,‘#’表示无法走的地方,‘Y’,'M’表示两个人,他们要在KTV见面,问他们都到达KTV要花费 ...
- reportlab生成pdf
文档地址:https://www.reportlab.com/docs/reportlab-userguide.pdf 源码地址:https://bitbucket.org/rptlab/report ...