我如何在一个单独的线程中更新 GTK+ GUI 的一部分,该线程不断地从另一个进程读取数据?

How can I update a section of a GTK+ GUI in a separate thread, which continuously reads data from another process?

我在 Linux 下编写 GTK+ GUI 以管理其他进程时遇到问题。我绝对不是 GTK+ 专家,而且我似乎无法解决这个问题。

我正在尝试编写一个 GTK+ 应用程序,它应该 运行 其他进程(特别是 iPerf - 网络测量程序 - 客户端和 iPerf 服务器,它们使用 system() 进行管理和 popen()/pclose(),具体取决于用户点击的按钮。

有一些与启动客户端相关的按钮和两个用于启动和停止服务器的按钮,它们调用各自的回调。

服务器启动按钮,特别是调用负责启动线程的回调,该线程应从服务器读取数据(非常异步)并相应地更新 GUI 的一部分,同时 GUI 应该响应执行其他操作(例如启动客户端)。

特别是,iPerf 设置为每 1 秒输出一次新数据,并且每个数据每秒都位于 iPerf 返回的每一行上。

我尝试使用 popen() 从服务器读取数据。

如果我从 GTK+ 回调中使用 gdk_threads_add_idle() 启动 serverParserIdle() 函数(如下所述),它会工作,但有两个大问题阻止程序正常工作:

1) iPerf 输出由 popen() 缓冲并且数据没有像程序应该做的那样实时解析

2)serverParserIdle()线程锁住了GUI,我不能同时做其他操作,比如运行ning一个client,这是我需要做的事情

尝试解决 (2),我尝试将 gdk_threads_add_idle() 更改为 gdk_threads_add_timeout(1000,...)。在这种情况下,GUI 不再被锁定,但 popen 返回 0 并且服务器未启动。你知道为什么吗?

我该怎么做才能解决上面列出的所有问题?

这就是之前提到的serverParserIdle()函数:

static gboolean serverParserIdle(gpointer data) {
    FILE *iperfFp;
    char linebuf[STRSIZE_LINEBUF];
    double goodput, final_goodput;
    char unit_letter;
    int total_datagrams, prev_total_datagrams=-1;
    struct parser_data *parser_data_struct=data;
    gchar *gput_label_str=NULL, *final_gput_label_str=NULL;
    char first_char;


    iperfFp=popen(parser_data_struct->cmd,"r"); //parser_data_struct->cmd contains a string containing the command to launch the iperf server "iperf -s -u -i 1 ..."

    if(!iperfFp) {
        // We enter here if gdk_threads_add_timeout(1000,...) is used to call serverParserIdle()
        return FALSE;
    }

    while(fgets(linebuf,sizeof(linebuf),iperfFp)!=NULL) {
        sscanf(linebuf,"%c %*s %*s %*f %*s %*f %*s %lf %c%*s %*f %*s %*s %d %*s",&first_char,&goodput,&unit_letter,&total_datagrams); // Parse useful data on this line

        if(first_char!='[' || (unit_letter!='K' && unit_letter!='M')) {
            // This is just to discrimate the useful lines
            continue;
        }

        if(unit_letter=='K') {
            goodput=goodput/1000;
        }

        // This is again a way to distinguish the last line of a client-server session from all the other lines
        if(prev_total_datagrams!=-1 && total_datagrams>prev_total_datagrams*2) {
            if(final_gput_label_str) {
                g_free(final_gput_label_str);
            }
            // Update final goodput value in the GUI
            final_goodput=goodput;
            prev_total_datagrams=-1;
            final_gput_label_str=g_strdup_printf("<b><span font=\"70\" foreground=\"blue\">%.2f</span></b>",goodput);
            gtk_label_set_text(GTK_LABEL(parser_data_struct->gput_labels.final_gput_info_label),final_gput_label_str);
        } else {
            if(gput_label_str) {
                g_free(gput_label_str);
            }
            prev_total_datagrams=total_datagrams;

            // Update current goodput value in the GUI (every 1s only when a client is being connected to the server)
            gput_label_str=g_strdup_printf("<b><span font=\"70\" foreground=\"#018729\">%.2f</span></b>",goodput);
            gtk_label_set_text(GTK_LABEL(parser_data_struct->gput_labels.gput_info_label),gput_label_str);
        }

        //fflush(iperfFp); <- tried flushing, but it does not work
    }

    pclose(iperfFp);

    g_free(gput_label_str);
    g_free(final_gput_label_str);


    return FALSE;
}

gdk_threads_add_idle()gdk_threads_add_timeout() 实际上是从回调 (start_server()) 中调用的,它被分配给 main() 中的一个按钮,使用:

g_signal_connect(button,"clicked",G_CALLBACK(start_server),&(data));

非常感谢您。

如果有人感兴趣,这里有一个 Perl 示例。这只是展示了如何在 GTK 事件循环中执行异步操作的基本原理:

#! /usr/bin/env perl
use feature qw(say);
use strict;
use warnings;

use Glib 'TRUE', 'FALSE';
use Gtk3 -init;
use AnyEvent;  # Important: load AnyEvent after Glib!
use AnyEvent::Subprocess;

use constant {
    GTK_STYLE_PROVIDER_PRIORITY_USER => 800,
};

my $window = Gtk3::Window->new( 'toplevel' );
my $grid1 = Gtk3::Grid->new();
$window->add( $grid1 );
my $frame1 = Gtk3::Frame->new('Output');
$frame1->set_size_request(800,600);
$grid1->attach($frame1, 0,0,1,1);
my $scrolled_window = Gtk3::ScrolledWindow->new();
$scrolled_window->set_border_width(5);
$scrolled_window->set_policy('automatic','automatic');
my $textview = Gtk3::TextView->new();
my $buffer = $textview->get_buffer();
$buffer->set_text ("Hello, this is some text\nHello world\n");
$textview->set_wrap_mode('none');
$textview->set_editable(FALSE);
$textview->set_cursor_visible(FALSE);
set_widget_property( $textview, 'font-size', '18px' );
my $bg_color = Gtk3::Gdk::RGBA::parse( "#411934" );
$textview->override_background_color('normal', $bg_color);
my $color = Gtk3::Gdk::RGBA::parse( "#e9e5e8" );
$textview->override_color('normal', $color);
$textview->set_monospace(TRUE);

$scrolled_window->add($textview);
$frame1->add($scrolled_window);
$window->set_border_width(5);
$window->set_default_size( 600, 400 );
$window->set_position('center_always');
$window->show_all();
setup_background_command( $buffer );  # start background command
my $condvar = AnyEvent->condvar;
$window->signal_connect( destroy  => sub { $condvar->send } );
my $done = $condvar->recv;  # enter main loop...

sub setup_background_command {
    my ( $buffer ) = @_;

    my $job = AnyEvent::Subprocess->new(
        delegates     => [ 'StandardHandles', 'CompletionCondvar' ],
        code          => sub { exec 'unbuffer', 'myscript.pl' }
    );
    my $run = $job->run;
    $run->delegate('stdout')->handle->on_read(
        sub {
            my ( $handle ) = @_;
            my $line = $handle->rbuf;
            chomp $line;
            my $iter = $buffer->get_end_iter();
            $buffer->insert( $iter, $line . "\n" );
            $handle->rbuf = ""; # clear buffer
        }
    );
}

sub set_widget_property {
    my ( $widget, $prop, $value ) = @_;

    my $context = $widget->get_style_context();
    my $cls_name = $prop . '_class';
    $context->add_class( $cls_name );
    my $provider = Gtk3::CssProvider->new();
    my $css = sprintf ".%s {%s: %s;}", $cls_name, $prop, $value;
    $provider->load_from_data( $css );
    $context->add_provider($provider, GTK_STYLE_PROVIDER_PRIORITY_USER);
}

这里,在 GTK 事件循环中异步到 运行 的命令是脚本 myscript.pl:

#! /usr/bin/env perl
use feature qw(say);
use strict;
use warnings;

#STDOUT->autoflush(1);
sleep 1;
say "data 1";
sleep 1;
say "data 2";
sleep 1;
say "data 3";

请注意,可以通过取消注释带有 autoflush(1) 的行来使脚本无缓冲。但通常我们必须假设我们不能修改命令的内部结构,所以我使用 unbuffer 到 运行 脚本。

我终于按照pan-mroku的建议解决了我的问题,即使用.

这是相关代码,最终能够从服务器读取信息,因为新行和新数据由服务器本身打印到 stdout , 并相应地更新 GTK+ GUI。

#include <gtk/gtk.h>
#include <errno.h>
// ...


static gboolean serverParser(GIOChannel *source, GIOCondition condition, gpointer data) {
    gchar *linebuf; gsize strsize_linebuf;
    GIOStatus opstatus;
    int scan_retval=0;
    // ...

    opstatus=g_io_channel_read_line(source,&linebuf,&strsize_linebuf,NULL,NULL);
    if(opstatus==G_IO_STATUS_NORMAL && strsize_linebuf!=0) {
        scan_retval=sscanf(linebuf,"%c %*s %f%*[- *]%f %*s %*f %*s %lf %c%*s %*f %*s %*f%*[/ *]%d %*s",&field_1,&field_2,&field_3,&field_4,&field_5,&field_6);

        if(scan_retval==6) {
            // Work with the parsed server data, line by line
        }
    }

    // ...

    g_free(linebuf);
    return TRUE;
}



static void start_server(GtkWidget *widget, gpointer data) {
    // ...
    FILE *iperfFp;
    int iperfFd;
    GIOChannel *iperfIOchannel;

    // ...
    // Start server using stdbuf to get a line buffered output
    iperfFp=popen("stdbuf -o L iperf -s -u","r");

    if(!iperfFp) {
        g_print("Error in launching the server. errno = %d\n",errno);
        return;
    }

    iperfFd=fileno(iperfFp);

    iperfIOchannel=g_io_channel_unix_new(iperfFd);
    g_io_channel_set_flags(iperfIOchannel,G_IO_FLAG_NONBLOCK,NULL);
    g_io_channel_set_line_term(iperfIOchannel,NULL,-1);
    g_io_add_watch(iperfIOchannel,G_IO_IN,serverParser,&(data_struct->parser_pointers));

    // ...
}

// ...

单击开始按钮时,将调用 start_server 回调,它会使用 popen 启动 iPerf 服务器(但对任何其他外部进程也可以这样做)并配置一个新的IO通道。然后,每次服务器自己生成一个新行时,都会调用serverParser来解析所有相关数据。

我必须首先通过调用 stdbuf(使用参数 -o L)来启动外部 iPerf 进程,以便获得行缓冲输出并为每个调用 serverParser该过程生成的行。